From 4b58751abc02873e0aca53c0e98e00507ac1e026 Mon Sep 17 00:00:00 2001 From: Ayan Sen Date: Sun, 2 Jul 2023 18:21:24 -0700 Subject: [PATCH] merging server repositories Signed-off-by: Ayan Sen --- .../.mvn/wrapper/maven-wrapper.properties | 1 + collector/LICENSE | 202 + collector/Makefile | 32 + collector/README.md | 52 + collector/checkstyles/scalastyle_config.xml | 134 + collector/commons/pom.xml | 77 + .../collector/commons/MetricsSupport.scala | 28 + .../commons/ProtoSpanExtractor.scala | 221 + .../commons/SpanDecoratorFactory.scala | 25 + .../commons/TtlAndOperationNames.java | 44 + .../commons/config/ConfigurationLoader.scala | 217 + .../config/ExtractorConfiguration.scala | 41 + .../config/KafkaProduceConfiguration.scala | 24 + .../commons/health/HealthController.scala | 72 + .../commons/health/HealthStatus.scala | 23 + .../health/HealthStatusChangeListener.scala | 33 + .../health/UpdateHealthStatusFile.scala | 41 + .../commons/logger/LoggerUtils.scala | 50 + .../commons/record/KeyValueExtractor.scala | 25 + .../collector/commons/sink/RecordSink.scala | 27 + .../commons/sink/kafka/KafkaRecordSink.scala | 92 + .../src/test/resources/logback-test.xml | 1 + .../commons/unit/HealthControllerSpec.scala | 52 + .../commons/unit/KeyExtractorSpec.scala | 80 + .../commons/unit/LoggerUtilsSpec.scala | 73 + .../commons/unit/ProtoSpanExtractorSpec.scala | 145 + .../scripts/publish-to-docker-hub.sh | 42 + .../terraform/http-span-collector/main.tf | 77 + .../terraform/http-span-collector/outputs.tf | 0 .../templates/deployment_yaml.tpl | 74 + .../templates/http-span-collector_conf.tpl | 33 + .../http-span-collector/variables.tf | 36 + .../terraform/kinesis-span-collector/main.tf | 80 + .../kinesis-span-collector/outputs.tf | 0 .../templates/deployment_yaml.tpl | 62 + .../templates/kinesis-span-collector_conf.tpl | 54 + .../kinesis-span-collector/variables.tf | 32 + collector/deployment/terraform/main.tf | 69 + collector/deployment/terraform/outputs.tf | 0 collector/deployment/terraform/variables.tf | 17 + collector/haystack-span-decorators/pom.xml | 24 + .../AdditionalTagsSpanDecorator.java | 49 + .../span/decorators/SpanDecorator.java | 10 + .../span/decorators/plugin/config/Plugin.java | 29 + .../plugin/config/PluginConfiguration.java | 33 + .../loader/SpanDecoratorPluginLoader.java | 74 + .../AdditionalTagsSpanDecoratorTest.java | 75 + .../src/test/resources/logback-test.xml | 1 + collector/http/Makefile | 33 + collector/http/README.md | 40 + collector/http/build/docker/Dockerfile | 19 + .../http/build/docker/jmxtrans-agent.xml | 28 + collector/http/build/docker/start-app.sh | 20 + .../app-integration-test.conf | 17 + .../integration-tests/docker-compose.yml | 20 + collector/http/pom.xml | 134 + .../http/src/main/resources/config/base.conf | 37 + collector/http/src/main/resources/logback.xml | 27 + .../span/collector/ProjectConfiguration.scala | 65 + .../http/span/collector/WebServer.scala | 133 + .../authenticator/Authenticator.scala | 29 + .../authenticator/NoopAuthenticator.scala | 29 + .../http/span/collector/json/Span.scala | 71 + .../http/src/test/resources/logback-test.xml | 1 + .../collector/integration/HttpProducer.scala | 52 + .../integration/IntegrationTestSpec.scala | 39 + .../integration/LocalKafkaConsumer.scala | 72 + .../config/TestConfiguration.scala | 24 + .../tests/HttpSpanCollectorSpec.scala | 139 + collector/kinesis/Makefile | 50 + collector/kinesis/README.md | 13 + collector/kinesis/build/docker/Dockerfile | 19 + .../kinesis/build/docker/jmxtrans-agent.xml | 33 + collector/kinesis/build/docker/start-app.sh | 20 + .../app-integration-test.conf | 40 + .../integration-tests/docker-compose.yml | 34 + .../scripts/create-dynamo-table.js | 37 + .../scripts/create-kinesis-stream.js | 41 + .../integration-tests/scripts/package.json | 8 + .../build/integration-tests/scripts/setup.sh | 5 + collector/kinesis/pom.xml | 176 + .../src/main/resources/config/base.conf | 70 + .../kinesis/src/main/resources/logback.xml | 27 + .../haystack/kinesis/span/collector/App.scala | 73 + .../config/ProjectConfiguration.scala | 69 + .../KinesisConsumerConfiguration.scala | 41 + .../collector/kinesis/RecordProcessor.scala | 137 + .../kinesis/client/KinesisConsumer.scala | 110 + .../kinesis/record/TtlAndOperationNames.java | 44 + .../collector/metrics/AppMetricNames.scala | 25 + .../pipeline/KinesisToKafkaPipeline.scala | 61 + .../src/test/resources/config/base.conf | 86 + .../src/test/resources/logback-test.xml | 27 + .../integration/IntegrationTestSpec.scala | 48 + .../integration/LocalKafkaConsumer.scala | 117 + .../integration/LocalKinesisProducer.scala | 59 + .../config/TestConfiguration.scala | 27 + .../tests/KinesisSpanCollectorSpec.scala | 130 + .../unit/tests/ConfigurationLoaderSpec.scala | 86 + .../unit/tests/RecordProcessorSpec.scala | 176 + collector/mvnw | 227 + collector/mvnw.cmd | 143 + collector/pom.xml | 464 + collector/sample-span-decorator/pom.xml | 45 + .../span/decorator/SampleSpanDecorator.java | 32 + ...www.haystack.span.decorators.SpanDecorator | 1 + .../src/main/resources/logback.xml | 27 + commons/.gitignore | 12 + commons/.gitmodules | 3 + .../.mvn/wrapper/MavenWrapperDownloader.java | 110 + commons/.mvn/wrapper/maven-wrapper.jar | Bin 0 -> 48337 bytes commons/.mvn/wrapper/maven-wrapper.properties | 1 + commons/.travis.yml | 20 + commons/CONTRIBUTING.md | 14 + commons/LICENSE | 201 + commons/README.md | 40 + commons/Release.md | 10 + commons/commons/pom.xml | 246 + .../commons/config/ConfigurationLoader.scala | 117 + .../haystack/commons/entities/GraphEdge.scala | 28 + .../commons/entities/GraphVertex.scala | 27 + .../haystack/commons/entities/Interval.scala | 57 + .../haystack/commons/entities/TagKeys.scala | 56 + .../entities/encoders/Base64Encoder.scala | 32 + .../commons/entities/encoders/Encoder.scala | 24 + .../entities/encoders/EncoderFactory.scala | 32 + .../entities/encoders/NoopEncoder.scala | 27 + .../encoders/PeriodReplacementEncoder.scala | 27 + .../commons/graph/GraphEdgeTagCollector.scala | 59 + .../commons/health/HealthController.scala | 90 + .../health/HealthStatusChangeListener.scala | 33 + .../health/UpdateHealthStatusFile.scala | 41 + .../GraphEdgeTimestampExtractor.scala | 34 + .../kstreams/IteratorAgeMetricSupport.scala | 13 + .../MetricDataTimestampExtractor.scala | 36 + .../kstreams/SpanTimestampExtractor.scala | 34 + .../haystack/commons/kstreams/app/Main.scala | 103 + .../kstreams/app/ManagedKafkaStreams.scala | 68 + .../commons/kstreams/app/ManagedService.scala | 44 + .../kstreams/app/StateChangeListener.scala | 53 + .../commons/kstreams/app/StreamsFactory.scala | 107 + .../commons/kstreams/app/StreamsRunner.scala | 54 + .../commons/kstreams/serde/SpanSerde.scala | 75 + .../serde/graph/GraphEdgeKeySerde.scala | 62 + .../serde/graph/GraphEdgeValueSerde.scala | 58 + .../serde/metricdata/MetricDataSerde.scala | 161 + .../serde/metricdata/MetricTankSerde.scala | 205 + .../haystack/commons/logger/LoggerUtils.scala | 49 + .../commons/metrics/MetricsSupport.scala | 44 + .../MaxRetriesAttemptedException.scala | 21 + .../commons/retries/RetryOperation.scala | 112 + .../util/MetricDefinitionKeyGenerator.scala | 19 + .../src/test/resources/logback-test.xml | 1 + .../commons/src/test/resources/sample.conf | 7 + .../config/ConfigurationLoaderSpec.scala | 97 + .../encoders/EncoderFactorySpec.scala | 56 + .../graph/GraphEdgeCollectorSpec.scala | 77 + .../commons/health/HealthControllerSpec.scala | 56 + .../GraphEdgeTimestampExtractorSpec.scala | 44 + .../MetricDataTimestampExtractorSpec.scala | 52 + .../kstreams/SpanTimestampExtractorSpec.scala | 46 + .../kstreams/app/ApplicationSpec.scala | 66 + .../app/ManagedKafkaStreamsSpec.scala | 66 + .../app/StateChangeListenerSpec.scala | 52 + .../kstreams/app/StreamsRunnerSpec.scala | 46 + .../kstreams/serde/SpanSerdeSpec.scala | 37 + .../serde/graph/GraphEdgeKeySerdeSpec.scala | 68 + .../serde/graph/GraphEdgeValueSerdeSpec.scala | 62 + .../metricdata/MetricTankSerdeSpec.scala | 165 + .../commons/logger/LoggerUtilSpec.scala | 94 + .../commons/metrics/MetricRegistySpec.scala | 47 + .../commons/retries/RetryOperationSpec.scala | 167 + .../haystack/commons/unit/UnitTestSpec.scala | 80 + .../MetricDefinitionKeyGeneratorSpec.scala | 37 + commons/idl/pom.xml | 138 + commons/mvnw | 286 + commons/mvnw.cmd | 161 + commons/pom.xml | 522 + commons/scalastyle/scalastyle_config.xml | 136 + docker/.gitignore | 2 + docker/LICENSE | 201 + docker/README.md | 103 + .../configs/aa-manager/docker.conf | 9 + .../configs/aa-mapper/docker.conf | 11 + docker/adaptive-alerting/db_init/init_db.sh | 5 + docker/adaptive-alerting/docker-compose.yml | 163 + docker/adaptive-alerting/sql/build-db.sql | 75 + docker/adaptive-alerting/sql/sample-data.sql | 29 + docker/adaptive-alerting/sql/stored-procs.sql | 73 + .../stubs/__files/get_detectors.json | 10 + .../stubs/__files/get_models.json | 10 + .../stubs/mappings/get-detectors.json | 15 + .../stubs/mappings/get-models.json | 16 + docker/agent/default.conf | 55 + docker/agent/docker-compose.yml | 13 + docker/connectors.json | 3 + docker/docker-compose.yml | 64 + docker/example/blobs/docker-compose.yml | 56 + .../example/opentelemetry/docker-compose.yml | 35 + docker/example/traces/docker-compose.yml | 28 + docker/run.sh | 27 + docker/service-graph/docker-compose.yml | 49 + docker/traces/docker-compose.yml | 63 + docker/trends/docker-compose.yml | 64 + docker/zipkin/docker-compose.yml | 48 + idl/.gitignore | 29 + idl/LICENSE | 201 + idl/README.md | 12 + idl/build.sh | 17 + idl/proto/agent/spanAgent.proto | 39 + idl/proto/api/anomaly/anomalyReader.proto | 52 + .../subscription/subscriptionManagement.proto | 117 + idl/proto/api/traceReader.proto | 178 + idl/proto/backend/storageBackend.proto | 68 + idl/proto/blobs/blob.proto | 30 + idl/proto/blobs/blobAgent.proto | 61 + idl/proto/span.proto | 74 + idl/proto/spanBuffer.proto | 29 + service-graph/.gitignore | 22 + service-graph/CONTRIBUTING.md | 14 + service-graph/LICENSE | 201 + service-graph/Makefile | 32 + service-graph/README.md | 140 + service-graph/Release.md | 10 + service-graph/ReleaseNotes.md | 31 + .../checkstyles/scalastyle_config.xml | 134 + .../scripts/publish-to-docker-hub.sh | 43 + .../terraform/graph-builder/main.tf | 70 + .../terraform/graph-builder/outputs.tf | 7 + .../templates/deployment_yaml.tpl | 84 + .../templates/graph-builder_conf.tpl | 47 + .../terraform/graph-builder/variables.tf | 27 + service-graph/deployment/terraform/main.tf | 64 + .../deployment/terraform/node-finder/main.tf | 70 + .../terraform/node-finder/outputs.tf | 0 .../node-finder/templates/deployment_yaml.tpl | 64 + .../templates/node-finder_conf.tpl | 51 + .../terraform/node-finder/variables.tf | 24 + service-graph/deployment/terraform/outputs.tf | 7 + .../snapshotter/templates/deployment_yaml.tpl | 50 + .../templates/snapshotter_conf.tpl | 3 + .../deployment/terraform/variables.tf | 14 + service-graph/graph-builder/Makefile | 11 + service-graph/graph-builder/README.md | 65 + .../graph-builder/build/docker/Dockerfile | 25 + .../build/docker/jmxtrans-agent.xml | 61 + .../graph-builder/build/docker/start-app.sh | 19 + service-graph/graph-builder/pom.xml | 222 + .../graph-builder/src/main/resources/app.conf | 55 + .../src/main/resources/logback.xml | 27 + .../App.scala | 144 + .../ManagedApplication.scala | 63 + .../config/AppConfiguration.scala | 140 + .../config/entities/CustomRocksDBConfig.scala | 52 + .../config/entities/KafkaConfiguration.scala | 45 + .../entities/ServiceClientConfiguration.scala | 28 + .../entities/ServiceConfiguration.scala | 34 + .../entities/ServiceHttpConfiguration.scala | 28 + .../ServiceThreadsConfiguration.scala | 30 + .../model/EdgeStats.scala | 54 + .../model/EdgeStatsSerde.scala | 60 + .../model/OperationGraph.scala | 26 + .../model/OperationGraphEdge.scala | 40 + .../model/ServiceGraph.scala | 26 + .../model/ServiceGraphEdge.scala | 80 + .../service/HttpService.scala | 68 + .../service/ManagedHttpService.scala | 40 + .../fetchers/LocalOperationEdgesFetcher.scala | 50 + .../fetchers/LocalServiceEdgesFetcher.scala | 51 + .../RemoteOperationEdgesFetcher.scala | 64 + .../fetchers/RemoteServiceEdgesFetcher.scala | 62 + .../GlobalOperationGraphResource.scala | 72 + .../GlobalServiceGraphResource.scala | 75 + .../service/resources/IsWorkingResource.scala | 27 + .../LocalOperationGraphResource.scala | 38 + .../resources/LocalServiceGraphResource.scala | 39 + .../service/resources/Resource.scala | 63 + .../service/utils/EdgesMerger.scala | 54 + .../service/utils/IOUtils.scala | 35 + .../service/utils/QueryTimestampReader.scala | 54 + .../stream/ServiceGraphStreamSupplier.scala | 84 + .../stream/StreamSupplier.scala | 107 + .../commons/scalatest/IntegrationSuite.java | 29 + .../integration/kafka-server.properties | 51 + .../src/test/resources/integration/local.conf | 60 + .../integration/zookeeper.properties | 6 + .../src/test/resources/log4j.properties | 5 + .../src/test/resources/logback-test.xml | 24 + .../src/test/resources/logback.xml | 35 + .../src/test/resources/test/test.conf | 56 + .../test/test_application_server_set.conf | 57 + .../test/resources/test/test_no_app_id.conf | 55 + .../resources/test/test_no_bootstrap.conf | 55 + .../test/resources/test/test_no_consumer.conf | 52 + .../test/resources/test/test_no_producer.conf | 53 + .../service/graph/graph/builder/AppSpec.scala | 249 + .../builder/ManagedApplicationSpec.scala | 125 + .../graph/graph/builder/TestSpec.scala | 23 + .../builder/config/AppConfigurationSpec.scala | 113 + .../graph/builder/kafka/KafkaController.scala | 89 + .../graph/builder/kafka/KafkaLocal.scala | 23 + .../graph/builder/kafka/ZooKeeperLocal.scala | 31 + .../builder/model/EdgeStatsSerdeSpec.scala | 61 + .../graph/builder/model/EdgeStatsSpec.scala | 90 + .../builder/model/ServiceGraphEdgeSpec.scala | 55 + .../service/ManagedHttpServiceSpec.scala | 58 + .../LocalOperationGraphResourceSpec.scala | 49 + .../service/utils/EdgesMergerSpec.scala | 103 + .../builder/stream/StreamSupplierSpec.scala | 66 + service-graph/node-finder/Makefile | 11 + service-graph/node-finder/README.md | 65 + .../node-finder/build/docker/Dockerfile | 24 + .../build/docker/jmxtrans-agent.xml | 86 + .../node-finder/build/docker/start-app.sh | 25 + service-graph/node-finder/pom.xml | 191 + .../node-finder/src/main/resources/app.conf | 43 + .../src/main/resources/logback.xml | 27 + .../service/graph/node/finder/App.scala | 70 + .../node/finder/app/GraphNodeProducer.scala | 62 + .../node/finder/app/LatencyProducer.scala | 62 + .../node/finder/app/SpanAccumulator.scala | 191 + .../graph/node/finder/app/Streams.scala | 234 + .../metadata/MetadataProducerSupplier.scala | 48 + ...MetadataStoreUpdateProcessorSupplier.scala | 47 + .../finder/app/metadata/TopicCreator.scala | 69 + .../node/finder/config/AppConfiguration.scala | 117 + .../finder/config/KafkaConfiguration.scala | 56 + .../config/NodeMetadataConfiguration.scala | 21 + .../graph/node/finder/model/LightSpan.scala | 98 + .../finder/model/ServiceNodeMetadata.scala | 76 + .../graph/node/finder/model/SpanPair.scala | 162 + .../graph/node/finder/utils/SpanUtils.scala | 134 + .../commons/scalatest/IntegrationSuite.java | 29 + .../integration/kafka-server.properties | 51 + .../src/test/resources/integration/local.conf | 45 + .../integration/zookeeper.properties | 6 + .../src/test/resources/log4j.properties | 5 + .../src/test/resources/logback-test.xml | 24 + .../src/test/resources/test/test.conf | 44 + .../test/resources/test/test_no_app_id.conf | 33 + .../resources/test/test_no_bootstrap.conf | 33 + .../test/resources/test/test_no_consumer.conf | 30 + .../resources/test/test_no_metrics_topic.conf | 31 + .../test/resources/test/test_no_producer.conf | 25 + .../com/expedia/www/haystack/TestSpec.scala | 189 + .../commons/kafka/KafkaController.scala | 100 + .../haystack/commons/kafka/KafkaLocal.scala | 23 + .../commons/kafka/ZooKeeperLocal.scala | 31 + .../service/graph/node/finder/AppSpec.scala | 142 + .../finder/app/GraphNodeProducerSpec.scala | 76 + .../node/finder/app/LatencyProducerSpec.scala | 69 + .../node/finder/app/SpanAccumulatorSpec.scala | 456 + .../graph/node/finder/app/StreamsSpec.scala | 79 + .../finder/config/AppConfigurationSpec.scala | 85 + .../node/finder/model/SpanPairSpec.scala | 95 + .../node/finder/utils/SpanUtilsSpec.scala | 120 + service-graph/pom.xml | 538 + service-graph/snapshot-store/README.md | 25 + service-graph/snapshot-store/pom.xml | 104 + .../graph/snapshot/store/Constants.scala | 46 + .../store/DataFramesIntoJsonTransformer.scala | 81 + .../service/graph/snapshot/store/Edge.scala | 96 + .../graph/snapshot/store/EdgeWithIds.scala | 27 + .../snapshot/store/FileSnapshotStore.scala | 116 + .../store/JsonIntoDataFramesTransformer.scala | 143 + .../service/graph/snapshot/store/Node.scala | 22 + .../graph/snapshot/store/NodeWithId.scala | 30 + .../graph/snapshot/store/NodesAndEdges.scala | 20 + .../snapshot/store/S3SnapshotStore.scala | 163 + .../graph/snapshot/store/SnapshotStore.scala | 83 + .../src/test/resources/serviceGraph.json | 2547 +++ .../src/test/resources/serviceGraph_edges.csv | 124 + .../src/test/resources/serviceGraph_nodes.csv | 90 + .../DataFramesIntoJsonTransformerSpec.scala | 66 + .../store/FileSnapshotStoreSpec.scala | 74 + .../JsonIntoDataFramesTransformerSpec.scala | 66 + .../snapshot/store/S3SnapshotStoreSpec.scala | 247 + .../snapshot/store/SnapshotStoreSpec.scala | 48 + .../store/SnapshotStoreSpecBase.scala | 46 + service-graph/snapshotter/Makefile | 11 + service-graph/snapshotter/README.md | 27 + .../snapshotter/build/docker/Dockerfile | 25 + .../build/docker/jmxtrans-agent.xml | 44 + .../snapshotter/build/docker/start-app.sh | 19 + service-graph/snapshotter/pom.xml | 135 + .../snapshotter/src/main/resources/app.conf | 12 + .../src/main/resources/logback.xml | 21 + .../graph/snapshotter/AppConfiguration.scala | 42 + .../service/graph/snapshotter/Main.scala | 115 + .../snapshotter/src/test/resources/app.conf | 7 + .../src/test/resources/serviceGraph.json | 2547 +++ .../src/test/resources/serviceGraph_edges.csv | 124 + .../src/test/resources/serviceGraph_nodes.csv | 90 + .../service/graph/snapshotter/MainSpec.scala | 204 + traces/.gitignore | 13 + .../.mvn/wrapper/MavenWrapperDownloader.java | 110 + traces/.mvn/wrapper/maven-wrapper.jar | Bin 0 -> 48337 bytes traces/.mvn/wrapper/maven-wrapper.properties | 1 + traces/.travis.yml | 39 + traces/CONTRIBUTING.md | 19 + traces/LICENSE | 201 + traces/Makefile | 38 + traces/README.md | 52 + traces/Release.md | 20 + traces/backends/Makefile | 23 + traces/backends/cassandra/Makefile | 24 + traces/backends/cassandra/README.md | 15 + .../cassandra/build/docker/Dockerfile | 30 + .../cassandra/build/docker/jmxtrans-agent.xml | 48 + .../cassandra/build/docker/start-app.sh | 21 + .../integration-tests/docker-compose.yml | 9 + traces/backends/cassandra/pom.xml | 173 + .../src/main/resources/config/base.conf | 62 + .../cassandra/src/main/resources/logback.xml | 27 + .../storage/backends/cassandra/Service.scala | 95 + .../cassandra/client/AwsNodeDiscoverer.scala | 82 + .../client/CassandraClusterFactory.scala | 64 + .../cassandra/client/CassandraSession.scala | 138 + .../client/CassandraTableSchema.scala | 69 + .../cassandra/client/ClusterFactory.scala | 28 + .../config/ProjectConfiguration.scala | 125 + .../AwsNodeDiscoveryConfiguration.scala | 25 + .../config/entities/ClientConfiguration.scala | 74 + .../entities/CredentialsConfiguration.scala | 21 + .../entities/ServiceConfiguration.scala | 23 + .../config/entities/SocketConfiguration.scala | 23 + .../cassandra/metrics/AppMetricNames.scala | 26 + .../cassandra/services/GrpcHandler.scala | 63 + .../services/GrpcHealthService.scala | 31 + .../services/SpansPersistenceService.scala | 58 + ...ssandraTraceRecordReadResultListener.scala | 87 + .../store/CassandraTraceRecordReader.scala | 52 + ...sandraTraceRecordWriteResultListener.scala | 65 + .../store/CassandraTraceRecordWriter.scala | 97 + .../src/test/resources/config/base.conf | 52 + .../src/test/resources/logback-test.xml | 1 + .../integration/BaseIntegrationTestSpec.scala | 139 + ...ageBackendServiceIntegrationTestSpec.scala | 61 + .../cassandra/unit/BaseUnitTestSpec.scala | 22 + .../unit/client/AwsNodeDiscovererSpec.scala | 55 + .../unit/client/CassandraSessionSpec.scala | 116 + .../client/CassandraTableSchemaSpec.scala | 133 + .../unit/config/ConfigurationLoaderSpec.scala | 65 + traces/backends/memory/Makefile | 16 + traces/backends/memory/README.md | 14 + .../backends/memory/build/docker/Dockerfile | 30 + .../memory/build/docker/jmxtrans-agent.xml | 13 + .../backends/memory/build/docker/start-app.sh | 21 + traces/backends/memory/pom.xml | 142 + .../src/main/resources/config/base.conf | 9 + .../memory/src/main/resources/logback.xml | 28 + .../storage/backends/memory/Service.scala | 93 + .../memory/config/ProjectConfiguration.scala | 36 + .../entities/ServiceConfiguration.scala | 23 + .../memory/services/GrpcHealthService.scala | 31 + .../services/SpansPersistenceService.scala | 54 + .../store/InMemoryTraceRecordStore.scala | 71 + .../src/test/resources/config/base.conf | 10 + .../src/test/resources/logback-test.xml | 1 + .../integration/BaseIntegrationTestSpec.scala | 87 + ...aceBackendServiceIntegrationTestSpec.scala | 49 + .../memory/unit/BaseUnitTestSpec.scala | 22 + .../unit/config/ConfigurationLoaderSpec.scala | 33 + traces/backends/pom.xml | 27 + traces/checkstyles/scalastyle_config.xml | 134 + traces/commons/pom.xml | 121 + .../es/AWSSigningJestClientFactory.scala | 70 + .../es/document/ServiceMetadataDoc.scala | 26 + .../clients/es/document/TraceIndexDoc.scala | 38 + .../AWSRequestSigningConfiguration.scala | 33 + .../config/entities/ReloadConfiguration.scala | 37 + .../config/entities/TraceStoreBackends.scala | 32 + .../WhitelistIndexFieldConfiguration.scala | 121 + ...igurationReloadElasticSearchProvider.scala | 87 + .../reload/ConfigurationReloadProvider.scala | 41 + .../commons/config/reload/Reloadable.scala | 30 + .../trace/commons/packer/PackedMessage.scala | 50 + .../trace/commons/packer/Packer.scala | 77 + .../trace/commons/packer/PackerFactory.scala | 32 + .../trace/commons/packer/Unpacker.scala | 80 + .../trace/commons/utils/SpanUtils.scala | 159 + .../src/test/resources/logback-test.xml | 1 + .../trace/commons/unit/BaseUnitTestSpec.scala | 24 + .../trace/commons/unit/PackerSpec.scala | 61 + .../commons/unit/TraceIndexDocSpec.scala | 34 + ...WhitelistIndexFieldConfigurationSpec.scala | 79 + .../scripts/publish-to-docker-hub.sh | 43 + .../curator-service-metadata/main.tf | 29 + .../curator-service-metadata/outputs.tf | 0 .../templates/curator-cron-job-yaml.tpl | 79 + .../curator-service-metadata/variables.tf | 7 + .../es-indices/curator-trace-index/main.tf | 29 + .../es-indices/curator-trace-index/outputs.tf | 0 .../templates/curator-cron-job-yaml.tpl | 79 + .../curator-trace-index/variables.tf | 7 + .../deployment/terraform/es-indices/main.tf | 29 + .../terraform/es-indices/outputs.tf | 0 .../terraform/es-indices/variables.tf | 7 + .../es-indices/whitelisted-fields/main.tf | 29 + .../es-indices/whitelisted-fields/outputs.tf | 0 .../templates/whitelisted-fields-pod-yaml.tpl | 44 + .../whitelisted-fields/variables.tf | 7 + traces/deployment/terraform/main.tf | 71 + traces/deployment/terraform/outputs.tf | 7 + .../terraform/trace-indexer/main.tf | 82 + .../terraform/trace-indexer/outputs.tf | 0 .../trace-indexer/templates/deployment.yaml | 97 + .../templates/trace-indexer.conf | 157 + .../terraform/trace-indexer/variables.tf | 35 + .../deployment/terraform/trace-reader/main.tf | 79 + .../terraform/trace-reader/outputs.tf | 7 + .../trace-reader/templates/deployment.yaml | 111 + .../trace-reader/templates/trace-reader.conf | 97 + .../terraform/trace-reader/variables.tf | 36 + traces/deployment/terraform/variables.tf | 19 + traces/indexer/Makefile | 20 + traces/indexer/README.md | 33 + traces/indexer/build/docker/Dockerfile | 24 + .../indexer/build/docker/jmxtrans-agent.xml | 39 + traces/indexer/build/docker/start-app.sh | 21 + .../integration-tests/docker-compose.yml | 8 + traces/indexer/pom.xml | 236 + .../src/main/resources/config/base.conf | 160 + traces/indexer/src/main/resources/logback.xml | 27 + .../www/haystack/trace/indexer/App.scala | 78 + .../haystack/trace/indexer/StreamRunner.scala | 132 + .../indexer/config/ProjectConfiguration.scala | 282 + .../entities/ElasticSearchConfiguration.scala | 56 + .../config/entities/KafkaConfiguration.scala | 44 + .../ServiceMetadataWriteConfiguration.scala | 58 + .../SpanAccumulatorConfiguration.scala | 33 + .../entities/TraceBackendConfiguration.scala | 27 + .../indexer/metrics/AppMetricNames.scala | 42 + .../processors/SpanIndexProcessor.scala | 133 + .../indexer/processors/StateListener.scala | 29 + .../indexer/processors/StreamProcessor.scala | 28 + .../processors/StreamTaskRunnable.scala | 265 + .../supplier/SpanIndexProcessorSupplier.scala | 39 + .../supplier/StreamProcessorSupplier.scala | 24 + .../indexer/serde/SpanDeserializer.scala | 50 + .../indexer/store/DynamicCacheSizer.scala | 66 + .../store/SpanBufferMemoryStoreSupplier.scala | 34 + .../trace/indexer/store/StoreSupplier.scala | 22 + .../data/model/SpanBufferWithMetadata.scala | 25 + .../store/impl/SpanBufferMemoryStore.scala | 129 + .../store/traits/CacheSizeObserver.scala | 26 + .../EldestBufferedSpanEvictionListener.scala | 27 + .../traits/SpanBufferKeyValueStore.scala | 62 + .../trace/indexer/writers/TraceWriter.scala | 32 + .../es/ElasticSearchResultHandler.scala | 64 + .../writers/es/ElasticSearchWriter.scala | 165 + .../writers/es/IndexDocumentGenerator.scala | 158 + .../writers/es/IndexTemplateHandler.scala | 106 + .../es/ServiceMetadataDocumentGenerator.scala | 84 + .../writers/es/ServiceMetadataWriter.scala | 167 + .../writers/es/ThreadSafeBulkBuilder.scala | 79 + .../writers/grpc/GrpcTraceWriter.scala | 98 + .../grpc/WriteSpansResponseObserver.scala | 55 + .../indexer/writers/kafka/KafkaWriter.scala | 53 + .../src/test/resources/config/base.conf | 155 + .../src/test/resources/logback-test.xml | 1 + .../integration/BaseIntegrationTestSpec.scala | 327 + .../integration/EvictedSpanBufferSpec.scala | 52 + .../FailedTopologyRecoverySpec.scala | 97 + .../MultipleTraceIndexingTopologySpec.scala | 88 + .../PartialTraceIndexingTopologySpec.scala | 101 + .../ServiceMetadataIndexingTopologySpec.scala | 68 + .../clients/ElasticSearchTestClient.scala | 261 + .../integration/clients/GrpcTestClient.scala | 66 + .../integration/clients/KafkaTestClient.scala | 95 + .../integration/serdes/TestSerdes.scala | 46 + .../unit/ConfigurationLoaderSpec.scala | 118 + .../indexer/unit/DynamicCacheSizerSpec.scala | 74 + .../unit/ElasticSearchResultHandlerSpec.scala | 101 + .../unit/ElasticSearchWriterUtilsSpec.scala | 48 + .../unit/IndexTemplateHandlerSpec.scala | 173 + .../unit/SpanBufferMemoryStoreSpec.scala | 124 + .../unit/SpanIndexDocumentGeneratorSpec.scala | 296 + .../indexer/unit/SpanIndexProcessorSpec.scala | 140 + .../trace/indexer/unit/SpanSerdeSpec.scala | 68 + .../unit/ThreadSafeBulkBuilderSpec.scala | 109 + traces/mvnw | 286 + traces/mvnw.cmd | 161 + traces/pom.xml | 528 + traces/reader/Makefile | 23 + traces/reader/README.md | 14 + traces/reader/build/docker/Dockerfile | 30 + traces/reader/build/docker/jmxtrans-agent.xml | 33 + traces/reader/build/docker/start-app.sh | 21 + .../build/integration-tests/docker-app.conf | 66 + .../integration-tests/docker-compose.yml | 8 + traces/reader/pom.xml | 181 + .../src/main/resources/config/base.conf | 109 + traces/reader/src/main/resources/logback.xml | 27 + .../www/haystack/trace/reader/Service.scala | 96 + .../reader/config/ProviderConfiguration.scala | 219 + .../entities/ElasticSearchConfiguration.scala | 40 + .../entities/ServiceConfiguration.scala | 23 + .../TraceTransformersConfiguration.scala | 20 + .../TraceValidatorsConfiguration.scala | 20 + .../exceptions/ElasticSearchClientError.scala | 22 + .../exceptions/InvalidTraceException.scala | 22 + .../exceptions/InvalidTraceIdInDocument.scala | 22 + .../exceptions/SpanNotFoundException.scala | 21 + .../exceptions/TraceNotFoundException.scala | 21 + .../trace/reader/metrics/AppMetricNames.scala | 31 + .../trace/reader/readers/TraceProcessor.scala | 37 + .../trace/reader/readers/TraceReader.scala | 156 + .../ClientServerEventLogTransformer.scala | 38 + .../ClockSkewFromParentTransformer.scala | 84 + .../transformers/ClockSkewTransformer.scala | 118 + .../DeDuplicateSpanTransformer.scala | 37 + .../InfrastructureTagTransformer.scala | 59 + .../InvalidParentTransformer.scala | 34 + .../transformers/InvalidRootTransformer.scala | 79 + .../OrphanedTraceTransformer.scala | 62 + .../transformers/PartialSpanTransformer.scala | 39 + .../PostTraceTransformationHandler.scala | 50 + .../ServerClientSpanMergeTransformer.scala | 41 + .../transformers/SortSpanTransformer.scala | 36 + .../transformers/SpanTreeTransformer.scala | 23 + .../TraceTransformationHandler.scala | 46 + .../transformers/TraceTransformer.scala | 23 + .../reader/readers/utils/AuxiliaryTags.scala | 42 + .../reader/readers/utils/SpanMerger.scala | 158 + .../trace/reader/readers/utils/SpanTree.scala | 160 + .../reader/readers/utils/TagBuilders.scala | 43 + .../reader/readers/utils/TagExtractors.scala | 40 + .../reader/readers/utils/TraceMerger.scala | 35 + .../validators/ParentIdValidator.scala | 43 + .../readers/validators/RootValidator.scala | 39 + .../readers/validators/TraceIdValidator.scala | 37 + .../validators/TraceValidationHandler.scala | 36 + .../readers/validators/TraceValidator.scala | 25 + .../trace/reader/services/GrpcHandler.scala | 64 + .../reader/services/GrpcHealthService.scala | 31 + .../trace/reader/services/TraceService.scala | 135 + .../reader/stores/EsIndexedTraceStore.scala | 149 + .../trace/reader/stores/ResponseParser.scala | 118 + .../trace/reader/stores/TraceStore.scala | 30 + .../reader/stores/readers/es/ESUtils.scala | 27 + .../es/ElasticSearchCountResultListener.scala | 64 + .../es/ElasticSearchReadResultListener.scala | 63 + .../readers/es/ElasticSearchReader.scala | 99 + .../es/ElasticSearchResultListener.scala | 23 + .../es/query/FieldValuesQueryGenerator.scala | 51 + .../query/ServiceMetadataQueryGenerator.scala | 59 + .../es/query/SpansIndexQueryGenerator.scala | 218 + .../es/query/TraceCountsQueryGenerator.scala | 100 + .../es/query/TraceSearchQueryGenerator.scala | 99 + .../readers/grpc/GrpcTraceReaders.scala | 99 + .../grpc/ReadSpansResponseListener.scala | 82 + .../src/test/resources/config/base.conf | 99 + .../src/test/resources/logback-test.xml | 1 + .../reader/src/test/resources/raw_trace.json | 651 + .../integration/BaseIntegrationTestSpec.scala | 338 + .../TraceServiceIntegrationTestSpec.scala | 538 + .../trace/reader/unit/BaseUnitTestSpec.scala | 22 + .../unit/config/ConfigurationLoaderSpec.scala | 88 + .../reader/unit/readers/TraceMergerSpec.scala | 61 + .../unit/readers/TraceProcessorSpec.scala | 383 + .../builders/ClockSkewedTraceBuilder.scala | 321 + .../builders/MultiRootTraceBuilder.scala | 61 + .../MultiServerSpanTraceBuilder.scala | 62 + .../unit/readers/builders/TraceBuilder.scala | 40 + .../readers/builders/ValidTraceBuilder.scala | 175 + .../ClientServerEventLogTransformerSpec.scala | 93 + .../ClockSkewFromParentTransformerSpec.scala | 99 + .../ClockSkewTransformerSpec.scala | 175 + .../DeDuplicateSpanTransformerSpec.scala | 46 + .../InfrastructureTagTransformerSpec.scala | 56 + .../InvalidParentTransformerSpec.scala | 94 + .../InvalidRootTransformerSpec.scala | 209 + .../OrphanedTraceTransformerSpec.scala | 67 + .../PartialSpanTransformerSpec.scala | 262 + ...ServerClientSpanMergeTransformerSpec.scala | 254 + .../SortSpanTransformerSpec.scala | 89 + .../validators/ParentIdValidatorSpec.scala | 77 + .../validators/RootValidatorSpec.scala | 77 + .../validators/TraceIdValidatorSpec.scala | 72 + .../unit/stores/ResponseParserSpec.scala | 119 + .../ElasticSearchReadResultListenerSpec.scala | 111 + .../query/FieldValuesQueryGeneratorSpec.scala | 73 + .../ServiceMetadataQueryGeneratorSpec.scala | 55 + .../query/TraceCountsQueryGeneratorSpec.scala | 230 + .../query/TraceSearchQueryGeneratorSpec.scala | 189 + .../query/helper/ExpressionTreeBuilder.scala | 103 + .../grpc/ReadSpansResponseListenerSpec.scala | 122 + trends/.gitignore | 11 + trends/.mvn/wrapper/maven-wrapper.jar | Bin 0 -> 48336 bytes trends/.mvn/wrapper/maven-wrapper.properties | 1 + trends/.travis.yml | 32 + trends/CONTRIBUTING.md | 14 + trends/LICENSE | 201 + trends/Makefile | 36 + trends/README.md | 77 + trends/Release.md | 10 + .../scripts/publish-to-docker-hub.sh | 43 + trends/deployment/terraform/main.tf | 74 + .../deployment/terraform/metrictank/main.tf | 48 + .../terraform/metrictank/outputs.tf | 7 + .../metrictank/templates/deployment_yaml.tpl | 66 + .../terraform/metrictank/variables.tf | 21 + trends/deployment/terraform/outputs.tf | 7 + .../span-timeseries-transformer/main.tf | 75 + .../span-timeseries-transformer/outputs.tf | 0 .../templates/deployment_yaml.tpl | 64 + .../span-timeseries-transformer_conf.tpl | 31 + .../span-timeseries-transformer/variables.tf | 26 + .../terraform/timeseries-aggregator/main.tf | 84 + .../timeseries-aggregator/outputs.tf | 0 .../templates/deployment_yaml.tpl | 64 + .../templates/timeseries-aggregator_conf.tpl | 73 + .../timeseries-aggregator/variables.tf | 36 + trends/deployment/terraform/variables.tf | 24 + trends/documents/diagrams/haystack_trends.png | Bin 0 -> 44638 bytes trends/mvnw | 286 + trends/mvnw.cmd | 161 + trends/pom.xml | 556 + trends/scalastyle/scalastyle_config.xml | 136 + trends/span-timeseries-transformer/Makefile | 19 + trends/span-timeseries-transformer/README.md | 35 + .../build/docker/Dockerfile | 23 + .../build/docker/jmxtrans-agent.xml | 115 + .../build/docker/start-app.sh | 24 + trends/span-timeseries-transformer/pom.xml | 113 + .../src/main/resources/config/base.conf | 36 + .../src/main/resources/logback.xml | 27 + .../com/expedia/www/haystack/trends/App.scala | 72 + .../haystack/trends/MetricDataGenerator.scala | 74 + .../expedia/www/haystack/trends/Streams.scala | 69 + .../trends/config/AppConfiguration.scala | 92 + .../config/entities/KafkaConfiguration.scala | 36 + .../entities/TransformerConfiguration.scala | 29 + .../transformer/MetricDataTransformer.scala | 84 + .../SpanDurationMetricDataTransformer.scala | 47 + .../SpanStatusMetricDataTransformer.scala | 71 + .../src/test/resources/config/base.conf | 25 + .../src/test/resources/logback-test.xml | 1 + .../haystack/trends/feature/FeatureSpec.scala | 69 + .../config/ConfigurationLoaderSpec.scala | 83 + .../feature/tests/kstreams/StreamsSpec.scala | 31 + .../transformer/MetricDataGeneratorSpec.scala | 167 + ...panDurationMetricDataTransformerSpec.scala | 81 + .../SpanStatusMetricDataTransformerSpec.scala | 248 + .../integration/IntegrationTestSpec.scala | 120 + .../TimeSeriesTransformerTopologySpec.scala | 125 + trends/timeseries-aggregator/Makefile | 19 + trends/timeseries-aggregator/README.md | 35 + .../build/docker/Dockerfile | 23 + .../build/docker/jmxtrans-agent.xml | 120 + .../build/docker/start-app.sh | 24 + trends/timeseries-aggregator/pom.xml | 107 + .../src/main/resources/config/base.conf | 74 + .../src/main/resources/logback.xml | 28 + .../com/expedia/www/haystack/trends/App.scala | 77 + .../aggregation/TrendHdrHistogram.scala | 81 + .../trends/aggregation/TrendMetric.scala | 140 + .../trends/aggregation/WindowedMetric.scala | 126 + .../aggregation/entities/StatValue.scala | 18 + .../aggregation/entities/TimeWindow.scala | 40 + .../aggregation/metrics/CountMetric.scala | 70 + .../aggregation/metrics/HistogramMetric.scala | 90 + .../trends/aggregation/metrics/Metric.scala | 84 + .../rules/DurationMetricRule.scala | 36 + .../aggregation/rules/FailureMetricRule.scala | 36 + .../aggregation/rules/LatencyMetricRule.scala | 37 + .../trends/aggregation/rules/MetricRule.scala | 39 + .../aggregation/rules/MetricRuleEngine.scala | 36 + .../aggregation/rules/SuccessMetricRule.scala | 37 + .../trends/config/AppConfiguration.scala | 189 + .../HistogramMetricConfiguration.scala | 47 + .../config/entities/KafkaConfiguration.scala | 37 + .../entities/KafkaProduceConfiguration.scala | 30 + .../entities/StateStoreConfiguration.scala | 30 + .../www/haystack/trends/kstream/Streams.scala | 119 + .../AdditionalTagsProcessorSupplier.scala | 32 + .../ExternalKafkaProcessorSupplier.scala | 81 + .../MetricAggProcessorSupplier.scala | 132 + .../kstream/serde/TrendMetricSerde.scala | 145 + .../kstream/serde/WindowedMetricSerde.scala | 125 + .../serde/metric/CountMetricSerde.scala | 41 + .../serde/metric/HistogramMetricSerde.scala | 70 + .../kstream/serde/metric/MetricSerde.scala | 8 + .../kstream/store/HaystackStoreBuilder.scala | 52 + .../src/test/resources/config/base.conf | 66 + .../src/test/resources/logback-test.xml | 1 + .../haystack/trends/feature/FeatureSpec.scala | 84 + .../tests/aggregation/TrendMetricSpec.scala | 151 + .../aggregation/WindowedMetricSpec.scala | 163 + .../aggregation/metrics/CountMetricSpec.scala | 83 + .../metrics/HistogramMetricSpec.scala | 126 + .../rules/DurationMetricRuleSpec.scala | 55 + .../rules/FailureMetricRuleSpec.scala | 54 + .../rules/LatencyMetricRuleSpec.scala | 55 + .../rules/SuccessMetricRuleSpec.scala | 55 + .../config/ConfigurationLoaderSpec.scala | 144 + .../feature/tests/kstreams/StreamsSpec.scala | 29 + .../AdditionalTagsProcessorSupplierSpec.scala | 79 + .../MetricAggProcessorSupplierSpec.scala | 82 + .../kstreams/serde/TrendMetricSerdeSpec.scala | 114 + .../serde/WindowedMetricSerdeSpec.scala | 88 + .../store/HaystackStoreBuilderSpec.scala | 27 + .../integration/IntegrationTestSpec.scala | 227 + .../integration/tests/CountTrendsSpec.scala | 62 + .../tests/HistogramTrendsSpec.scala | 61 + .../integration/tests/StateStoreSpec.scala | 59 + .../integration/tests/WatermarkingSpec.scala | 42 + ui/.babelrc | 26 + ui/.coveralls.yml | 2 + ui/.dockerignore | 1 + ui/.eslintignore | 4 + ui/.eslintrc | 75 + ui/.gitignore | 31 + ui/.gitmodules | 3 + ui/.npmrc | 1 + ui/.prettierignore | 2 + ui/.prettierrc | 7 + ui/.travis.yml | 29 + ui/.vscode/settings.json | 8 + ui/CONTRIBUTING.md | 14 + ui/LICENSE | 201 + ui/Makefile | 23 + ui/README.md | 122 + ui/Release.md | 10 + ui/build/docker/Dockerfile | 28 + ui/build/docker/publish-to-docker-hub.sh | 38 + ui/build/zipkin/README.md | 26 + ui/build/zipkin/base.json | 15 + ui/build/zipkin/zipkin-quickstart.sh | 116 + ui/deployment/terraform/main.tf | 79 + ui/deployment/terraform/outputs.tf | 0 .../terraform/templates/deployment_yaml.tpl | 59 + .../terraform/templates/haystack-ui_json.tpl | 47 + ui/deployment/terraform/variables.tf | 51 + ui/package-lock.json | 14057 ++++++++++++++++ ui/package.json | 159 + ui/public/assets.json | 1 + ui/public/favicon.ico | Bin 0 -> 8736 bytes ui/public/fonts/LICENSE_THEMIFY | 5 + ui/public/fonts/LICENSE_TITILLIUM_WEB | 93 + ui/public/fonts/themify.ttf | Bin 0 -> 78584 bytes ui/public/fonts/themify.woff | Bin 0 -> 56108 bytes .../fonts/titillium-web-v5-latin-200.ttf | Bin 0 -> 27768 bytes .../fonts/titillium-web-v5-latin-200.woff | Bin 0 -> 15248 bytes .../fonts/titillium-web-v5-latin-200.woff2 | Bin 0 -> 11556 bytes .../fonts/titillium-web-v5-latin-300.ttf | Bin 0 -> 28012 bytes .../fonts/titillium-web-v5-latin-300.woff | Bin 0 -> 15564 bytes .../fonts/titillium-web-v5-latin-300.woff2 | Bin 0 -> 11656 bytes .../fonts/titillium-web-v5-latin-600.ttf | Bin 0 -> 27568 bytes .../fonts/titillium-web-v5-latin-600.woff | Bin 0 -> 15268 bytes .../fonts/titillium-web-v5-latin-600.woff2 | Bin 0 -> 11484 bytes .../fonts/titillium-web-v5-latin-700.ttf | Bin 0 -> 26592 bytes .../fonts/titillium-web-v5-latin-700.woff | Bin 0 -> 14684 bytes .../fonts/titillium-web-v5-latin-700.woff2 | Bin 0 -> 10960 bytes .../fonts/titillium-web-v5-latin-regular.ttf | Bin 0 -> 27920 bytes .../fonts/titillium-web-v5-latin-regular.woff | Bin 0 -> 15472 bytes .../titillium-web-v5-latin-regular.woff2 | Bin 0 -> 11608 bytes ui/public/images/assets/alerts.png | Bin 0 -> 229415 bytes ui/public/images/assets/demo.gif | Bin 0 -> 4137920 bytes ui/public/images/assets/logo_lighter.png | Bin 0 -> 8763 bytes ui/public/images/assets/logo_with_title.png | Bin 0 -> 42425 bytes .../images/assets/logo_with_title_dark.png | Bin 0 -> 34912 bytes .../assets/logo_with_title_transparent.png | Bin 0 -> 42779 bytes ui/public/images/assets/service_graph.png | Bin 0 -> 127758 bytes ui/public/images/assets/trace_timeline.png | Bin 0 -> 113379 bytes ui/public/images/assets/trends.png | Bin 0 -> 281265 bytes ui/public/images/assets/universal_search.png | Bin 0 -> 227555 bytes ui/public/images/error.svg | 6 + ui/public/images/loading.gif | Bin 0 -> 7785 bytes ui/public/images/logo-white.png | Bin 0 -> 3383 bytes ui/public/images/logo.png | Bin 0 -> 9586 bytes ui/public/images/slack.png | Bin 0 -> 13920 bytes ui/public/images/success.svg | 6 + ui/public/images/zipkin-logo.jpg | Bin 0 -> 8949 bytes ui/public/scripts/particles.json | 110 + ui/public/scripts/particles.min.js | 9 + ui/server/app.js | 119 + ui/server/config/base.js | 251 + ui/server/config/config.js | 42 + ui/server/config/override.js | 56 + .../alerts/haystack/alertsConnector.js | 191 + .../alerts/haystack/expressionTreeBuilder.js | 42 + .../alerts/haystack/subscriptionsConnector.js | 169 + .../connectors/alerts/stub/alertsConnector.js | 110 + .../alerts/stub/subscriptionsConnector.js | 106 + .../connectors/operations/grpcDeleter.js | 51 + .../connectors/operations/grpcFetcher.js | 51 + ui/server/connectors/operations/grpcPoster.js | 51 + ui/server/connectors/operations/grpcPutter.js | 51 + .../connectors/operations/restFetcher.js | 48 + .../haystack/graphDataExtractor.js | 159 + .../haystack/serviceGraphConnector.js | 36 + .../stub/serviceGraphConnector.js | 164 + .../serviceGraph/zipkin/converter.js | 59 + .../zipkin/serviceGraphConnector.js | 40 + .../serviceInsights/detectCycles.js | 102 + .../connectors/serviceInsights/fetcher.js | 91 + .../serviceInsights/graphDataExtractor.js | 478 + .../serviceInsightsConnector.js | 46 + .../connectors/services/servicesConnector.js | 29 + .../traces/haystack/expressionTreeBuilder.js | 97 + .../protobufConverters/callGraphConverter.js | 64 + .../protobufConverters/traceConverter.js | 73 + .../traceCountsConverter.js | 26 + .../haystack/search/searchRequestBuilder.js | 35 + .../search/searchResultsTransformer.js | 134 + .../timeline/traceCountsRequestBuilder.js | 47 + .../traces/haystack/tracesConnector.js | 223 + .../connectors/traces/mock/mock-web-ui.js | 168 + ui/server/connectors/traces/mock/spanTypes.js | 100 + .../connectors/traces/mock/tracesConnector.js | 84 + .../connectors/traces/mock/tracesGenerator.js | 183 + .../connectors/traces/stub/tracesConnector.js | 939 ++ .../connectors/traces/zipkin/converter.js | 447 + .../traces/zipkin/tracesConnector.js | 113 + .../trends/haystack/trendsConnector.js | 324 + .../connectors/trends/stub/trendsConnector.js | 238 + .../connectors/utils/LoaderBackedCache.js | 76 + .../utils/encoders/Base64Encoder.js | 27 + .../utils/encoders/MetricpointNameEncoder.js | 41 + .../connectors/utils/encoders/NoopEncoder.js | 27 + .../encoders/PeriodReplacementEncoder.js | 27 + ui/server/connectors/utils/errorConverter.js | 23 + ui/server/connectors/utils/objectUtils.js | 26 + ui/server/routes/alertsApi.js | 82 + ui/server/routes/auth.js | 51 + ui/server/routes/index.js | 67 + ui/server/routes/login.js | 33 + ui/server/routes/serviceGraphApi.js | 32 + ui/server/routes/serviceInsightsApi.js | 39 + ui/server/routes/servicesApi.js | 36 + ui/server/routes/servicesPerfApi.js | 37 + ui/server/routes/sso.js | 32 + ui/server/routes/tracesApi.js | 75 + ui/server/routes/trendsApi.js | 74 + ui/server/routes/user.js | 25 + ui/server/routes/utils/apiResponseHandler.js | 36 + ui/server/sso/authChecker.js | 30 + ui/server/sso/samlSsoAuthenticator.js | 82 + ui/server/start.js | 28 + ui/server/utils/logger.js | 56 + ui/server/utils/metrics.js | 19 + ui/server/utils/metricsMiddleware.js | 30 + ui/server/utils/metricsReporter.js | 33 + ui/server/utils/server.js | 70 + ui/server/views/index.pug | 38 + ui/src/app.jsx | 34 + ui/src/app.less | 477 + ui/src/bootstrap/LICENSE_BOOTSTRAP | 21 + ui/src/bootstrap/LICENSE_BOOTSWATCH | 21 + ui/src/bootstrap/alerts.less | 73 + ui/src/bootstrap/badges.less | 66 + ui/src/bootstrap/bootstrap.less | 63 + ui/src/bootstrap/bootswatch.less | 639 + ui/src/bootstrap/breadcrumbs.less | 26 + ui/src/bootstrap/button-groups.less | 244 + ui/src/bootstrap/buttons.less | 166 + ui/src/bootstrap/carousel.less | 270 + ui/src/bootstrap/close.less | 34 + ui/src/bootstrap/code.less | 69 + ui/src/bootstrap/component-animations.less | 33 + ui/src/bootstrap/dropdowns.less | 216 + ui/src/bootstrap/forms.less | 613 + ui/src/bootstrap/grid.less | 84 + ui/src/bootstrap/input-groups.less | 171 + ui/src/bootstrap/jumbotron.less | 54 + ui/src/bootstrap/labels.less | 64 + ui/src/bootstrap/list-group.less | 130 + ui/src/bootstrap/media.less | 66 + ui/src/bootstrap/mixins.less | 40 + ui/src/bootstrap/mixins/alerts.less | 14 + .../bootstrap/mixins/background-variant.less | 9 + ui/src/bootstrap/mixins/border-radius.less | 18 + ui/src/bootstrap/mixins/buttons.less | 65 + ui/src/bootstrap/mixins/center-block.less | 7 + ui/src/bootstrap/mixins/clearfix.less | 22 + ui/src/bootstrap/mixins/forms.less | 84 + ui/src/bootstrap/mixins/gradients.less | 108 + ui/src/bootstrap/mixins/grid-framework.less | 97 + ui/src/bootstrap/mixins/grid.less | 122 + ui/src/bootstrap/mixins/hide-text.less | 21 + ui/src/bootstrap/mixins/image.less | 30 + ui/src/bootstrap/mixins/labels.less | 12 + ui/src/bootstrap/mixins/list-group.less | 30 + ui/src/bootstrap/mixins/nav-divider.less | 10 + .../bootstrap/mixins/nav-vertical-align.less | 9 + ui/src/bootstrap/mixins/opacity.less | 8 + ui/src/bootstrap/mixins/pagination.less | 24 + ui/src/bootstrap/mixins/panels.less | 24 + ui/src/bootstrap/mixins/progress-bar.less | 10 + ui/src/bootstrap/mixins/reset-filter.less | 8 + ui/src/bootstrap/mixins/reset-text.less | 18 + ui/src/bootstrap/mixins/resize.less | 6 + .../mixins/responsive-visibility.less | 21 + ui/src/bootstrap/mixins/size.less | 10 + ui/src/bootstrap/mixins/tab-focus.less | 9 + ui/src/bootstrap/mixins/table-row.less | 28 + ui/src/bootstrap/mixins/text-emphasis.less | 9 + ui/src/bootstrap/mixins/text-overflow.less | 8 + ui/src/bootstrap/mixins/vendor-prefixes.less | 228 + ui/src/bootstrap/modals.less | 150 + ui/src/bootstrap/navbar.less | 655 + ui/src/bootstrap/navs.less | 242 + ui/src/bootstrap/normalize.less | 424 + ui/src/bootstrap/pager.less | 54 + ui/src/bootstrap/pagination.less | 89 + ui/src/bootstrap/panels.less | 271 + ui/src/bootstrap/popovers.less | 131 + ui/src/bootstrap/print.less | 101 + ui/src/bootstrap/progress-bars.less | 87 + ui/src/bootstrap/responsive-embed.less | 35 + ui/src/bootstrap/responsive-utilities.less | 194 + ui/src/bootstrap/scaffolding.less | 161 + ui/src/bootstrap/svc-colors.less | 236 + ui/src/bootstrap/tables.less | 236 + ui/src/bootstrap/theme.less | 291 + ui/src/bootstrap/themify-icons.less | 1078 ++ ui/src/bootstrap/thumbnails.less | 36 + ui/src/bootstrap/titillium-web.less | 40 + ui/src/bootstrap/tooltip.less | 101 + ui/src/bootstrap/type.less | 302 + ui/src/bootstrap/utilities.less | 55 + ui/src/bootstrap/variables.less | 834 + ui/src/bootstrap/wells.less | 29 + ui/src/components/alerts/alertCounter.jsx | 53 + ui/src/components/alerts/alertTabs.jsx | 105 + ui/src/components/alerts/alerts.jsx | 69 + ui/src/components/alerts/alerts.less | 177 + ui/src/components/alerts/alertsTable.jsx | 293 + .../alerts/alertsTableSparkline.jsx | 108 + ui/src/components/alerts/alertsToolbar.jsx | 180 + .../alerts/details/alertDetails.jsx | 94 + .../alerts/details/alertDetailsToolbar.jsx | 104 + .../alerts/details/alertHistory.jsx | 156 + .../alerts/details/alertSubscriptions.jsx | 200 + .../alerts/details/subscriptionRow.jsx | 162 + .../alerts/stores/alertDetailsStore.js | 100 + .../alerts/stores/alertTrendFetcher.js | 37 + .../alerts/stores/serviceAlertsStore.js | 56 + .../alerts/utils/subscriptionConstructor.js | 31 + ui/src/components/common/error.jsx | 40 + ui/src/components/common/error.less | 28 + ui/src/components/common/loading.jsx | 28 + ui/src/components/common/loading.less | 26 + ui/src/components/common/login.jsx | 54 + ui/src/components/common/login.less | 54 + ui/src/components/common/modal.jsx | 101 + ui/src/components/common/modal.less | 39 + ui/src/components/common/noMatch.jsx | 31 + ui/src/components/common/resultsTable.less | 354 + ui/src/components/common/timeRangePicker.jsx | 88 + ui/src/components/common/timeRangePicker.less | 21 + ui/src/components/common/withTracker.jsx | 58 + ui/src/components/common/workInProgress.jsx | 36 + ui/src/components/docs/help.jsx | 27 + .../layout/authenticationTimeoutModal.jsx | 48 + ui/src/components/layout/footer.jsx | 55 + ui/src/components/layout/footer.less | 38 + ui/src/components/layout/slimHeader.jsx | 97 + .../serviceGraph/connectionDetails.jsx | 57 + .../components/serviceGraph/graphSearch.jsx | 41 + .../components/serviceGraph/nodeDetails.jsx | 83 + .../components/serviceGraph/serviceGraph.jsx | 41 + .../components/serviceGraph/serviceGraph.less | 147 + .../serviceGraph/serviceGraphContainer.jsx | 176 + .../serviceGraph/serviceGraphResults.jsx | 279 + .../serviceGraph/stores/serviceGraphStore.js | 41 + .../components/serviceGraph/trafficTable.jsx | 129 + ui/src/components/serviceGraph/util/graph.js | 121 + .../components/serviceGraph/vizceralConfig.js | 12 + .../components/serviceGraph/vizceralExt.jsx | 26 + .../serviceInsights/serviceInsights.jsx | 90 + .../serviceInsights/serviceInsights.less | 63 + .../serviceInsightsGraph/dataLayout.js | 107 + .../serviceInsightsGraph/dragGroup.jsx | 72 + .../serviceInsightsGraph/icons/db.svg | 9 + .../serviceInsightsGraph/icons/gateway.svg | 8 + .../serviceInsightsGraph/icons/outbound.svg | 6 + .../icons/uninstrumented.svg | 7 + .../serviceInsightsGraph/label.jsx | 73 + .../serviceInsightsGraph/labels.jsx | 40 + .../serviceInsightsGraph/lines.jsx | 115 + .../serviceInsightsGraph/nodes.jsx | 86 + .../serviceInsightsGraph.jsx | 105 + .../serviceInsightsGraph.less | 115 + .../serviceInsightsGraph/tooltip.jsx | 102 + .../serviceInsightsGraph/tooltip.less | 105 + .../stores/serviceInsightsStore.js | 52 + ui/src/components/serviceInsights/summary.jsx | 45 + .../servicePerf/servicePerformance.jsx | 191 + .../servicePerf/servicePerformance.less | 100 + .../servicePerf/stores/servicePerfStore.js | 43 + .../traces/details/latency/latencyCostTab.jsx | 414 + .../latency/latencyCostTabContainer.jsx | 51 + .../traces/details/rawTraceModal.jsx | 53 + .../relatedTraces/relatedTracesRow.jsx | 106 + .../relatedTraces/relatedTracesTab.jsx | 78 + .../relatedTracesTabContainer.jsx | 128 + .../traces/details/timeline/logsTable.jsx | 70 + .../traces/details/timeline/rawSpan.jsx | 50 + .../traces/details/timeline/span.jsx | 261 + .../details/timeline/spanDetailsModal.jsx | 140 + .../traces/details/timeline/tagsTable.jsx | 63 + .../traces/details/timeline/timelineTab.jsx | 87 + .../details/timeline/timelineTabContainer.jsx | 58 + .../traces/details/traceDetails.jsx | 163 + .../traces/details/traceDetails.less | 191 + .../trends/serviceOperationTrendRow.jsx | 122 + .../traces/details/trends/trendsTab.jsx | 94 + .../details/trends/trendsTabContainer.jsx | 48 + ui/src/components/traces/results/noSearch.jsx | 27 + .../components/traces/results/noSearch.less | 25 + .../traces/results/spanResultsTable.jsx | 234 + .../components/traces/results/spansView.jsx | 52 + .../components/traces/results/tagsFilter.jsx | 61 + .../traces/results/traceResults.jsx | 60 + .../traces/results/traceResultsTable.jsx | 320 + .../traces/results/traceTimeline.jsx | 167 + .../traces/results/traceTimeline.less | 1331 ++ .../traces/results/tracesContainer.jsx | 55 + .../traces/stores/latencyCostStore.js | 44 + .../components/traces/stores/rawSpanStore.js | 40 + .../components/traces/stores/rawTraceStore.js | 41 + .../traces/stores/searchableKeysStore.js | 37 + .../traces/stores/spansSearchStore.js | 40 + .../traces/stores/traceDetailsStore.js | 225 + .../traces/stores/traceTrendFetcher.js | 37 + .../traces/stores/tracesSearchStore.js | 120 + ui/src/components/traces/traces.less | 118 + .../traces/upload/uploadContainer.jsx | 61 + .../components/traces/upload/uploadHeader.jsx | 66 + .../components/traces/utils/auxiliaryTags.js | 25 + ui/src/components/traces/utils/presets.js | 55 + .../traces/utils/traceQueryParser.js | 72 + .../trends/details/graphs/countGraph.jsx | 138 + .../trends/details/graphs/durationGraph.jsx | 136 + .../trends/details/graphs/graphContainer.jsx | 83 + .../trends/details/graphs/graphContainer.less | 24 + .../trends/details/graphs/missingTrend.jsx | 38 + .../trends/details/graphs/missingTrend.less | 24 + .../trends/details/graphs/options.js | 54 + .../trends/details/graphs/successGraph.jsx | 125 + .../trends/details/trendDetails.jsx | 67 + .../trends/details/trendDetails.less | 26 + .../trends/details/trendDetailsToolbar.jsx | 346 + .../trends/details/trendDetailsToolbar.less | 94 + .../trends/operation/operationResults.jsx | 94 + .../trends/operation/operationResults.less | 26 + .../operation/operationResultsHeatmap.jsx | 189 + .../operation/operationResultsHeatmap.less | 110 + .../operation/operationResultsTable.jsx | 222 + .../operation/operationResultsTable.less | 81 + .../trends/stores/operationStore.js | 66 + .../trends/utils/metricGranularity.js | 49 + .../trends/utils/trendSparklines.jsx | 88 + .../trends/utils/trendsTableFormatters.jsx | 110 + .../universalSearch/searchBar/autosuggest.jsx | 527 + .../searchBar/autosuggest.less | 254 + .../universalSearch/searchBar/chips.jsx | 95 + .../universalSearch/searchBar/guide.jsx | 89 + .../universalSearch/searchBar/queryBank.jsx | 53 + .../universalSearch/searchBar/searchBar.jsx | 80 + .../searchBar/searchSubmit.jsx | 32 + .../searchBar/stores/searchBarUiStateStore.js | 188 + .../searchBar/stores/searchableKeysStore.js | 43 + .../universalSearch/searchBar/suggestions.jsx | 87 + .../searchBar/timeRangePicker.jsx | 147 + .../searchBar/timeRangePicker.less | 297 + .../searchBar/timeWindowPicker.jsx | 93 + .../tabs/emptyTabPlaceholder.jsx | 35 + .../tabs/externalLinksList.jsx | 75 + .../universalSearch/tabs/serviceGraph.jsx | 56 + .../universalSearch/tabs/serviceInsights.jsx | 46 + .../tabs/servicePerformance.jsx | 56 + .../tabs/tabStores/alertsTabStateStore.js | 50 + .../tabs/tabStores/serviceGraphStateStore.js | 40 + .../tabStores/serviceInsightsTabStateStore.js | 81 + .../tabStores/servicePerformanceStateStore.js | 31 + .../tabs/tabStores/tracesTabStateStore.js | 60 + .../tabs/tabStores/trendsTabStateStore.js | 76 + .../components/universalSearch/tabs/tabs.jsx | 188 + .../universalSearch/universalSearch.jsx | 119 + .../universalSearch/universalSearch.less | 157 + .../universalSearch/utils/urlUtils.js | 59 + ui/src/main.jsx | 34 + ui/src/stores/authenticationStore.js | 23 + ui/src/stores/errorHandlingStore.js | 33 + ui/src/stores/operationStore.js | 41 + ui/src/stores/serviceStore.js | 46 + ui/src/stores/storesInitializer.js | 25 + ui/src/utils/blobUtil.js | 31 + ui/src/utils/externalLinkFormatter.jsx | 82 + ui/src/utils/formatters.js | 90 + ui/src/utils/hashUtil.js | 27 + ui/src/utils/linkBuilder.js | 31 + ui/src/utils/loginRenewer.js | 23 + ui/src/utils/queryParser.js | 35 + ui/src/utils/serviceColorMapper.js | 30 + ui/src/utils/timeWindow.js | 71 + ui/src/utils/validUrl.js | 23 + ui/test/server/config/override.spec.js | 69 + .../haystack/serviceGraphConnector.spec.js | 60 + .../serviceGraph/zipkin/converter.spec.js | 91 + .../serviceInsights/detectCycles.spec.js | 122 + .../serviceInsights/fetcher.spec.js | 147 + .../graphDataExtractor.spec.js | 399 + .../serviceInsightsConnector.spec.js | 94 + .../search/searchResultsTransformer.spec.js | 142 + .../traces/haystack/tracesConnector.spec.js | 80 + .../traces/zipkin/converter.spec.js | 1114 ++ .../traces/zipkin/tracesConnector.spec.js | 195 + .../trends/haystack/trendsConnector.spec.js | 61 + .../utils/encoders/Base64Encoder.spec.js | 31 + .../encoders/MetricpointNameEncoder.spec.js | 44 + .../utils/encoders/NoopEncoder.spec.js | 31 + .../encoders/PeriodReplacementEncoder.spec.js | 31 + ui/test/server/routes/alertsApi.spec.js | 112 + ui/test/server/routes/index.spec.js | 32 + .../server/routes/serviceInsightsApi.spec.js | 32 + ui/test/server/routes/servicesApi.spec.js | 44 + ui/test/server/routes/servicesPerfApi.spec.js | 32 + ui/test/server/routes/tracesApi.spec.js | 140 + ui/test/server/routes/trendsApi.spec.js | 92 + ui/test/src/components/alerts.spec.jsx | 300 + ui/test/src/components/common/help.spec.jsx | 28 + .../src/components/common/noMatch.spec.jsx | 28 + .../common/timeRangePicker.spec.jsx | 36 + ui/test/src/components/layout.spec.jsx | 30 + .../src/components/serviceGraph/graph.spec.js | 62 + .../serviceGraph/serviceGraph.spec.jsx | 154 + .../src/components/serviceGraph/util/edges.js | 144 + .../serviceInsights/serviceInsights.spec.jsx | 132 + .../serviceInsightsGraph/dataLayout.spec.js | 83 + .../serviceInsightsGraph/dragGroup.spec.jsx | 104 + .../serviceInsightsGraph/label.spec.jsx | 75 + .../serviceInsightsGraph/labels.spec.jsx | 41 + .../serviceInsightsGraph/lines.spec.jsx | 101 + .../serviceInsightsGraph/nodes.spec.jsx | 111 + .../serviceInsightsGraph.spec.jsx | 118 + .../serviceInsightsGraph/tooltip.spec.jsx | 117 + .../stores/serviceInsightsStore.spec.js | 137 + .../serviceInsights/summary.spec.jsx | 57 + ui/test/src/components/servicePerf.spec.jsx | 77 + .../components/traces/latencyCost.spec.jsx | 114 + .../src/components/traces/rawSpan.spec.jsx | 103 + .../src/components/traces/rawTrace.spec.jsx | 83 + .../src/components/traces/spansView.spec.jsx | 223 + ui/test/src/components/traces/traces.spec.jsx | 557 + .../details/graphs/durationGraph.spec.jsx | 42 + ui/test/src/components/trends/trends.spec.jsx | 329 + .../stores/searchBarUiStateStore.spec.js | 151 + .../searchBar/timeRangePicker.spec.jsx | 36 + .../tabs/serviceInsights.spec.jsx | 48 + .../serviceInsightsTabStateStore.spec.js | 95 + .../tabStores/tracesTabStateStore.spec.js | 45 + .../universalSearch/universalSearch.spec.jsx | 780 + ui/test/src/main.spec.jsx | 26 + ui/test/src/stores/alertStore.spec.js | 99 + ui/test/src/stores/operationStore.spec.js | 50 + ui/test/src/stores/serviceGraphStore.spec.js | 83 + ui/test/src/stores/serviceStore.spec.js | 53 + ui/test/src/stores/tracesStore.spec.js | 269 + ui/test/src/stores/trendsStore.spec.js | 85 + ui/test/src/test_helper.js | 95 + ui/test/src/utils/timeWindow.spec.js | 43 + ui/universal/enums.js | 42 + ui/webpack.config.js | 137 + 1268 files changed, 121700 insertions(+) create mode 100755 collector/.mvn/wrapper/maven-wrapper.properties create mode 100644 collector/LICENSE create mode 100644 collector/Makefile create mode 100644 collector/README.md create mode 100644 collector/checkstyles/scalastyle_config.xml create mode 100644 collector/commons/pom.xml create mode 100644 collector/commons/src/main/scala/com/expedia/www/haystack/collector/commons/MetricsSupport.scala create mode 100644 collector/commons/src/main/scala/com/expedia/www/haystack/collector/commons/ProtoSpanExtractor.scala create mode 100644 collector/commons/src/main/scala/com/expedia/www/haystack/collector/commons/SpanDecoratorFactory.scala create mode 100644 collector/commons/src/main/scala/com/expedia/www/haystack/collector/commons/TtlAndOperationNames.java create mode 100644 collector/commons/src/main/scala/com/expedia/www/haystack/collector/commons/config/ConfigurationLoader.scala create mode 100644 collector/commons/src/main/scala/com/expedia/www/haystack/collector/commons/config/ExtractorConfiguration.scala create mode 100644 collector/commons/src/main/scala/com/expedia/www/haystack/collector/commons/config/KafkaProduceConfiguration.scala create mode 100644 collector/commons/src/main/scala/com/expedia/www/haystack/collector/commons/health/HealthController.scala create mode 100644 collector/commons/src/main/scala/com/expedia/www/haystack/collector/commons/health/HealthStatus.scala create mode 100644 collector/commons/src/main/scala/com/expedia/www/haystack/collector/commons/health/HealthStatusChangeListener.scala create mode 100644 collector/commons/src/main/scala/com/expedia/www/haystack/collector/commons/health/UpdateHealthStatusFile.scala create mode 100644 collector/commons/src/main/scala/com/expedia/www/haystack/collector/commons/logger/LoggerUtils.scala create mode 100644 collector/commons/src/main/scala/com/expedia/www/haystack/collector/commons/record/KeyValueExtractor.scala create mode 100644 collector/commons/src/main/scala/com/expedia/www/haystack/collector/commons/sink/RecordSink.scala create mode 100644 collector/commons/src/main/scala/com/expedia/www/haystack/collector/commons/sink/kafka/KafkaRecordSink.scala create mode 100644 collector/commons/src/test/resources/logback-test.xml create mode 100644 collector/commons/src/test/scala/com/expedia/www/haystack/collector/commons/unit/HealthControllerSpec.scala create mode 100644 collector/commons/src/test/scala/com/expedia/www/haystack/collector/commons/unit/KeyExtractorSpec.scala create mode 100644 collector/commons/src/test/scala/com/expedia/www/haystack/collector/commons/unit/LoggerUtilsSpec.scala create mode 100644 collector/commons/src/test/scala/com/expedia/www/haystack/collector/commons/unit/ProtoSpanExtractorSpec.scala create mode 100755 collector/deployment/scripts/publish-to-docker-hub.sh create mode 100644 collector/deployment/terraform/http-span-collector/main.tf create mode 100644 collector/deployment/terraform/http-span-collector/outputs.tf create mode 100644 collector/deployment/terraform/http-span-collector/templates/deployment_yaml.tpl create mode 100644 collector/deployment/terraform/http-span-collector/templates/http-span-collector_conf.tpl create mode 100644 collector/deployment/terraform/http-span-collector/variables.tf create mode 100644 collector/deployment/terraform/kinesis-span-collector/main.tf create mode 100644 collector/deployment/terraform/kinesis-span-collector/outputs.tf create mode 100644 collector/deployment/terraform/kinesis-span-collector/templates/deployment_yaml.tpl create mode 100644 collector/deployment/terraform/kinesis-span-collector/templates/kinesis-span-collector_conf.tpl create mode 100644 collector/deployment/terraform/kinesis-span-collector/variables.tf create mode 100644 collector/deployment/terraform/main.tf create mode 100644 collector/deployment/terraform/outputs.tf create mode 100644 collector/deployment/terraform/variables.tf create mode 100644 collector/haystack-span-decorators/pom.xml create mode 100644 collector/haystack-span-decorators/src/main/java/com/expedia/www/haystack/span/decorators/AdditionalTagsSpanDecorator.java create mode 100644 collector/haystack-span-decorators/src/main/java/com/expedia/www/haystack/span/decorators/SpanDecorator.java create mode 100644 collector/haystack-span-decorators/src/main/java/com/expedia/www/haystack/span/decorators/plugin/config/Plugin.java create mode 100644 collector/haystack-span-decorators/src/main/java/com/expedia/www/haystack/span/decorators/plugin/config/PluginConfiguration.java create mode 100644 collector/haystack-span-decorators/src/main/java/com/expedia/www/haystack/span/decorators/plugin/loader/SpanDecoratorPluginLoader.java create mode 100644 collector/haystack-span-decorators/src/test/java/com/expedia/www/haystack/span/decorators/AdditionalTagsSpanDecoratorTest.java create mode 100644 collector/haystack-span-decorators/src/test/resources/logback-test.xml create mode 100644 collector/http/Makefile create mode 100644 collector/http/README.md create mode 100644 collector/http/build/docker/Dockerfile create mode 100644 collector/http/build/docker/jmxtrans-agent.xml create mode 100755 collector/http/build/docker/start-app.sh create mode 100644 collector/http/build/integration-tests/app-integration-test.conf create mode 100644 collector/http/build/integration-tests/docker-compose.yml create mode 100644 collector/http/pom.xml create mode 100644 collector/http/src/main/resources/config/base.conf create mode 100644 collector/http/src/main/resources/logback.xml create mode 100644 collector/http/src/main/scala/com/expedia/www/haystack/http/span/collector/ProjectConfiguration.scala create mode 100644 collector/http/src/main/scala/com/expedia/www/haystack/http/span/collector/WebServer.scala create mode 100644 collector/http/src/main/scala/com/expedia/www/haystack/http/span/collector/authenticator/Authenticator.scala create mode 100644 collector/http/src/main/scala/com/expedia/www/haystack/http/span/collector/authenticator/NoopAuthenticator.scala create mode 100644 collector/http/src/main/scala/com/expedia/www/haystack/http/span/collector/json/Span.scala create mode 100644 collector/http/src/test/resources/logback-test.xml create mode 100644 collector/http/src/test/scala/com/expedia/www/haystack/http/span/collector/integration/HttpProducer.scala create mode 100644 collector/http/src/test/scala/com/expedia/www/haystack/http/span/collector/integration/IntegrationTestSpec.scala create mode 100644 collector/http/src/test/scala/com/expedia/www/haystack/http/span/collector/integration/LocalKafkaConsumer.scala create mode 100644 collector/http/src/test/scala/com/expedia/www/haystack/http/span/collector/integration/config/TestConfiguration.scala create mode 100644 collector/http/src/test/scala/com/expedia/www/haystack/http/span/collector/integration/tests/HttpSpanCollectorSpec.scala create mode 100644 collector/kinesis/Makefile create mode 100644 collector/kinesis/README.md create mode 100644 collector/kinesis/build/docker/Dockerfile create mode 100644 collector/kinesis/build/docker/jmxtrans-agent.xml create mode 100755 collector/kinesis/build/docker/start-app.sh create mode 100644 collector/kinesis/build/integration-tests/app-integration-test.conf create mode 100644 collector/kinesis/build/integration-tests/docker-compose.yml create mode 100644 collector/kinesis/build/integration-tests/scripts/create-dynamo-table.js create mode 100644 collector/kinesis/build/integration-tests/scripts/create-kinesis-stream.js create mode 100644 collector/kinesis/build/integration-tests/scripts/package.json create mode 100755 collector/kinesis/build/integration-tests/scripts/setup.sh create mode 100644 collector/kinesis/pom.xml create mode 100644 collector/kinesis/src/main/resources/config/base.conf create mode 100644 collector/kinesis/src/main/resources/logback.xml create mode 100644 collector/kinesis/src/main/scala/com/expedia/www/haystack/kinesis/span/collector/App.scala create mode 100644 collector/kinesis/src/main/scala/com/expedia/www/haystack/kinesis/span/collector/config/ProjectConfiguration.scala create mode 100644 collector/kinesis/src/main/scala/com/expedia/www/haystack/kinesis/span/collector/config/entities/KinesisConsumerConfiguration.scala create mode 100644 collector/kinesis/src/main/scala/com/expedia/www/haystack/kinesis/span/collector/kinesis/RecordProcessor.scala create mode 100644 collector/kinesis/src/main/scala/com/expedia/www/haystack/kinesis/span/collector/kinesis/client/KinesisConsumer.scala create mode 100644 collector/kinesis/src/main/scala/com/expedia/www/haystack/kinesis/span/collector/kinesis/record/TtlAndOperationNames.java create mode 100644 collector/kinesis/src/main/scala/com/expedia/www/haystack/kinesis/span/collector/metrics/AppMetricNames.scala create mode 100644 collector/kinesis/src/main/scala/com/expedia/www/haystack/kinesis/span/collector/pipeline/KinesisToKafkaPipeline.scala create mode 100644 collector/kinesis/src/test/resources/config/base.conf create mode 100644 collector/kinesis/src/test/resources/logback-test.xml create mode 100644 collector/kinesis/src/test/scala/com/expedia/www/haystack/kinesis/span/collector/integration/IntegrationTestSpec.scala create mode 100644 collector/kinesis/src/test/scala/com/expedia/www/haystack/kinesis/span/collector/integration/LocalKafkaConsumer.scala create mode 100644 collector/kinesis/src/test/scala/com/expedia/www/haystack/kinesis/span/collector/integration/LocalKinesisProducer.scala create mode 100644 collector/kinesis/src/test/scala/com/expedia/www/haystack/kinesis/span/collector/integration/config/TestConfiguration.scala create mode 100644 collector/kinesis/src/test/scala/com/expedia/www/haystack/kinesis/span/collector/integration/tests/KinesisSpanCollectorSpec.scala create mode 100644 collector/kinesis/src/test/scala/com/expedia/www/haystack/kinesis/span/collector/unit/tests/ConfigurationLoaderSpec.scala create mode 100644 collector/kinesis/src/test/scala/com/expedia/www/haystack/kinesis/span/collector/unit/tests/RecordProcessorSpec.scala create mode 100755 collector/mvnw create mode 100755 collector/mvnw.cmd create mode 100644 collector/pom.xml create mode 100644 collector/sample-span-decorator/pom.xml create mode 100644 collector/sample-span-decorator/src/main/java/com/expedia/www/sample/span/decorator/SampleSpanDecorator.java create mode 100644 collector/sample-span-decorator/src/main/resources/META-INF/services/com.expedia.www.haystack.span.decorators.SpanDecorator create mode 100644 collector/sample-span-decorator/src/main/resources/logback.xml create mode 100644 commons/.gitignore create mode 100644 commons/.gitmodules create mode 100755 commons/.mvn/wrapper/MavenWrapperDownloader.java create mode 100755 commons/.mvn/wrapper/maven-wrapper.jar create mode 100755 commons/.mvn/wrapper/maven-wrapper.properties create mode 100644 commons/.travis.yml create mode 100644 commons/CONTRIBUTING.md create mode 100644 commons/LICENSE create mode 100644 commons/README.md create mode 100644 commons/Release.md create mode 100644 commons/commons/pom.xml create mode 100644 commons/commons/src/main/scala/com/expedia/www/haystack/commons/config/ConfigurationLoader.scala create mode 100644 commons/commons/src/main/scala/com/expedia/www/haystack/commons/entities/GraphEdge.scala create mode 100644 commons/commons/src/main/scala/com/expedia/www/haystack/commons/entities/GraphVertex.scala create mode 100644 commons/commons/src/main/scala/com/expedia/www/haystack/commons/entities/Interval.scala create mode 100644 commons/commons/src/main/scala/com/expedia/www/haystack/commons/entities/TagKeys.scala create mode 100644 commons/commons/src/main/scala/com/expedia/www/haystack/commons/entities/encoders/Base64Encoder.scala create mode 100644 commons/commons/src/main/scala/com/expedia/www/haystack/commons/entities/encoders/Encoder.scala create mode 100644 commons/commons/src/main/scala/com/expedia/www/haystack/commons/entities/encoders/EncoderFactory.scala create mode 100644 commons/commons/src/main/scala/com/expedia/www/haystack/commons/entities/encoders/NoopEncoder.scala create mode 100644 commons/commons/src/main/scala/com/expedia/www/haystack/commons/entities/encoders/PeriodReplacementEncoder.scala create mode 100644 commons/commons/src/main/scala/com/expedia/www/haystack/commons/graph/GraphEdgeTagCollector.scala create mode 100644 commons/commons/src/main/scala/com/expedia/www/haystack/commons/health/HealthController.scala create mode 100644 commons/commons/src/main/scala/com/expedia/www/haystack/commons/health/HealthStatusChangeListener.scala create mode 100644 commons/commons/src/main/scala/com/expedia/www/haystack/commons/health/UpdateHealthStatusFile.scala create mode 100644 commons/commons/src/main/scala/com/expedia/www/haystack/commons/kstreams/GraphEdgeTimestampExtractor.scala create mode 100644 commons/commons/src/main/scala/com/expedia/www/haystack/commons/kstreams/IteratorAgeMetricSupport.scala create mode 100644 commons/commons/src/main/scala/com/expedia/www/haystack/commons/kstreams/MetricDataTimestampExtractor.scala create mode 100644 commons/commons/src/main/scala/com/expedia/www/haystack/commons/kstreams/SpanTimestampExtractor.scala create mode 100644 commons/commons/src/main/scala/com/expedia/www/haystack/commons/kstreams/app/Main.scala create mode 100644 commons/commons/src/main/scala/com/expedia/www/haystack/commons/kstreams/app/ManagedKafkaStreams.scala create mode 100644 commons/commons/src/main/scala/com/expedia/www/haystack/commons/kstreams/app/ManagedService.scala create mode 100644 commons/commons/src/main/scala/com/expedia/www/haystack/commons/kstreams/app/StateChangeListener.scala create mode 100644 commons/commons/src/main/scala/com/expedia/www/haystack/commons/kstreams/app/StreamsFactory.scala create mode 100644 commons/commons/src/main/scala/com/expedia/www/haystack/commons/kstreams/app/StreamsRunner.scala create mode 100644 commons/commons/src/main/scala/com/expedia/www/haystack/commons/kstreams/serde/SpanSerde.scala create mode 100644 commons/commons/src/main/scala/com/expedia/www/haystack/commons/kstreams/serde/graph/GraphEdgeKeySerde.scala create mode 100644 commons/commons/src/main/scala/com/expedia/www/haystack/commons/kstreams/serde/graph/GraphEdgeValueSerde.scala create mode 100644 commons/commons/src/main/scala/com/expedia/www/haystack/commons/kstreams/serde/metricdata/MetricDataSerde.scala create mode 100644 commons/commons/src/main/scala/com/expedia/www/haystack/commons/kstreams/serde/metricdata/MetricTankSerde.scala create mode 100644 commons/commons/src/main/scala/com/expedia/www/haystack/commons/logger/LoggerUtils.scala create mode 100644 commons/commons/src/main/scala/com/expedia/www/haystack/commons/metrics/MetricsSupport.scala create mode 100644 commons/commons/src/main/scala/com/expedia/www/haystack/commons/retries/MaxRetriesAttemptedException.scala create mode 100644 commons/commons/src/main/scala/com/expedia/www/haystack/commons/retries/RetryOperation.scala create mode 100644 commons/commons/src/main/scala/com/expedia/www/haystack/commons/util/MetricDefinitionKeyGenerator.scala create mode 100644 commons/commons/src/test/resources/logback-test.xml create mode 100644 commons/commons/src/test/resources/sample.conf create mode 100644 commons/commons/src/test/scala/com/expedia/www/haystack/commons/config/ConfigurationLoaderSpec.scala create mode 100644 commons/commons/src/test/scala/com/expedia/www/haystack/commons/entities/encoders/EncoderFactorySpec.scala create mode 100644 commons/commons/src/test/scala/com/expedia/www/haystack/commons/graph/GraphEdgeCollectorSpec.scala create mode 100644 commons/commons/src/test/scala/com/expedia/www/haystack/commons/health/HealthControllerSpec.scala create mode 100644 commons/commons/src/test/scala/com/expedia/www/haystack/commons/kstreams/GraphEdgeTimestampExtractorSpec.scala create mode 100644 commons/commons/src/test/scala/com/expedia/www/haystack/commons/kstreams/MetricDataTimestampExtractorSpec.scala create mode 100644 commons/commons/src/test/scala/com/expedia/www/haystack/commons/kstreams/SpanTimestampExtractorSpec.scala create mode 100644 commons/commons/src/test/scala/com/expedia/www/haystack/commons/kstreams/app/ApplicationSpec.scala create mode 100644 commons/commons/src/test/scala/com/expedia/www/haystack/commons/kstreams/app/ManagedKafkaStreamsSpec.scala create mode 100644 commons/commons/src/test/scala/com/expedia/www/haystack/commons/kstreams/app/StateChangeListenerSpec.scala create mode 100644 commons/commons/src/test/scala/com/expedia/www/haystack/commons/kstreams/app/StreamsRunnerSpec.scala create mode 100644 commons/commons/src/test/scala/com/expedia/www/haystack/commons/kstreams/serde/SpanSerdeSpec.scala create mode 100644 commons/commons/src/test/scala/com/expedia/www/haystack/commons/kstreams/serde/graph/GraphEdgeKeySerdeSpec.scala create mode 100644 commons/commons/src/test/scala/com/expedia/www/haystack/commons/kstreams/serde/graph/GraphEdgeValueSerdeSpec.scala create mode 100644 commons/commons/src/test/scala/com/expedia/www/haystack/commons/kstreams/serde/metricdata/MetricTankSerdeSpec.scala create mode 100644 commons/commons/src/test/scala/com/expedia/www/haystack/commons/logger/LoggerUtilSpec.scala create mode 100644 commons/commons/src/test/scala/com/expedia/www/haystack/commons/metrics/MetricRegistySpec.scala create mode 100644 commons/commons/src/test/scala/com/expedia/www/haystack/commons/retries/RetryOperationSpec.scala create mode 100644 commons/commons/src/test/scala/com/expedia/www/haystack/commons/unit/UnitTestSpec.scala create mode 100644 commons/commons/src/test/scala/com/expedia/www/haystack/commons/util/MetricDefinitionKeyGeneratorSpec.scala create mode 100644 commons/idl/pom.xml create mode 100755 commons/mvnw create mode 100755 commons/mvnw.cmd create mode 100644 commons/pom.xml create mode 100644 commons/scalastyle/scalastyle_config.xml create mode 100644 docker/.gitignore create mode 100644 docker/LICENSE create mode 100644 docker/README.md create mode 100644 docker/adaptive-alerting/configs/aa-manager/docker.conf create mode 100644 docker/adaptive-alerting/configs/aa-mapper/docker.conf create mode 100644 docker/adaptive-alerting/db_init/init_db.sh create mode 100644 docker/adaptive-alerting/docker-compose.yml create mode 100644 docker/adaptive-alerting/sql/build-db.sql create mode 100644 docker/adaptive-alerting/sql/sample-data.sql create mode 100644 docker/adaptive-alerting/sql/stored-procs.sql create mode 100644 docker/adaptive-alerting/stubs/__files/get_detectors.json create mode 100644 docker/adaptive-alerting/stubs/__files/get_models.json create mode 100644 docker/adaptive-alerting/stubs/mappings/get-detectors.json create mode 100644 docker/adaptive-alerting/stubs/mappings/get-models.json create mode 100644 docker/agent/default.conf create mode 100644 docker/agent/docker-compose.yml create mode 100644 docker/connectors.json create mode 100644 docker/docker-compose.yml create mode 100644 docker/example/blobs/docker-compose.yml create mode 100644 docker/example/opentelemetry/docker-compose.yml create mode 100644 docker/example/traces/docker-compose.yml create mode 100755 docker/run.sh create mode 100644 docker/service-graph/docker-compose.yml create mode 100644 docker/traces/docker-compose.yml create mode 100644 docker/trends/docker-compose.yml create mode 100644 docker/zipkin/docker-compose.yml create mode 100644 idl/.gitignore create mode 100644 idl/LICENSE create mode 100644 idl/README.md create mode 100755 idl/build.sh create mode 100644 idl/proto/agent/spanAgent.proto create mode 100644 idl/proto/api/anomaly/anomalyReader.proto create mode 100644 idl/proto/api/subscription/subscriptionManagement.proto create mode 100644 idl/proto/api/traceReader.proto create mode 100644 idl/proto/backend/storageBackend.proto create mode 100644 idl/proto/blobs/blob.proto create mode 100644 idl/proto/blobs/blobAgent.proto create mode 100644 idl/proto/span.proto create mode 100644 idl/proto/spanBuffer.proto create mode 100644 service-graph/.gitignore create mode 100644 service-graph/CONTRIBUTING.md create mode 100644 service-graph/LICENSE create mode 100644 service-graph/Makefile create mode 100644 service-graph/README.md create mode 100644 service-graph/Release.md create mode 100644 service-graph/ReleaseNotes.md create mode 100644 service-graph/checkstyles/scalastyle_config.xml create mode 100755 service-graph/deployment/scripts/publish-to-docker-hub.sh create mode 100644 service-graph/deployment/terraform/graph-builder/main.tf create mode 100644 service-graph/deployment/terraform/graph-builder/outputs.tf create mode 100644 service-graph/deployment/terraform/graph-builder/templates/deployment_yaml.tpl create mode 100644 service-graph/deployment/terraform/graph-builder/templates/graph-builder_conf.tpl create mode 100644 service-graph/deployment/terraform/graph-builder/variables.tf create mode 100644 service-graph/deployment/terraform/main.tf create mode 100644 service-graph/deployment/terraform/node-finder/main.tf create mode 100644 service-graph/deployment/terraform/node-finder/outputs.tf create mode 100644 service-graph/deployment/terraform/node-finder/templates/deployment_yaml.tpl create mode 100644 service-graph/deployment/terraform/node-finder/templates/node-finder_conf.tpl create mode 100644 service-graph/deployment/terraform/node-finder/variables.tf create mode 100644 service-graph/deployment/terraform/outputs.tf create mode 100644 service-graph/deployment/terraform/snapshotter/templates/deployment_yaml.tpl create mode 100644 service-graph/deployment/terraform/snapshotter/templates/snapshotter_conf.tpl create mode 100644 service-graph/deployment/terraform/variables.tf create mode 100644 service-graph/graph-builder/Makefile create mode 100644 service-graph/graph-builder/README.md create mode 100644 service-graph/graph-builder/build/docker/Dockerfile create mode 100644 service-graph/graph-builder/build/docker/jmxtrans-agent.xml create mode 100755 service-graph/graph-builder/build/docker/start-app.sh create mode 100644 service-graph/graph-builder/pom.xml create mode 100644 service-graph/graph-builder/src/main/resources/app.conf create mode 100644 service-graph/graph-builder/src/main/resources/logback.xml create mode 100644 service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/App.scala create mode 100644 service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/ManagedApplication.scala create mode 100644 service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/config/AppConfiguration.scala create mode 100644 service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/config/entities/CustomRocksDBConfig.scala create mode 100644 service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/config/entities/KafkaConfiguration.scala create mode 100644 service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/config/entities/ServiceClientConfiguration.scala create mode 100644 service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/config/entities/ServiceConfiguration.scala create mode 100644 service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/config/entities/ServiceHttpConfiguration.scala create mode 100644 service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/config/entities/ServiceThreadsConfiguration.scala create mode 100644 service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/model/EdgeStats.scala create mode 100644 service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/model/EdgeStatsSerde.scala create mode 100644 service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/model/OperationGraph.scala create mode 100644 service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/model/OperationGraphEdge.scala create mode 100644 service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/model/ServiceGraph.scala create mode 100644 service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/model/ServiceGraphEdge.scala create mode 100644 service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/service/HttpService.scala create mode 100644 service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/service/ManagedHttpService.scala create mode 100644 service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/service/fetchers/LocalOperationEdgesFetcher.scala create mode 100644 service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/service/fetchers/LocalServiceEdgesFetcher.scala create mode 100644 service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/service/fetchers/RemoteOperationEdgesFetcher.scala create mode 100644 service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/service/fetchers/RemoteServiceEdgesFetcher.scala create mode 100644 service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/service/resources/GlobalOperationGraphResource.scala create mode 100644 service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/service/resources/GlobalServiceGraphResource.scala create mode 100644 service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/service/resources/IsWorkingResource.scala create mode 100644 service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/service/resources/LocalOperationGraphResource.scala create mode 100644 service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/service/resources/LocalServiceGraphResource.scala create mode 100644 service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/service/resources/Resource.scala create mode 100644 service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/service/utils/EdgesMerger.scala create mode 100644 service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/service/utils/IOUtils.scala create mode 100644 service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/service/utils/QueryTimestampReader.scala create mode 100644 service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/stream/ServiceGraphStreamSupplier.scala create mode 100644 service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/stream/StreamSupplier.scala create mode 100644 service-graph/graph-builder/src/test/java/org/expedia/www/haystack/commons/scalatest/IntegrationSuite.java create mode 100644 service-graph/graph-builder/src/test/resources/integration/kafka-server.properties create mode 100644 service-graph/graph-builder/src/test/resources/integration/local.conf create mode 100644 service-graph/graph-builder/src/test/resources/integration/zookeeper.properties create mode 100644 service-graph/graph-builder/src/test/resources/log4j.properties create mode 100644 service-graph/graph-builder/src/test/resources/logback-test.xml create mode 100644 service-graph/graph-builder/src/test/resources/logback.xml create mode 100644 service-graph/graph-builder/src/test/resources/test/test.conf create mode 100644 service-graph/graph-builder/src/test/resources/test/test_application_server_set.conf create mode 100644 service-graph/graph-builder/src/test/resources/test/test_no_app_id.conf create mode 100644 service-graph/graph-builder/src/test/resources/test/test_no_bootstrap.conf create mode 100644 service-graph/graph-builder/src/test/resources/test/test_no_consumer.conf create mode 100644 service-graph/graph-builder/src/test/resources/test/test_no_producer.conf create mode 100644 service-graph/graph-builder/src/test/scala/com/expedia/www/haystack/service/graph/graph/builder/AppSpec.scala create mode 100644 service-graph/graph-builder/src/test/scala/com/expedia/www/haystack/service/graph/graph/builder/ManagedApplicationSpec.scala create mode 100644 service-graph/graph-builder/src/test/scala/com/expedia/www/haystack/service/graph/graph/builder/TestSpec.scala create mode 100644 service-graph/graph-builder/src/test/scala/com/expedia/www/haystack/service/graph/graph/builder/config/AppConfigurationSpec.scala create mode 100644 service-graph/graph-builder/src/test/scala/com/expedia/www/haystack/service/graph/graph/builder/kafka/KafkaController.scala create mode 100644 service-graph/graph-builder/src/test/scala/com/expedia/www/haystack/service/graph/graph/builder/kafka/KafkaLocal.scala create mode 100644 service-graph/graph-builder/src/test/scala/com/expedia/www/haystack/service/graph/graph/builder/kafka/ZooKeeperLocal.scala create mode 100644 service-graph/graph-builder/src/test/scala/com/expedia/www/haystack/service/graph/graph/builder/model/EdgeStatsSerdeSpec.scala create mode 100644 service-graph/graph-builder/src/test/scala/com/expedia/www/haystack/service/graph/graph/builder/model/EdgeStatsSpec.scala create mode 100644 service-graph/graph-builder/src/test/scala/com/expedia/www/haystack/service/graph/graph/builder/model/ServiceGraphEdgeSpec.scala create mode 100644 service-graph/graph-builder/src/test/scala/com/expedia/www/haystack/service/graph/graph/builder/service/ManagedHttpServiceSpec.scala create mode 100644 service-graph/graph-builder/src/test/scala/com/expedia/www/haystack/service/graph/graph/builder/service/resources/LocalOperationGraphResourceSpec.scala create mode 100644 service-graph/graph-builder/src/test/scala/com/expedia/www/haystack/service/graph/graph/builder/service/utils/EdgesMergerSpec.scala create mode 100644 service-graph/graph-builder/src/test/scala/com/expedia/www/haystack/service/graph/graph/builder/stream/StreamSupplierSpec.scala create mode 100644 service-graph/node-finder/Makefile create mode 100644 service-graph/node-finder/README.md create mode 100644 service-graph/node-finder/build/docker/Dockerfile create mode 100644 service-graph/node-finder/build/docker/jmxtrans-agent.xml create mode 100755 service-graph/node-finder/build/docker/start-app.sh create mode 100644 service-graph/node-finder/pom.xml create mode 100644 service-graph/node-finder/src/main/resources/app.conf create mode 100644 service-graph/node-finder/src/main/resources/logback.xml create mode 100644 service-graph/node-finder/src/main/scala/com/expedia/www/haystack/service/graph/node/finder/App.scala create mode 100644 service-graph/node-finder/src/main/scala/com/expedia/www/haystack/service/graph/node/finder/app/GraphNodeProducer.scala create mode 100644 service-graph/node-finder/src/main/scala/com/expedia/www/haystack/service/graph/node/finder/app/LatencyProducer.scala create mode 100644 service-graph/node-finder/src/main/scala/com/expedia/www/haystack/service/graph/node/finder/app/SpanAccumulator.scala create mode 100644 service-graph/node-finder/src/main/scala/com/expedia/www/haystack/service/graph/node/finder/app/Streams.scala create mode 100644 service-graph/node-finder/src/main/scala/com/expedia/www/haystack/service/graph/node/finder/app/metadata/MetadataProducerSupplier.scala create mode 100644 service-graph/node-finder/src/main/scala/com/expedia/www/haystack/service/graph/node/finder/app/metadata/MetadataStoreUpdateProcessorSupplier.scala create mode 100644 service-graph/node-finder/src/main/scala/com/expedia/www/haystack/service/graph/node/finder/app/metadata/TopicCreator.scala create mode 100644 service-graph/node-finder/src/main/scala/com/expedia/www/haystack/service/graph/node/finder/config/AppConfiguration.scala create mode 100644 service-graph/node-finder/src/main/scala/com/expedia/www/haystack/service/graph/node/finder/config/KafkaConfiguration.scala create mode 100644 service-graph/node-finder/src/main/scala/com/expedia/www/haystack/service/graph/node/finder/config/NodeMetadataConfiguration.scala create mode 100644 service-graph/node-finder/src/main/scala/com/expedia/www/haystack/service/graph/node/finder/model/LightSpan.scala create mode 100644 service-graph/node-finder/src/main/scala/com/expedia/www/haystack/service/graph/node/finder/model/ServiceNodeMetadata.scala create mode 100644 service-graph/node-finder/src/main/scala/com/expedia/www/haystack/service/graph/node/finder/model/SpanPair.scala create mode 100644 service-graph/node-finder/src/main/scala/com/expedia/www/haystack/service/graph/node/finder/utils/SpanUtils.scala create mode 100644 service-graph/node-finder/src/test/java/org/expedia/www/haystack/commons/scalatest/IntegrationSuite.java create mode 100644 service-graph/node-finder/src/test/resources/integration/kafka-server.properties create mode 100644 service-graph/node-finder/src/test/resources/integration/local.conf create mode 100644 service-graph/node-finder/src/test/resources/integration/zookeeper.properties create mode 100644 service-graph/node-finder/src/test/resources/log4j.properties create mode 100644 service-graph/node-finder/src/test/resources/logback-test.xml create mode 100644 service-graph/node-finder/src/test/resources/test/test.conf create mode 100644 service-graph/node-finder/src/test/resources/test/test_no_app_id.conf create mode 100644 service-graph/node-finder/src/test/resources/test/test_no_bootstrap.conf create mode 100644 service-graph/node-finder/src/test/resources/test/test_no_consumer.conf create mode 100644 service-graph/node-finder/src/test/resources/test/test_no_metrics_topic.conf create mode 100644 service-graph/node-finder/src/test/resources/test/test_no_producer.conf create mode 100644 service-graph/node-finder/src/test/scala/com/expedia/www/haystack/TestSpec.scala create mode 100644 service-graph/node-finder/src/test/scala/com/expedia/www/haystack/commons/kafka/KafkaController.scala create mode 100644 service-graph/node-finder/src/test/scala/com/expedia/www/haystack/commons/kafka/KafkaLocal.scala create mode 100644 service-graph/node-finder/src/test/scala/com/expedia/www/haystack/commons/kafka/ZooKeeperLocal.scala create mode 100644 service-graph/node-finder/src/test/scala/com/expedia/www/haystack/service/graph/node/finder/AppSpec.scala create mode 100644 service-graph/node-finder/src/test/scala/com/expedia/www/haystack/service/graph/node/finder/app/GraphNodeProducerSpec.scala create mode 100644 service-graph/node-finder/src/test/scala/com/expedia/www/haystack/service/graph/node/finder/app/LatencyProducerSpec.scala create mode 100644 service-graph/node-finder/src/test/scala/com/expedia/www/haystack/service/graph/node/finder/app/SpanAccumulatorSpec.scala create mode 100644 service-graph/node-finder/src/test/scala/com/expedia/www/haystack/service/graph/node/finder/app/StreamsSpec.scala create mode 100644 service-graph/node-finder/src/test/scala/com/expedia/www/haystack/service/graph/node/finder/config/AppConfigurationSpec.scala create mode 100644 service-graph/node-finder/src/test/scala/com/expedia/www/haystack/service/graph/node/finder/model/SpanPairSpec.scala create mode 100644 service-graph/node-finder/src/test/scala/com/expedia/www/haystack/service/graph/node/finder/utils/SpanUtilsSpec.scala create mode 100644 service-graph/pom.xml create mode 100644 service-graph/snapshot-store/README.md create mode 100644 service-graph/snapshot-store/pom.xml create mode 100644 service-graph/snapshot-store/src/main/scala/com/expedia/www/haystack/service/graph/snapshot/store/Constants.scala create mode 100644 service-graph/snapshot-store/src/main/scala/com/expedia/www/haystack/service/graph/snapshot/store/DataFramesIntoJsonTransformer.scala create mode 100644 service-graph/snapshot-store/src/main/scala/com/expedia/www/haystack/service/graph/snapshot/store/Edge.scala create mode 100644 service-graph/snapshot-store/src/main/scala/com/expedia/www/haystack/service/graph/snapshot/store/EdgeWithIds.scala create mode 100644 service-graph/snapshot-store/src/main/scala/com/expedia/www/haystack/service/graph/snapshot/store/FileSnapshotStore.scala create mode 100644 service-graph/snapshot-store/src/main/scala/com/expedia/www/haystack/service/graph/snapshot/store/JsonIntoDataFramesTransformer.scala create mode 100644 service-graph/snapshot-store/src/main/scala/com/expedia/www/haystack/service/graph/snapshot/store/Node.scala create mode 100644 service-graph/snapshot-store/src/main/scala/com/expedia/www/haystack/service/graph/snapshot/store/NodeWithId.scala create mode 100644 service-graph/snapshot-store/src/main/scala/com/expedia/www/haystack/service/graph/snapshot/store/NodesAndEdges.scala create mode 100644 service-graph/snapshot-store/src/main/scala/com/expedia/www/haystack/service/graph/snapshot/store/S3SnapshotStore.scala create mode 100644 service-graph/snapshot-store/src/main/scala/com/expedia/www/haystack/service/graph/snapshot/store/SnapshotStore.scala create mode 100644 service-graph/snapshot-store/src/test/resources/serviceGraph.json create mode 100644 service-graph/snapshot-store/src/test/resources/serviceGraph_edges.csv create mode 100644 service-graph/snapshot-store/src/test/resources/serviceGraph_nodes.csv create mode 100644 service-graph/snapshot-store/src/test/scala/com/expedia/www/haystack/service/graph/snapshot/store/DataFramesIntoJsonTransformerSpec.scala create mode 100644 service-graph/snapshot-store/src/test/scala/com/expedia/www/haystack/service/graph/snapshot/store/FileSnapshotStoreSpec.scala create mode 100644 service-graph/snapshot-store/src/test/scala/com/expedia/www/haystack/service/graph/snapshot/store/JsonIntoDataFramesTransformerSpec.scala create mode 100644 service-graph/snapshot-store/src/test/scala/com/expedia/www/haystack/service/graph/snapshot/store/S3SnapshotStoreSpec.scala create mode 100644 service-graph/snapshot-store/src/test/scala/com/expedia/www/haystack/service/graph/snapshot/store/SnapshotStoreSpec.scala create mode 100644 service-graph/snapshot-store/src/test/scala/com/expedia/www/haystack/service/graph/snapshot/store/SnapshotStoreSpecBase.scala create mode 100644 service-graph/snapshotter/Makefile create mode 100644 service-graph/snapshotter/README.md create mode 100644 service-graph/snapshotter/build/docker/Dockerfile create mode 100644 service-graph/snapshotter/build/docker/jmxtrans-agent.xml create mode 100755 service-graph/snapshotter/build/docker/start-app.sh create mode 100644 service-graph/snapshotter/pom.xml create mode 100644 service-graph/snapshotter/src/main/resources/app.conf create mode 100644 service-graph/snapshotter/src/main/resources/logback.xml create mode 100644 service-graph/snapshotter/src/main/scala/com/expedia/www/haystack/service/graph/snapshotter/AppConfiguration.scala create mode 100644 service-graph/snapshotter/src/main/scala/com/expedia/www/haystack/service/graph/snapshotter/Main.scala create mode 100644 service-graph/snapshotter/src/test/resources/app.conf create mode 100644 service-graph/snapshotter/src/test/resources/serviceGraph.json create mode 100644 service-graph/snapshotter/src/test/resources/serviceGraph_edges.csv create mode 100644 service-graph/snapshotter/src/test/resources/serviceGraph_nodes.csv create mode 100644 service-graph/snapshotter/src/test/scala/com/expedia/www/haystack/service/graph/snapshotter/MainSpec.scala create mode 100644 traces/.gitignore create mode 100755 traces/.mvn/wrapper/MavenWrapperDownloader.java create mode 100755 traces/.mvn/wrapper/maven-wrapper.jar create mode 100755 traces/.mvn/wrapper/maven-wrapper.properties create mode 100644 traces/.travis.yml create mode 100644 traces/CONTRIBUTING.md create mode 100644 traces/LICENSE create mode 100644 traces/Makefile create mode 100644 traces/README.md create mode 100644 traces/Release.md create mode 100644 traces/backends/Makefile create mode 100644 traces/backends/cassandra/Makefile create mode 100644 traces/backends/cassandra/README.md create mode 100644 traces/backends/cassandra/build/docker/Dockerfile create mode 100644 traces/backends/cassandra/build/docker/jmxtrans-agent.xml create mode 100755 traces/backends/cassandra/build/docker/start-app.sh create mode 100644 traces/backends/cassandra/build/integration-tests/docker-compose.yml create mode 100644 traces/backends/cassandra/pom.xml create mode 100644 traces/backends/cassandra/src/main/resources/config/base.conf create mode 100644 traces/backends/cassandra/src/main/resources/logback.xml create mode 100644 traces/backends/cassandra/src/main/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/Service.scala create mode 100644 traces/backends/cassandra/src/main/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/client/AwsNodeDiscoverer.scala create mode 100644 traces/backends/cassandra/src/main/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/client/CassandraClusterFactory.scala create mode 100644 traces/backends/cassandra/src/main/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/client/CassandraSession.scala create mode 100644 traces/backends/cassandra/src/main/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/client/CassandraTableSchema.scala create mode 100644 traces/backends/cassandra/src/main/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/client/ClusterFactory.scala create mode 100644 traces/backends/cassandra/src/main/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/config/ProjectConfiguration.scala create mode 100644 traces/backends/cassandra/src/main/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/config/entities/AwsNodeDiscoveryConfiguration.scala create mode 100644 traces/backends/cassandra/src/main/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/config/entities/ClientConfiguration.scala create mode 100644 traces/backends/cassandra/src/main/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/config/entities/CredentialsConfiguration.scala create mode 100644 traces/backends/cassandra/src/main/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/config/entities/ServiceConfiguration.scala create mode 100644 traces/backends/cassandra/src/main/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/config/entities/SocketConfiguration.scala create mode 100644 traces/backends/cassandra/src/main/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/metrics/AppMetricNames.scala create mode 100644 traces/backends/cassandra/src/main/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/services/GrpcHandler.scala create mode 100644 traces/backends/cassandra/src/main/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/services/GrpcHealthService.scala create mode 100644 traces/backends/cassandra/src/main/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/services/SpansPersistenceService.scala create mode 100644 traces/backends/cassandra/src/main/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/store/CassandraTraceRecordReadResultListener.scala create mode 100644 traces/backends/cassandra/src/main/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/store/CassandraTraceRecordReader.scala create mode 100644 traces/backends/cassandra/src/main/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/store/CassandraTraceRecordWriteResultListener.scala create mode 100644 traces/backends/cassandra/src/main/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/store/CassandraTraceRecordWriter.scala create mode 100644 traces/backends/cassandra/src/test/resources/config/base.conf create mode 100644 traces/backends/cassandra/src/test/resources/logback-test.xml create mode 100644 traces/backends/cassandra/src/test/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/integration/BaseIntegrationTestSpec.scala create mode 100644 traces/backends/cassandra/src/test/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/integration/CassandraStorageBackendServiceIntegrationTestSpec.scala create mode 100644 traces/backends/cassandra/src/test/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/unit/BaseUnitTestSpec.scala create mode 100644 traces/backends/cassandra/src/test/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/unit/client/AwsNodeDiscovererSpec.scala create mode 100644 traces/backends/cassandra/src/test/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/unit/client/CassandraSessionSpec.scala create mode 100644 traces/backends/cassandra/src/test/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/unit/client/CassandraTableSchemaSpec.scala create mode 100644 traces/backends/cassandra/src/test/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/unit/config/ConfigurationLoaderSpec.scala create mode 100644 traces/backends/memory/Makefile create mode 100644 traces/backends/memory/README.md create mode 100644 traces/backends/memory/build/docker/Dockerfile create mode 100644 traces/backends/memory/build/docker/jmxtrans-agent.xml create mode 100755 traces/backends/memory/build/docker/start-app.sh create mode 100644 traces/backends/memory/pom.xml create mode 100644 traces/backends/memory/src/main/resources/config/base.conf create mode 100644 traces/backends/memory/src/main/resources/logback.xml create mode 100644 traces/backends/memory/src/main/scala/com/expedia/www/haystack/trace/storage/backends/memory/Service.scala create mode 100644 traces/backends/memory/src/main/scala/com/expedia/www/haystack/trace/storage/backends/memory/config/ProjectConfiguration.scala create mode 100644 traces/backends/memory/src/main/scala/com/expedia/www/haystack/trace/storage/backends/memory/config/entities/ServiceConfiguration.scala create mode 100644 traces/backends/memory/src/main/scala/com/expedia/www/haystack/trace/storage/backends/memory/services/GrpcHealthService.scala create mode 100644 traces/backends/memory/src/main/scala/com/expedia/www/haystack/trace/storage/backends/memory/services/SpansPersistenceService.scala create mode 100644 traces/backends/memory/src/main/scala/com/expedia/www/haystack/trace/storage/backends/memory/store/InMemoryTraceRecordStore.scala create mode 100644 traces/backends/memory/src/test/resources/config/base.conf create mode 100644 traces/backends/memory/src/test/resources/logback-test.xml create mode 100644 traces/backends/memory/src/test/scala/com/expedia/www/haystack/trace/storage/backends/memory/integration/BaseIntegrationTestSpec.scala create mode 100644 traces/backends/memory/src/test/scala/com/expedia/www/haystack/trace/storage/backends/memory/integration/InMemoryTraceBackendServiceIntegrationTestSpec.scala create mode 100644 traces/backends/memory/src/test/scala/com/expedia/www/haystack/trace/storage/backends/memory/unit/BaseUnitTestSpec.scala create mode 100644 traces/backends/memory/src/test/scala/com/expedia/www/haystack/trace/storage/backends/memory/unit/config/ConfigurationLoaderSpec.scala create mode 100644 traces/backends/pom.xml create mode 100644 traces/checkstyles/scalastyle_config.xml create mode 100644 traces/commons/pom.xml create mode 100644 traces/commons/src/main/scala/com/expedia/www/haystack/trace/commons/clients/es/AWSSigningJestClientFactory.scala create mode 100644 traces/commons/src/main/scala/com/expedia/www/haystack/trace/commons/clients/es/document/ServiceMetadataDoc.scala create mode 100644 traces/commons/src/main/scala/com/expedia/www/haystack/trace/commons/clients/es/document/TraceIndexDoc.scala create mode 100644 traces/commons/src/main/scala/com/expedia/www/haystack/trace/commons/config/entities/AWSRequestSigningConfiguration.scala create mode 100644 traces/commons/src/main/scala/com/expedia/www/haystack/trace/commons/config/entities/ReloadConfiguration.scala create mode 100644 traces/commons/src/main/scala/com/expedia/www/haystack/trace/commons/config/entities/TraceStoreBackends.scala create mode 100644 traces/commons/src/main/scala/com/expedia/www/haystack/trace/commons/config/entities/WhitelistIndexFieldConfiguration.scala create mode 100644 traces/commons/src/main/scala/com/expedia/www/haystack/trace/commons/config/reload/ConfigurationReloadElasticSearchProvider.scala create mode 100644 traces/commons/src/main/scala/com/expedia/www/haystack/trace/commons/config/reload/ConfigurationReloadProvider.scala create mode 100644 traces/commons/src/main/scala/com/expedia/www/haystack/trace/commons/config/reload/Reloadable.scala create mode 100644 traces/commons/src/main/scala/com/expedia/www/haystack/trace/commons/packer/PackedMessage.scala create mode 100644 traces/commons/src/main/scala/com/expedia/www/haystack/trace/commons/packer/Packer.scala create mode 100644 traces/commons/src/main/scala/com/expedia/www/haystack/trace/commons/packer/PackerFactory.scala create mode 100644 traces/commons/src/main/scala/com/expedia/www/haystack/trace/commons/packer/Unpacker.scala create mode 100644 traces/commons/src/main/scala/com/expedia/www/haystack/trace/commons/utils/SpanUtils.scala create mode 100644 traces/commons/src/test/resources/logback-test.xml create mode 100644 traces/commons/src/test/scala/com/expedia/www/haystack/trace/commons/unit/BaseUnitTestSpec.scala create mode 100644 traces/commons/src/test/scala/com/expedia/www/haystack/trace/commons/unit/PackerSpec.scala create mode 100644 traces/commons/src/test/scala/com/expedia/www/haystack/trace/commons/unit/TraceIndexDocSpec.scala create mode 100644 traces/commons/src/test/scala/com/expedia/www/haystack/trace/commons/unit/WhitelistIndexFieldConfigurationSpec.scala create mode 100755 traces/deployment/scripts/publish-to-docker-hub.sh create mode 100755 traces/deployment/terraform/es-indices/curator-service-metadata/main.tf create mode 100755 traces/deployment/terraform/es-indices/curator-service-metadata/outputs.tf create mode 100755 traces/deployment/terraform/es-indices/curator-service-metadata/templates/curator-cron-job-yaml.tpl create mode 100755 traces/deployment/terraform/es-indices/curator-service-metadata/variables.tf create mode 100755 traces/deployment/terraform/es-indices/curator-trace-index/main.tf create mode 100755 traces/deployment/terraform/es-indices/curator-trace-index/outputs.tf create mode 100755 traces/deployment/terraform/es-indices/curator-trace-index/templates/curator-cron-job-yaml.tpl create mode 100755 traces/deployment/terraform/es-indices/curator-trace-index/variables.tf create mode 100644 traces/deployment/terraform/es-indices/main.tf create mode 100644 traces/deployment/terraform/es-indices/outputs.tf create mode 100644 traces/deployment/terraform/es-indices/variables.tf create mode 100755 traces/deployment/terraform/es-indices/whitelisted-fields/main.tf create mode 100755 traces/deployment/terraform/es-indices/whitelisted-fields/outputs.tf create mode 100755 traces/deployment/terraform/es-indices/whitelisted-fields/templates/whitelisted-fields-pod-yaml.tpl create mode 100755 traces/deployment/terraform/es-indices/whitelisted-fields/variables.tf create mode 100644 traces/deployment/terraform/main.tf create mode 100644 traces/deployment/terraform/outputs.tf create mode 100644 traces/deployment/terraform/trace-indexer/main.tf create mode 100644 traces/deployment/terraform/trace-indexer/outputs.tf create mode 100644 traces/deployment/terraform/trace-indexer/templates/deployment.yaml create mode 100644 traces/deployment/terraform/trace-indexer/templates/trace-indexer.conf create mode 100644 traces/deployment/terraform/trace-indexer/variables.tf create mode 100644 traces/deployment/terraform/trace-reader/main.tf create mode 100644 traces/deployment/terraform/trace-reader/outputs.tf create mode 100644 traces/deployment/terraform/trace-reader/templates/deployment.yaml create mode 100644 traces/deployment/terraform/trace-reader/templates/trace-reader.conf create mode 100644 traces/deployment/terraform/trace-reader/variables.tf create mode 100644 traces/deployment/terraform/variables.tf create mode 100644 traces/indexer/Makefile create mode 100644 traces/indexer/README.md create mode 100644 traces/indexer/build/docker/Dockerfile create mode 100644 traces/indexer/build/docker/jmxtrans-agent.xml create mode 100755 traces/indexer/build/docker/start-app.sh create mode 100644 traces/indexer/build/integration-tests/docker-compose.yml create mode 100644 traces/indexer/pom.xml create mode 100644 traces/indexer/src/main/resources/config/base.conf create mode 100644 traces/indexer/src/main/resources/logback.xml create mode 100644 traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/App.scala create mode 100644 traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/StreamRunner.scala create mode 100644 traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/config/ProjectConfiguration.scala create mode 100644 traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/config/entities/ElasticSearchConfiguration.scala create mode 100644 traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/config/entities/KafkaConfiguration.scala create mode 100644 traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/config/entities/ServiceMetadataWriteConfiguration.scala create mode 100644 traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/config/entities/SpanAccumulatorConfiguration.scala create mode 100644 traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/config/entities/TraceBackendConfiguration.scala create mode 100644 traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/metrics/AppMetricNames.scala create mode 100644 traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/processors/SpanIndexProcessor.scala create mode 100644 traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/processors/StateListener.scala create mode 100644 traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/processors/StreamProcessor.scala create mode 100644 traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/processors/StreamTaskRunnable.scala create mode 100644 traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/processors/supplier/SpanIndexProcessorSupplier.scala create mode 100644 traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/processors/supplier/StreamProcessorSupplier.scala create mode 100644 traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/serde/SpanDeserializer.scala create mode 100644 traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/store/DynamicCacheSizer.scala create mode 100644 traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/store/SpanBufferMemoryStoreSupplier.scala create mode 100644 traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/store/StoreSupplier.scala create mode 100644 traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/store/data/model/SpanBufferWithMetadata.scala create mode 100644 traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/store/impl/SpanBufferMemoryStore.scala create mode 100644 traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/store/traits/CacheSizeObserver.scala create mode 100644 traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/store/traits/EldestBufferedSpanEvictionListener.scala create mode 100644 traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/store/traits/SpanBufferKeyValueStore.scala create mode 100644 traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/writers/TraceWriter.scala create mode 100644 traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/writers/es/ElasticSearchResultHandler.scala create mode 100644 traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/writers/es/ElasticSearchWriter.scala create mode 100644 traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/writers/es/IndexDocumentGenerator.scala create mode 100644 traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/writers/es/IndexTemplateHandler.scala create mode 100644 traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/writers/es/ServiceMetadataDocumentGenerator.scala create mode 100644 traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/writers/es/ServiceMetadataWriter.scala create mode 100644 traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/writers/es/ThreadSafeBulkBuilder.scala create mode 100644 traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/writers/grpc/GrpcTraceWriter.scala create mode 100644 traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/writers/grpc/WriteSpansResponseObserver.scala create mode 100644 traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/writers/kafka/KafkaWriter.scala create mode 100644 traces/indexer/src/test/resources/config/base.conf create mode 100644 traces/indexer/src/test/resources/logback-test.xml create mode 100644 traces/indexer/src/test/scala/com/expedia/www/haystack/trace/indexer/integration/BaseIntegrationTestSpec.scala create mode 100644 traces/indexer/src/test/scala/com/expedia/www/haystack/trace/indexer/integration/EvictedSpanBufferSpec.scala create mode 100644 traces/indexer/src/test/scala/com/expedia/www/haystack/trace/indexer/integration/FailedTopologyRecoverySpec.scala create mode 100644 traces/indexer/src/test/scala/com/expedia/www/haystack/trace/indexer/integration/MultipleTraceIndexingTopologySpec.scala create mode 100644 traces/indexer/src/test/scala/com/expedia/www/haystack/trace/indexer/integration/PartialTraceIndexingTopologySpec.scala create mode 100644 traces/indexer/src/test/scala/com/expedia/www/haystack/trace/indexer/integration/ServiceMetadataIndexingTopologySpec.scala create mode 100644 traces/indexer/src/test/scala/com/expedia/www/haystack/trace/indexer/integration/clients/ElasticSearchTestClient.scala create mode 100644 traces/indexer/src/test/scala/com/expedia/www/haystack/trace/indexer/integration/clients/GrpcTestClient.scala create mode 100644 traces/indexer/src/test/scala/com/expedia/www/haystack/trace/indexer/integration/clients/KafkaTestClient.scala create mode 100644 traces/indexer/src/test/scala/com/expedia/www/haystack/trace/indexer/integration/serdes/TestSerdes.scala create mode 100644 traces/indexer/src/test/scala/com/expedia/www/haystack/trace/indexer/unit/ConfigurationLoaderSpec.scala create mode 100644 traces/indexer/src/test/scala/com/expedia/www/haystack/trace/indexer/unit/DynamicCacheSizerSpec.scala create mode 100644 traces/indexer/src/test/scala/com/expedia/www/haystack/trace/indexer/unit/ElasticSearchResultHandlerSpec.scala create mode 100644 traces/indexer/src/test/scala/com/expedia/www/haystack/trace/indexer/unit/ElasticSearchWriterUtilsSpec.scala create mode 100644 traces/indexer/src/test/scala/com/expedia/www/haystack/trace/indexer/unit/IndexTemplateHandlerSpec.scala create mode 100644 traces/indexer/src/test/scala/com/expedia/www/haystack/trace/indexer/unit/SpanBufferMemoryStoreSpec.scala create mode 100644 traces/indexer/src/test/scala/com/expedia/www/haystack/trace/indexer/unit/SpanIndexDocumentGeneratorSpec.scala create mode 100644 traces/indexer/src/test/scala/com/expedia/www/haystack/trace/indexer/unit/SpanIndexProcessorSpec.scala create mode 100644 traces/indexer/src/test/scala/com/expedia/www/haystack/trace/indexer/unit/SpanSerdeSpec.scala create mode 100644 traces/indexer/src/test/scala/com/expedia/www/haystack/trace/indexer/unit/ThreadSafeBulkBuilderSpec.scala create mode 100755 traces/mvnw create mode 100755 traces/mvnw.cmd create mode 100644 traces/pom.xml create mode 100644 traces/reader/Makefile create mode 100644 traces/reader/README.md create mode 100644 traces/reader/build/docker/Dockerfile create mode 100644 traces/reader/build/docker/jmxtrans-agent.xml create mode 100755 traces/reader/build/docker/start-app.sh create mode 100644 traces/reader/build/integration-tests/docker-app.conf create mode 100644 traces/reader/build/integration-tests/docker-compose.yml create mode 100644 traces/reader/pom.xml create mode 100644 traces/reader/src/main/resources/config/base.conf create mode 100644 traces/reader/src/main/resources/logback.xml create mode 100644 traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/Service.scala create mode 100644 traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/config/ProviderConfiguration.scala create mode 100644 traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/config/entities/ElasticSearchConfiguration.scala create mode 100644 traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/config/entities/ServiceConfiguration.scala create mode 100644 traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/config/entities/TraceTransformersConfiguration.scala create mode 100644 traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/config/entities/TraceValidatorsConfiguration.scala create mode 100644 traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/exceptions/ElasticSearchClientError.scala create mode 100644 traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/exceptions/InvalidTraceException.scala create mode 100644 traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/exceptions/InvalidTraceIdInDocument.scala create mode 100644 traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/exceptions/SpanNotFoundException.scala create mode 100644 traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/exceptions/TraceNotFoundException.scala create mode 100644 traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/metrics/AppMetricNames.scala create mode 100644 traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/TraceProcessor.scala create mode 100644 traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/TraceReader.scala create mode 100644 traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/transformers/ClientServerEventLogTransformer.scala create mode 100644 traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/transformers/ClockSkewFromParentTransformer.scala create mode 100644 traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/transformers/ClockSkewTransformer.scala create mode 100644 traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/transformers/DeDuplicateSpanTransformer.scala create mode 100644 traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/transformers/InfrastructureTagTransformer.scala create mode 100644 traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/transformers/InvalidParentTransformer.scala create mode 100644 traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/transformers/InvalidRootTransformer.scala create mode 100644 traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/transformers/OrphanedTraceTransformer.scala create mode 100644 traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/transformers/PartialSpanTransformer.scala create mode 100644 traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/transformers/PostTraceTransformationHandler.scala create mode 100644 traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/transformers/ServerClientSpanMergeTransformer.scala create mode 100644 traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/transformers/SortSpanTransformer.scala create mode 100644 traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/transformers/SpanTreeTransformer.scala create mode 100644 traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/transformers/TraceTransformationHandler.scala create mode 100644 traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/transformers/TraceTransformer.scala create mode 100644 traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/utils/AuxiliaryTags.scala create mode 100644 traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/utils/SpanMerger.scala create mode 100644 traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/utils/SpanTree.scala create mode 100644 traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/utils/TagBuilders.scala create mode 100644 traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/utils/TagExtractors.scala create mode 100644 traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/utils/TraceMerger.scala create mode 100644 traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/validators/ParentIdValidator.scala create mode 100644 traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/validators/RootValidator.scala create mode 100644 traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/validators/TraceIdValidator.scala create mode 100644 traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/validators/TraceValidationHandler.scala create mode 100644 traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/validators/TraceValidator.scala create mode 100644 traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/services/GrpcHandler.scala create mode 100644 traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/services/GrpcHealthService.scala create mode 100644 traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/services/TraceService.scala create mode 100644 traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/stores/EsIndexedTraceStore.scala create mode 100644 traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/stores/ResponseParser.scala create mode 100644 traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/stores/TraceStore.scala create mode 100644 traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/stores/readers/es/ESUtils.scala create mode 100644 traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/stores/readers/es/ElasticSearchCountResultListener.scala create mode 100644 traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/stores/readers/es/ElasticSearchReadResultListener.scala create mode 100644 traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/stores/readers/es/ElasticSearchReader.scala create mode 100644 traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/stores/readers/es/ElasticSearchResultListener.scala create mode 100644 traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/stores/readers/es/query/FieldValuesQueryGenerator.scala create mode 100644 traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/stores/readers/es/query/ServiceMetadataQueryGenerator.scala create mode 100644 traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/stores/readers/es/query/SpansIndexQueryGenerator.scala create mode 100644 traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/stores/readers/es/query/TraceCountsQueryGenerator.scala create mode 100644 traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/stores/readers/es/query/TraceSearchQueryGenerator.scala create mode 100644 traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/stores/readers/grpc/GrpcTraceReaders.scala create mode 100644 traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/stores/readers/grpc/ReadSpansResponseListener.scala create mode 100644 traces/reader/src/test/resources/config/base.conf create mode 100644 traces/reader/src/test/resources/logback-test.xml create mode 100644 traces/reader/src/test/resources/raw_trace.json create mode 100644 traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/integration/BaseIntegrationTestSpec.scala create mode 100644 traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/integration/TraceServiceIntegrationTestSpec.scala create mode 100644 traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/BaseUnitTestSpec.scala create mode 100644 traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/config/ConfigurationLoaderSpec.scala create mode 100644 traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/readers/TraceMergerSpec.scala create mode 100644 traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/readers/TraceProcessorSpec.scala create mode 100644 traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/readers/builders/ClockSkewedTraceBuilder.scala create mode 100644 traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/readers/builders/MultiRootTraceBuilder.scala create mode 100644 traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/readers/builders/MultiServerSpanTraceBuilder.scala create mode 100644 traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/readers/builders/TraceBuilder.scala create mode 100644 traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/readers/builders/ValidTraceBuilder.scala create mode 100644 traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/readers/transformers/ClientServerEventLogTransformerSpec.scala create mode 100644 traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/readers/transformers/ClockSkewFromParentTransformerSpec.scala create mode 100644 traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/readers/transformers/ClockSkewTransformerSpec.scala create mode 100644 traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/readers/transformers/DeDuplicateSpanTransformerSpec.scala create mode 100644 traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/readers/transformers/InfrastructureTagTransformerSpec.scala create mode 100644 traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/readers/transformers/InvalidParentTransformerSpec.scala create mode 100644 traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/readers/transformers/InvalidRootTransformerSpec.scala create mode 100644 traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/readers/transformers/OrphanedTraceTransformerSpec.scala create mode 100644 traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/readers/transformers/PartialSpanTransformerSpec.scala create mode 100644 traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/readers/transformers/ServerClientSpanMergeTransformerSpec.scala create mode 100644 traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/readers/transformers/SortSpanTransformerSpec.scala create mode 100644 traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/readers/validators/ParentIdValidatorSpec.scala create mode 100644 traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/readers/validators/RootValidatorSpec.scala create mode 100644 traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/readers/validators/TraceIdValidatorSpec.scala create mode 100644 traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/stores/ResponseParserSpec.scala create mode 100644 traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/stores/readers/es/query/ElasticSearchReadResultListenerSpec.scala create mode 100644 traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/stores/readers/es/query/FieldValuesQueryGeneratorSpec.scala create mode 100644 traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/stores/readers/es/query/ServiceMetadataQueryGeneratorSpec.scala create mode 100644 traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/stores/readers/es/query/TraceCountsQueryGeneratorSpec.scala create mode 100644 traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/stores/readers/es/query/TraceSearchQueryGeneratorSpec.scala create mode 100644 traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/stores/readers/es/query/helper/ExpressionTreeBuilder.scala create mode 100644 traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/stores/readers/grpc/ReadSpansResponseListenerSpec.scala create mode 100644 trends/.gitignore create mode 100755 trends/.mvn/wrapper/maven-wrapper.jar create mode 100755 trends/.mvn/wrapper/maven-wrapper.properties create mode 100644 trends/.travis.yml create mode 100644 trends/CONTRIBUTING.md create mode 100644 trends/LICENSE create mode 100644 trends/Makefile create mode 100644 trends/README.md create mode 100644 trends/Release.md create mode 100755 trends/deployment/scripts/publish-to-docker-hub.sh create mode 100644 trends/deployment/terraform/main.tf create mode 100644 trends/deployment/terraform/metrictank/main.tf create mode 100644 trends/deployment/terraform/metrictank/outputs.tf create mode 100644 trends/deployment/terraform/metrictank/templates/deployment_yaml.tpl create mode 100644 trends/deployment/terraform/metrictank/variables.tf create mode 100644 trends/deployment/terraform/outputs.tf create mode 100644 trends/deployment/terraform/span-timeseries-transformer/main.tf create mode 100644 trends/deployment/terraform/span-timeseries-transformer/outputs.tf create mode 100644 trends/deployment/terraform/span-timeseries-transformer/templates/deployment_yaml.tpl create mode 100644 trends/deployment/terraform/span-timeseries-transformer/templates/span-timeseries-transformer_conf.tpl create mode 100644 trends/deployment/terraform/span-timeseries-transformer/variables.tf create mode 100644 trends/deployment/terraform/timeseries-aggregator/main.tf create mode 100644 trends/deployment/terraform/timeseries-aggregator/outputs.tf create mode 100644 trends/deployment/terraform/timeseries-aggregator/templates/deployment_yaml.tpl create mode 100644 trends/deployment/terraform/timeseries-aggregator/templates/timeseries-aggregator_conf.tpl create mode 100644 trends/deployment/terraform/timeseries-aggregator/variables.tf create mode 100644 trends/deployment/terraform/variables.tf create mode 100644 trends/documents/diagrams/haystack_trends.png create mode 100755 trends/mvnw create mode 100755 trends/mvnw.cmd create mode 100644 trends/pom.xml create mode 100644 trends/scalastyle/scalastyle_config.xml create mode 100644 trends/span-timeseries-transformer/Makefile create mode 100644 trends/span-timeseries-transformer/README.md create mode 100644 trends/span-timeseries-transformer/build/docker/Dockerfile create mode 100644 trends/span-timeseries-transformer/build/docker/jmxtrans-agent.xml create mode 100755 trends/span-timeseries-transformer/build/docker/start-app.sh create mode 100644 trends/span-timeseries-transformer/pom.xml create mode 100644 trends/span-timeseries-transformer/src/main/resources/config/base.conf create mode 100644 trends/span-timeseries-transformer/src/main/resources/logback.xml create mode 100644 trends/span-timeseries-transformer/src/main/scala/com/expedia/www/haystack/trends/App.scala create mode 100644 trends/span-timeseries-transformer/src/main/scala/com/expedia/www/haystack/trends/MetricDataGenerator.scala create mode 100644 trends/span-timeseries-transformer/src/main/scala/com/expedia/www/haystack/trends/Streams.scala create mode 100644 trends/span-timeseries-transformer/src/main/scala/com/expedia/www/haystack/trends/config/AppConfiguration.scala create mode 100644 trends/span-timeseries-transformer/src/main/scala/com/expedia/www/haystack/trends/config/entities/KafkaConfiguration.scala create mode 100644 trends/span-timeseries-transformer/src/main/scala/com/expedia/www/haystack/trends/config/entities/TransformerConfiguration.scala create mode 100644 trends/span-timeseries-transformer/src/main/scala/com/expedia/www/haystack/trends/transformer/MetricDataTransformer.scala create mode 100644 trends/span-timeseries-transformer/src/main/scala/com/expedia/www/haystack/trends/transformer/SpanDurationMetricDataTransformer.scala create mode 100644 trends/span-timeseries-transformer/src/main/scala/com/expedia/www/haystack/trends/transformer/SpanStatusMetricDataTransformer.scala create mode 100644 trends/span-timeseries-transformer/src/test/resources/config/base.conf create mode 100644 trends/span-timeseries-transformer/src/test/resources/logback-test.xml create mode 100644 trends/span-timeseries-transformer/src/test/scala/com/expedia/www/haystack/trends/feature/FeatureSpec.scala create mode 100644 trends/span-timeseries-transformer/src/test/scala/com/expedia/www/haystack/trends/feature/tests/config/ConfigurationLoaderSpec.scala create mode 100644 trends/span-timeseries-transformer/src/test/scala/com/expedia/www/haystack/trends/feature/tests/kstreams/StreamsSpec.scala create mode 100644 trends/span-timeseries-transformer/src/test/scala/com/expedia/www/haystack/trends/feature/tests/transformer/MetricDataGeneratorSpec.scala create mode 100644 trends/span-timeseries-transformer/src/test/scala/com/expedia/www/haystack/trends/feature/tests/transformer/SpanDurationMetricDataTransformerSpec.scala create mode 100644 trends/span-timeseries-transformer/src/test/scala/com/expedia/www/haystack/trends/feature/tests/transformer/SpanStatusMetricDataTransformerSpec.scala create mode 100644 trends/span-timeseries-transformer/src/test/scala/com/expedia/www/haystack/trends/integration/IntegrationTestSpec.scala create mode 100644 trends/span-timeseries-transformer/src/test/scala/com/expedia/www/haystack/trends/integration/tests/TimeSeriesTransformerTopologySpec.scala create mode 100644 trends/timeseries-aggregator/Makefile create mode 100644 trends/timeseries-aggregator/README.md create mode 100644 trends/timeseries-aggregator/build/docker/Dockerfile create mode 100644 trends/timeseries-aggregator/build/docker/jmxtrans-agent.xml create mode 100755 trends/timeseries-aggregator/build/docker/start-app.sh create mode 100644 trends/timeseries-aggregator/pom.xml create mode 100644 trends/timeseries-aggregator/src/main/resources/config/base.conf create mode 100644 trends/timeseries-aggregator/src/main/resources/logback.xml create mode 100644 trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/App.scala create mode 100644 trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/aggregation/TrendHdrHistogram.scala create mode 100644 trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/aggregation/TrendMetric.scala create mode 100644 trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/aggregation/WindowedMetric.scala create mode 100644 trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/aggregation/entities/StatValue.scala create mode 100644 trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/aggregation/entities/TimeWindow.scala create mode 100644 trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/aggregation/metrics/CountMetric.scala create mode 100644 trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/aggregation/metrics/HistogramMetric.scala create mode 100644 trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/aggregation/metrics/Metric.scala create mode 100644 trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/aggregation/rules/DurationMetricRule.scala create mode 100644 trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/aggregation/rules/FailureMetricRule.scala create mode 100644 trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/aggregation/rules/LatencyMetricRule.scala create mode 100644 trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/aggregation/rules/MetricRule.scala create mode 100644 trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/aggregation/rules/MetricRuleEngine.scala create mode 100644 trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/aggregation/rules/SuccessMetricRule.scala create mode 100644 trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/config/AppConfiguration.scala create mode 100644 trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/config/entities/HistogramMetricConfiguration.scala create mode 100644 trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/config/entities/KafkaConfiguration.scala create mode 100644 trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/config/entities/KafkaProduceConfiguration.scala create mode 100644 trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/config/entities/StateStoreConfiguration.scala create mode 100644 trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/kstream/Streams.scala create mode 100644 trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/kstream/processor/AdditionalTagsProcessorSupplier.scala create mode 100644 trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/kstream/processor/ExternalKafkaProcessorSupplier.scala create mode 100644 trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/kstream/processor/MetricAggProcessorSupplier.scala create mode 100644 trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/kstream/serde/TrendMetricSerde.scala create mode 100644 trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/kstream/serde/WindowedMetricSerde.scala create mode 100644 trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/kstream/serde/metric/CountMetricSerde.scala create mode 100644 trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/kstream/serde/metric/HistogramMetricSerde.scala create mode 100644 trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/kstream/serde/metric/MetricSerde.scala create mode 100644 trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/kstream/store/HaystackStoreBuilder.scala create mode 100644 trends/timeseries-aggregator/src/test/resources/config/base.conf create mode 100644 trends/timeseries-aggregator/src/test/resources/logback-test.xml create mode 100644 trends/timeseries-aggregator/src/test/scala/com/expedia/www/haystack/trends/feature/FeatureSpec.scala create mode 100644 trends/timeseries-aggregator/src/test/scala/com/expedia/www/haystack/trends/feature/tests/aggregation/TrendMetricSpec.scala create mode 100644 trends/timeseries-aggregator/src/test/scala/com/expedia/www/haystack/trends/feature/tests/aggregation/WindowedMetricSpec.scala create mode 100644 trends/timeseries-aggregator/src/test/scala/com/expedia/www/haystack/trends/feature/tests/aggregation/metrics/CountMetricSpec.scala create mode 100644 trends/timeseries-aggregator/src/test/scala/com/expedia/www/haystack/trends/feature/tests/aggregation/metrics/HistogramMetricSpec.scala create mode 100644 trends/timeseries-aggregator/src/test/scala/com/expedia/www/haystack/trends/feature/tests/aggregation/rules/DurationMetricRuleSpec.scala create mode 100644 trends/timeseries-aggregator/src/test/scala/com/expedia/www/haystack/trends/feature/tests/aggregation/rules/FailureMetricRuleSpec.scala create mode 100644 trends/timeseries-aggregator/src/test/scala/com/expedia/www/haystack/trends/feature/tests/aggregation/rules/LatencyMetricRuleSpec.scala create mode 100644 trends/timeseries-aggregator/src/test/scala/com/expedia/www/haystack/trends/feature/tests/aggregation/rules/SuccessMetricRuleSpec.scala create mode 100644 trends/timeseries-aggregator/src/test/scala/com/expedia/www/haystack/trends/feature/tests/config/ConfigurationLoaderSpec.scala create mode 100644 trends/timeseries-aggregator/src/test/scala/com/expedia/www/haystack/trends/feature/tests/kstreams/StreamsSpec.scala create mode 100644 trends/timeseries-aggregator/src/test/scala/com/expedia/www/haystack/trends/feature/tests/kstreams/processor/AdditionalTagsProcessorSupplierSpec.scala create mode 100644 trends/timeseries-aggregator/src/test/scala/com/expedia/www/haystack/trends/feature/tests/kstreams/processor/MetricAggProcessorSupplierSpec.scala create mode 100644 trends/timeseries-aggregator/src/test/scala/com/expedia/www/haystack/trends/feature/tests/kstreams/serde/TrendMetricSerdeSpec.scala create mode 100644 trends/timeseries-aggregator/src/test/scala/com/expedia/www/haystack/trends/feature/tests/kstreams/serde/WindowedMetricSerdeSpec.scala create mode 100644 trends/timeseries-aggregator/src/test/scala/com/expedia/www/haystack/trends/feature/tests/kstreams/store/HaystackStoreBuilderSpec.scala create mode 100644 trends/timeseries-aggregator/src/test/scala/com/expedia/www/haystack/trends/integration/IntegrationTestSpec.scala create mode 100644 trends/timeseries-aggregator/src/test/scala/com/expedia/www/haystack/trends/integration/tests/CountTrendsSpec.scala create mode 100644 trends/timeseries-aggregator/src/test/scala/com/expedia/www/haystack/trends/integration/tests/HistogramTrendsSpec.scala create mode 100644 trends/timeseries-aggregator/src/test/scala/com/expedia/www/haystack/trends/integration/tests/StateStoreSpec.scala create mode 100644 trends/timeseries-aggregator/src/test/scala/com/expedia/www/haystack/trends/integration/tests/WatermarkingSpec.scala create mode 100644 ui/.babelrc create mode 100644 ui/.coveralls.yml create mode 100644 ui/.dockerignore create mode 100644 ui/.eslintignore create mode 100644 ui/.eslintrc create mode 100644 ui/.gitignore create mode 100644 ui/.gitmodules create mode 100644 ui/.npmrc create mode 100644 ui/.prettierignore create mode 100644 ui/.prettierrc create mode 100644 ui/.travis.yml create mode 100644 ui/.vscode/settings.json create mode 100644 ui/CONTRIBUTING.md create mode 100644 ui/LICENSE create mode 100644 ui/Makefile create mode 100644 ui/README.md create mode 100644 ui/Release.md create mode 100644 ui/build/docker/Dockerfile create mode 100755 ui/build/docker/publish-to-docker-hub.sh create mode 100644 ui/build/zipkin/README.md create mode 100644 ui/build/zipkin/base.json create mode 100755 ui/build/zipkin/zipkin-quickstart.sh create mode 100644 ui/deployment/terraform/main.tf create mode 100644 ui/deployment/terraform/outputs.tf create mode 100644 ui/deployment/terraform/templates/deployment_yaml.tpl create mode 100644 ui/deployment/terraform/templates/haystack-ui_json.tpl create mode 100644 ui/deployment/terraform/variables.tf create mode 100644 ui/package-lock.json create mode 100644 ui/package.json create mode 100644 ui/public/assets.json create mode 100755 ui/public/favicon.ico create mode 100644 ui/public/fonts/LICENSE_THEMIFY create mode 100755 ui/public/fonts/LICENSE_TITILLIUM_WEB create mode 100755 ui/public/fonts/themify.ttf create mode 100755 ui/public/fonts/themify.woff create mode 100644 ui/public/fonts/titillium-web-v5-latin-200.ttf create mode 100644 ui/public/fonts/titillium-web-v5-latin-200.woff create mode 100644 ui/public/fonts/titillium-web-v5-latin-200.woff2 create mode 100644 ui/public/fonts/titillium-web-v5-latin-300.ttf create mode 100644 ui/public/fonts/titillium-web-v5-latin-300.woff create mode 100644 ui/public/fonts/titillium-web-v5-latin-300.woff2 create mode 100644 ui/public/fonts/titillium-web-v5-latin-600.ttf create mode 100644 ui/public/fonts/titillium-web-v5-latin-600.woff create mode 100644 ui/public/fonts/titillium-web-v5-latin-600.woff2 create mode 100644 ui/public/fonts/titillium-web-v5-latin-700.ttf create mode 100644 ui/public/fonts/titillium-web-v5-latin-700.woff create mode 100644 ui/public/fonts/titillium-web-v5-latin-700.woff2 create mode 100644 ui/public/fonts/titillium-web-v5-latin-regular.ttf create mode 100644 ui/public/fonts/titillium-web-v5-latin-regular.woff create mode 100644 ui/public/fonts/titillium-web-v5-latin-regular.woff2 create mode 100644 ui/public/images/assets/alerts.png create mode 100644 ui/public/images/assets/demo.gif create mode 100644 ui/public/images/assets/logo_lighter.png create mode 100644 ui/public/images/assets/logo_with_title.png create mode 100644 ui/public/images/assets/logo_with_title_dark.png create mode 100644 ui/public/images/assets/logo_with_title_transparent.png create mode 100644 ui/public/images/assets/service_graph.png create mode 100644 ui/public/images/assets/trace_timeline.png create mode 100644 ui/public/images/assets/trends.png create mode 100644 ui/public/images/assets/universal_search.png create mode 100644 ui/public/images/error.svg create mode 100644 ui/public/images/loading.gif create mode 100644 ui/public/images/logo-white.png create mode 100644 ui/public/images/logo.png create mode 100644 ui/public/images/slack.png create mode 100644 ui/public/images/success.svg create mode 100644 ui/public/images/zipkin-logo.jpg create mode 100644 ui/public/scripts/particles.json create mode 100755 ui/public/scripts/particles.min.js create mode 100644 ui/server/app.js create mode 100644 ui/server/config/base.js create mode 100644 ui/server/config/config.js create mode 100644 ui/server/config/override.js create mode 100644 ui/server/connectors/alerts/haystack/alertsConnector.js create mode 100644 ui/server/connectors/alerts/haystack/expressionTreeBuilder.js create mode 100644 ui/server/connectors/alerts/haystack/subscriptionsConnector.js create mode 100644 ui/server/connectors/alerts/stub/alertsConnector.js create mode 100644 ui/server/connectors/alerts/stub/subscriptionsConnector.js create mode 100644 ui/server/connectors/operations/grpcDeleter.js create mode 100644 ui/server/connectors/operations/grpcFetcher.js create mode 100644 ui/server/connectors/operations/grpcPoster.js create mode 100644 ui/server/connectors/operations/grpcPutter.js create mode 100644 ui/server/connectors/operations/restFetcher.js create mode 100644 ui/server/connectors/serviceGraph/haystack/graphDataExtractor.js create mode 100644 ui/server/connectors/serviceGraph/haystack/serviceGraphConnector.js create mode 100644 ui/server/connectors/serviceGraph/stub/serviceGraphConnector.js create mode 100644 ui/server/connectors/serviceGraph/zipkin/converter.js create mode 100644 ui/server/connectors/serviceGraph/zipkin/serviceGraphConnector.js create mode 100644 ui/server/connectors/serviceInsights/detectCycles.js create mode 100644 ui/server/connectors/serviceInsights/fetcher.js create mode 100644 ui/server/connectors/serviceInsights/graphDataExtractor.js create mode 100644 ui/server/connectors/serviceInsights/serviceInsightsConnector.js create mode 100644 ui/server/connectors/services/servicesConnector.js create mode 100644 ui/server/connectors/traces/haystack/expressionTreeBuilder.js create mode 100644 ui/server/connectors/traces/haystack/protobufConverters/callGraphConverter.js create mode 100644 ui/server/connectors/traces/haystack/protobufConverters/traceConverter.js create mode 100644 ui/server/connectors/traces/haystack/protobufConverters/traceCountsConverter.js create mode 100644 ui/server/connectors/traces/haystack/search/searchRequestBuilder.js create mode 100644 ui/server/connectors/traces/haystack/search/searchResultsTransformer.js create mode 100644 ui/server/connectors/traces/haystack/timeline/traceCountsRequestBuilder.js create mode 100644 ui/server/connectors/traces/haystack/tracesConnector.js create mode 100644 ui/server/connectors/traces/mock/mock-web-ui.js create mode 100644 ui/server/connectors/traces/mock/spanTypes.js create mode 100644 ui/server/connectors/traces/mock/tracesConnector.js create mode 100644 ui/server/connectors/traces/mock/tracesGenerator.js create mode 100644 ui/server/connectors/traces/stub/tracesConnector.js create mode 100644 ui/server/connectors/traces/zipkin/converter.js create mode 100644 ui/server/connectors/traces/zipkin/tracesConnector.js create mode 100644 ui/server/connectors/trends/haystack/trendsConnector.js create mode 100644 ui/server/connectors/trends/stub/trendsConnector.js create mode 100644 ui/server/connectors/utils/LoaderBackedCache.js create mode 100644 ui/server/connectors/utils/encoders/Base64Encoder.js create mode 100644 ui/server/connectors/utils/encoders/MetricpointNameEncoder.js create mode 100644 ui/server/connectors/utils/encoders/NoopEncoder.js create mode 100644 ui/server/connectors/utils/encoders/PeriodReplacementEncoder.js create mode 100644 ui/server/connectors/utils/errorConverter.js create mode 100644 ui/server/connectors/utils/objectUtils.js create mode 100644 ui/server/routes/alertsApi.js create mode 100644 ui/server/routes/auth.js create mode 100644 ui/server/routes/index.js create mode 100644 ui/server/routes/login.js create mode 100644 ui/server/routes/serviceGraphApi.js create mode 100644 ui/server/routes/serviceInsightsApi.js create mode 100644 ui/server/routes/servicesApi.js create mode 100644 ui/server/routes/servicesPerfApi.js create mode 100644 ui/server/routes/sso.js create mode 100644 ui/server/routes/tracesApi.js create mode 100644 ui/server/routes/trendsApi.js create mode 100644 ui/server/routes/user.js create mode 100644 ui/server/routes/utils/apiResponseHandler.js create mode 100644 ui/server/sso/authChecker.js create mode 100644 ui/server/sso/samlSsoAuthenticator.js create mode 100644 ui/server/start.js create mode 100644 ui/server/utils/logger.js create mode 100644 ui/server/utils/metrics.js create mode 100644 ui/server/utils/metricsMiddleware.js create mode 100644 ui/server/utils/metricsReporter.js create mode 100644 ui/server/utils/server.js create mode 100644 ui/server/views/index.pug create mode 100644 ui/src/app.jsx create mode 100644 ui/src/app.less create mode 100755 ui/src/bootstrap/LICENSE_BOOTSTRAP create mode 100644 ui/src/bootstrap/LICENSE_BOOTSWATCH create mode 100755 ui/src/bootstrap/alerts.less create mode 100755 ui/src/bootstrap/badges.less create mode 100755 ui/src/bootstrap/bootstrap.less create mode 100644 ui/src/bootstrap/bootswatch.less create mode 100755 ui/src/bootstrap/breadcrumbs.less create mode 100755 ui/src/bootstrap/button-groups.less create mode 100755 ui/src/bootstrap/buttons.less create mode 100755 ui/src/bootstrap/carousel.less create mode 100755 ui/src/bootstrap/close.less create mode 100755 ui/src/bootstrap/code.less create mode 100755 ui/src/bootstrap/component-animations.less create mode 100755 ui/src/bootstrap/dropdowns.less create mode 100755 ui/src/bootstrap/forms.less create mode 100755 ui/src/bootstrap/grid.less create mode 100755 ui/src/bootstrap/input-groups.less create mode 100755 ui/src/bootstrap/jumbotron.less create mode 100755 ui/src/bootstrap/labels.less create mode 100755 ui/src/bootstrap/list-group.less create mode 100755 ui/src/bootstrap/media.less create mode 100755 ui/src/bootstrap/mixins.less create mode 100755 ui/src/bootstrap/mixins/alerts.less create mode 100755 ui/src/bootstrap/mixins/background-variant.less create mode 100755 ui/src/bootstrap/mixins/border-radius.less create mode 100755 ui/src/bootstrap/mixins/buttons.less create mode 100755 ui/src/bootstrap/mixins/center-block.less create mode 100755 ui/src/bootstrap/mixins/clearfix.less create mode 100755 ui/src/bootstrap/mixins/forms.less create mode 100755 ui/src/bootstrap/mixins/gradients.less create mode 100755 ui/src/bootstrap/mixins/grid-framework.less create mode 100755 ui/src/bootstrap/mixins/grid.less create mode 100755 ui/src/bootstrap/mixins/hide-text.less create mode 100755 ui/src/bootstrap/mixins/image.less create mode 100755 ui/src/bootstrap/mixins/labels.less create mode 100755 ui/src/bootstrap/mixins/list-group.less create mode 100755 ui/src/bootstrap/mixins/nav-divider.less create mode 100755 ui/src/bootstrap/mixins/nav-vertical-align.less create mode 100755 ui/src/bootstrap/mixins/opacity.less create mode 100755 ui/src/bootstrap/mixins/pagination.less create mode 100755 ui/src/bootstrap/mixins/panels.less create mode 100755 ui/src/bootstrap/mixins/progress-bar.less create mode 100755 ui/src/bootstrap/mixins/reset-filter.less create mode 100755 ui/src/bootstrap/mixins/reset-text.less create mode 100755 ui/src/bootstrap/mixins/resize.less create mode 100755 ui/src/bootstrap/mixins/responsive-visibility.less create mode 100755 ui/src/bootstrap/mixins/size.less create mode 100755 ui/src/bootstrap/mixins/tab-focus.less create mode 100755 ui/src/bootstrap/mixins/table-row.less create mode 100755 ui/src/bootstrap/mixins/text-emphasis.less create mode 100755 ui/src/bootstrap/mixins/text-overflow.less create mode 100755 ui/src/bootstrap/mixins/vendor-prefixes.less create mode 100755 ui/src/bootstrap/modals.less create mode 100755 ui/src/bootstrap/navbar.less create mode 100755 ui/src/bootstrap/navs.less create mode 100755 ui/src/bootstrap/normalize.less create mode 100755 ui/src/bootstrap/pager.less create mode 100755 ui/src/bootstrap/pagination.less create mode 100755 ui/src/bootstrap/panels.less create mode 100755 ui/src/bootstrap/popovers.less create mode 100755 ui/src/bootstrap/print.less create mode 100755 ui/src/bootstrap/progress-bars.less create mode 100755 ui/src/bootstrap/responsive-embed.less create mode 100755 ui/src/bootstrap/responsive-utilities.less create mode 100755 ui/src/bootstrap/scaffolding.less create mode 100644 ui/src/bootstrap/svc-colors.less create mode 100755 ui/src/bootstrap/tables.less create mode 100755 ui/src/bootstrap/theme.less create mode 100755 ui/src/bootstrap/themify-icons.less create mode 100755 ui/src/bootstrap/thumbnails.less create mode 100755 ui/src/bootstrap/titillium-web.less create mode 100755 ui/src/bootstrap/tooltip.less create mode 100755 ui/src/bootstrap/type.less create mode 100755 ui/src/bootstrap/utilities.less create mode 100755 ui/src/bootstrap/variables.less create mode 100755 ui/src/bootstrap/wells.less create mode 100644 ui/src/components/alerts/alertCounter.jsx create mode 100644 ui/src/components/alerts/alertTabs.jsx create mode 100644 ui/src/components/alerts/alerts.jsx create mode 100644 ui/src/components/alerts/alerts.less create mode 100644 ui/src/components/alerts/alertsTable.jsx create mode 100644 ui/src/components/alerts/alertsTableSparkline.jsx create mode 100644 ui/src/components/alerts/alertsToolbar.jsx create mode 100644 ui/src/components/alerts/details/alertDetails.jsx create mode 100644 ui/src/components/alerts/details/alertDetailsToolbar.jsx create mode 100644 ui/src/components/alerts/details/alertHistory.jsx create mode 100644 ui/src/components/alerts/details/alertSubscriptions.jsx create mode 100644 ui/src/components/alerts/details/subscriptionRow.jsx create mode 100644 ui/src/components/alerts/stores/alertDetailsStore.js create mode 100644 ui/src/components/alerts/stores/alertTrendFetcher.js create mode 100644 ui/src/components/alerts/stores/serviceAlertsStore.js create mode 100644 ui/src/components/alerts/utils/subscriptionConstructor.js create mode 100644 ui/src/components/common/error.jsx create mode 100644 ui/src/components/common/error.less create mode 100644 ui/src/components/common/loading.jsx create mode 100644 ui/src/components/common/loading.less create mode 100644 ui/src/components/common/login.jsx create mode 100644 ui/src/components/common/login.less create mode 100644 ui/src/components/common/modal.jsx create mode 100644 ui/src/components/common/modal.less create mode 100644 ui/src/components/common/noMatch.jsx create mode 100644 ui/src/components/common/resultsTable.less create mode 100644 ui/src/components/common/timeRangePicker.jsx create mode 100644 ui/src/components/common/timeRangePicker.less create mode 100644 ui/src/components/common/withTracker.jsx create mode 100644 ui/src/components/common/workInProgress.jsx create mode 100644 ui/src/components/docs/help.jsx create mode 100644 ui/src/components/layout/authenticationTimeoutModal.jsx create mode 100644 ui/src/components/layout/footer.jsx create mode 100644 ui/src/components/layout/footer.less create mode 100644 ui/src/components/layout/slimHeader.jsx create mode 100644 ui/src/components/serviceGraph/connectionDetails.jsx create mode 100644 ui/src/components/serviceGraph/graphSearch.jsx create mode 100644 ui/src/components/serviceGraph/nodeDetails.jsx create mode 100644 ui/src/components/serviceGraph/serviceGraph.jsx create mode 100644 ui/src/components/serviceGraph/serviceGraph.less create mode 100644 ui/src/components/serviceGraph/serviceGraphContainer.jsx create mode 100644 ui/src/components/serviceGraph/serviceGraphResults.jsx create mode 100644 ui/src/components/serviceGraph/stores/serviceGraphStore.js create mode 100644 ui/src/components/serviceGraph/trafficTable.jsx create mode 100644 ui/src/components/serviceGraph/util/graph.js create mode 100644 ui/src/components/serviceGraph/vizceralConfig.js create mode 100644 ui/src/components/serviceGraph/vizceralExt.jsx create mode 100644 ui/src/components/serviceInsights/serviceInsights.jsx create mode 100644 ui/src/components/serviceInsights/serviceInsights.less create mode 100644 ui/src/components/serviceInsights/serviceInsightsGraph/dataLayout.js create mode 100644 ui/src/components/serviceInsights/serviceInsightsGraph/dragGroup.jsx create mode 100644 ui/src/components/serviceInsights/serviceInsightsGraph/icons/db.svg create mode 100644 ui/src/components/serviceInsights/serviceInsightsGraph/icons/gateway.svg create mode 100644 ui/src/components/serviceInsights/serviceInsightsGraph/icons/outbound.svg create mode 100644 ui/src/components/serviceInsights/serviceInsightsGraph/icons/uninstrumented.svg create mode 100644 ui/src/components/serviceInsights/serviceInsightsGraph/label.jsx create mode 100644 ui/src/components/serviceInsights/serviceInsightsGraph/labels.jsx create mode 100644 ui/src/components/serviceInsights/serviceInsightsGraph/lines.jsx create mode 100644 ui/src/components/serviceInsights/serviceInsightsGraph/nodes.jsx create mode 100644 ui/src/components/serviceInsights/serviceInsightsGraph/serviceInsightsGraph.jsx create mode 100644 ui/src/components/serviceInsights/serviceInsightsGraph/serviceInsightsGraph.less create mode 100644 ui/src/components/serviceInsights/serviceInsightsGraph/tooltip.jsx create mode 100644 ui/src/components/serviceInsights/serviceInsightsGraph/tooltip.less create mode 100644 ui/src/components/serviceInsights/stores/serviceInsightsStore.js create mode 100644 ui/src/components/serviceInsights/summary.jsx create mode 100644 ui/src/components/servicePerf/servicePerformance.jsx create mode 100644 ui/src/components/servicePerf/servicePerformance.less create mode 100644 ui/src/components/servicePerf/stores/servicePerfStore.js create mode 100644 ui/src/components/traces/details/latency/latencyCostTab.jsx create mode 100644 ui/src/components/traces/details/latency/latencyCostTabContainer.jsx create mode 100644 ui/src/components/traces/details/rawTraceModal.jsx create mode 100644 ui/src/components/traces/details/relatedTraces/relatedTracesRow.jsx create mode 100644 ui/src/components/traces/details/relatedTraces/relatedTracesTab.jsx create mode 100644 ui/src/components/traces/details/relatedTraces/relatedTracesTabContainer.jsx create mode 100644 ui/src/components/traces/details/timeline/logsTable.jsx create mode 100644 ui/src/components/traces/details/timeline/rawSpan.jsx create mode 100644 ui/src/components/traces/details/timeline/span.jsx create mode 100644 ui/src/components/traces/details/timeline/spanDetailsModal.jsx create mode 100644 ui/src/components/traces/details/timeline/tagsTable.jsx create mode 100644 ui/src/components/traces/details/timeline/timelineTab.jsx create mode 100644 ui/src/components/traces/details/timeline/timelineTabContainer.jsx create mode 100644 ui/src/components/traces/details/traceDetails.jsx create mode 100644 ui/src/components/traces/details/traceDetails.less create mode 100644 ui/src/components/traces/details/trends/serviceOperationTrendRow.jsx create mode 100644 ui/src/components/traces/details/trends/trendsTab.jsx create mode 100644 ui/src/components/traces/details/trends/trendsTabContainer.jsx create mode 100644 ui/src/components/traces/results/noSearch.jsx create mode 100644 ui/src/components/traces/results/noSearch.less create mode 100644 ui/src/components/traces/results/spanResultsTable.jsx create mode 100644 ui/src/components/traces/results/spansView.jsx create mode 100644 ui/src/components/traces/results/tagsFilter.jsx create mode 100644 ui/src/components/traces/results/traceResults.jsx create mode 100644 ui/src/components/traces/results/traceResultsTable.jsx create mode 100644 ui/src/components/traces/results/traceTimeline.jsx create mode 100644 ui/src/components/traces/results/traceTimeline.less create mode 100644 ui/src/components/traces/results/tracesContainer.jsx create mode 100644 ui/src/components/traces/stores/latencyCostStore.js create mode 100644 ui/src/components/traces/stores/rawSpanStore.js create mode 100644 ui/src/components/traces/stores/rawTraceStore.js create mode 100644 ui/src/components/traces/stores/searchableKeysStore.js create mode 100644 ui/src/components/traces/stores/spansSearchStore.js create mode 100644 ui/src/components/traces/stores/traceDetailsStore.js create mode 100644 ui/src/components/traces/stores/traceTrendFetcher.js create mode 100644 ui/src/components/traces/stores/tracesSearchStore.js create mode 100644 ui/src/components/traces/traces.less create mode 100644 ui/src/components/traces/upload/uploadContainer.jsx create mode 100644 ui/src/components/traces/upload/uploadHeader.jsx create mode 100644 ui/src/components/traces/utils/auxiliaryTags.js create mode 100644 ui/src/components/traces/utils/presets.js create mode 100644 ui/src/components/traces/utils/traceQueryParser.js create mode 100644 ui/src/components/trends/details/graphs/countGraph.jsx create mode 100644 ui/src/components/trends/details/graphs/durationGraph.jsx create mode 100644 ui/src/components/trends/details/graphs/graphContainer.jsx create mode 100644 ui/src/components/trends/details/graphs/graphContainer.less create mode 100644 ui/src/components/trends/details/graphs/missingTrend.jsx create mode 100644 ui/src/components/trends/details/graphs/missingTrend.less create mode 100644 ui/src/components/trends/details/graphs/options.js create mode 100644 ui/src/components/trends/details/graphs/successGraph.jsx create mode 100644 ui/src/components/trends/details/trendDetails.jsx create mode 100644 ui/src/components/trends/details/trendDetails.less create mode 100644 ui/src/components/trends/details/trendDetailsToolbar.jsx create mode 100644 ui/src/components/trends/details/trendDetailsToolbar.less create mode 100644 ui/src/components/trends/operation/operationResults.jsx create mode 100644 ui/src/components/trends/operation/operationResults.less create mode 100644 ui/src/components/trends/operation/operationResultsHeatmap.jsx create mode 100644 ui/src/components/trends/operation/operationResultsHeatmap.less create mode 100644 ui/src/components/trends/operation/operationResultsTable.jsx create mode 100644 ui/src/components/trends/operation/operationResultsTable.less create mode 100644 ui/src/components/trends/stores/operationStore.js create mode 100644 ui/src/components/trends/utils/metricGranularity.js create mode 100644 ui/src/components/trends/utils/trendSparklines.jsx create mode 100644 ui/src/components/trends/utils/trendsTableFormatters.jsx create mode 100644 ui/src/components/universalSearch/searchBar/autosuggest.jsx create mode 100644 ui/src/components/universalSearch/searchBar/autosuggest.less create mode 100644 ui/src/components/universalSearch/searchBar/chips.jsx create mode 100644 ui/src/components/universalSearch/searchBar/guide.jsx create mode 100644 ui/src/components/universalSearch/searchBar/queryBank.jsx create mode 100644 ui/src/components/universalSearch/searchBar/searchBar.jsx create mode 100644 ui/src/components/universalSearch/searchBar/searchSubmit.jsx create mode 100644 ui/src/components/universalSearch/searchBar/stores/searchBarUiStateStore.js create mode 100644 ui/src/components/universalSearch/searchBar/stores/searchableKeysStore.js create mode 100644 ui/src/components/universalSearch/searchBar/suggestions.jsx create mode 100644 ui/src/components/universalSearch/searchBar/timeRangePicker.jsx create mode 100644 ui/src/components/universalSearch/searchBar/timeRangePicker.less create mode 100644 ui/src/components/universalSearch/searchBar/timeWindowPicker.jsx create mode 100644 ui/src/components/universalSearch/tabs/emptyTabPlaceholder.jsx create mode 100644 ui/src/components/universalSearch/tabs/externalLinksList.jsx create mode 100644 ui/src/components/universalSearch/tabs/serviceGraph.jsx create mode 100644 ui/src/components/universalSearch/tabs/serviceInsights.jsx create mode 100644 ui/src/components/universalSearch/tabs/servicePerformance.jsx create mode 100644 ui/src/components/universalSearch/tabs/tabStores/alertsTabStateStore.js create mode 100644 ui/src/components/universalSearch/tabs/tabStores/serviceGraphStateStore.js create mode 100644 ui/src/components/universalSearch/tabs/tabStores/serviceInsightsTabStateStore.js create mode 100644 ui/src/components/universalSearch/tabs/tabStores/servicePerformanceStateStore.js create mode 100644 ui/src/components/universalSearch/tabs/tabStores/tracesTabStateStore.js create mode 100644 ui/src/components/universalSearch/tabs/tabStores/trendsTabStateStore.js create mode 100644 ui/src/components/universalSearch/tabs/tabs.jsx create mode 100644 ui/src/components/universalSearch/universalSearch.jsx create mode 100644 ui/src/components/universalSearch/universalSearch.less create mode 100644 ui/src/components/universalSearch/utils/urlUtils.js create mode 100644 ui/src/main.jsx create mode 100644 ui/src/stores/authenticationStore.js create mode 100644 ui/src/stores/errorHandlingStore.js create mode 100644 ui/src/stores/operationStore.js create mode 100644 ui/src/stores/serviceStore.js create mode 100644 ui/src/stores/storesInitializer.js create mode 100644 ui/src/utils/blobUtil.js create mode 100644 ui/src/utils/externalLinkFormatter.jsx create mode 100644 ui/src/utils/formatters.js create mode 100644 ui/src/utils/hashUtil.js create mode 100644 ui/src/utils/linkBuilder.js create mode 100644 ui/src/utils/loginRenewer.js create mode 100644 ui/src/utils/queryParser.js create mode 100644 ui/src/utils/serviceColorMapper.js create mode 100644 ui/src/utils/timeWindow.js create mode 100644 ui/src/utils/validUrl.js create mode 100644 ui/test/server/config/override.spec.js create mode 100644 ui/test/server/connectors/serviceGraph/haystack/serviceGraphConnector.spec.js create mode 100644 ui/test/server/connectors/serviceGraph/zipkin/converter.spec.js create mode 100644 ui/test/server/connectors/serviceInsights/detectCycles.spec.js create mode 100644 ui/test/server/connectors/serviceInsights/fetcher.spec.js create mode 100644 ui/test/server/connectors/serviceInsights/graphDataExtractor.spec.js create mode 100644 ui/test/server/connectors/serviceInsights/serviceInsightsConnector.spec.js create mode 100644 ui/test/server/connectors/traces/haystack/search/searchResultsTransformer.spec.js create mode 100644 ui/test/server/connectors/traces/haystack/tracesConnector.spec.js create mode 100644 ui/test/server/connectors/traces/zipkin/converter.spec.js create mode 100644 ui/test/server/connectors/traces/zipkin/tracesConnector.spec.js create mode 100644 ui/test/server/connectors/trends/haystack/trendsConnector.spec.js create mode 100644 ui/test/server/connectors/utils/encoders/Base64Encoder.spec.js create mode 100644 ui/test/server/connectors/utils/encoders/MetricpointNameEncoder.spec.js create mode 100644 ui/test/server/connectors/utils/encoders/NoopEncoder.spec.js create mode 100644 ui/test/server/connectors/utils/encoders/PeriodReplacementEncoder.spec.js create mode 100644 ui/test/server/routes/alertsApi.spec.js create mode 100644 ui/test/server/routes/index.spec.js create mode 100644 ui/test/server/routes/serviceInsightsApi.spec.js create mode 100644 ui/test/server/routes/servicesApi.spec.js create mode 100644 ui/test/server/routes/servicesPerfApi.spec.js create mode 100644 ui/test/server/routes/tracesApi.spec.js create mode 100644 ui/test/server/routes/trendsApi.spec.js create mode 100644 ui/test/src/components/alerts.spec.jsx create mode 100644 ui/test/src/components/common/help.spec.jsx create mode 100644 ui/test/src/components/common/noMatch.spec.jsx create mode 100644 ui/test/src/components/common/timeRangePicker.spec.jsx create mode 100644 ui/test/src/components/layout.spec.jsx create mode 100644 ui/test/src/components/serviceGraph/graph.spec.js create mode 100644 ui/test/src/components/serviceGraph/serviceGraph.spec.jsx create mode 100644 ui/test/src/components/serviceGraph/util/edges.js create mode 100644 ui/test/src/components/serviceInsights/serviceInsights.spec.jsx create mode 100644 ui/test/src/components/serviceInsights/serviceInsightsGraph/dataLayout.spec.js create mode 100644 ui/test/src/components/serviceInsights/serviceInsightsGraph/dragGroup.spec.jsx create mode 100644 ui/test/src/components/serviceInsights/serviceInsightsGraph/label.spec.jsx create mode 100644 ui/test/src/components/serviceInsights/serviceInsightsGraph/labels.spec.jsx create mode 100644 ui/test/src/components/serviceInsights/serviceInsightsGraph/lines.spec.jsx create mode 100644 ui/test/src/components/serviceInsights/serviceInsightsGraph/nodes.spec.jsx create mode 100644 ui/test/src/components/serviceInsights/serviceInsightsGraph/serviceInsightsGraph.spec.jsx create mode 100644 ui/test/src/components/serviceInsights/serviceInsightsGraph/tooltip.spec.jsx create mode 100644 ui/test/src/components/serviceInsights/stores/serviceInsightsStore.spec.js create mode 100644 ui/test/src/components/serviceInsights/summary.spec.jsx create mode 100644 ui/test/src/components/servicePerf.spec.jsx create mode 100644 ui/test/src/components/traces/latencyCost.spec.jsx create mode 100644 ui/test/src/components/traces/rawSpan.spec.jsx create mode 100644 ui/test/src/components/traces/rawTrace.spec.jsx create mode 100644 ui/test/src/components/traces/spansView.spec.jsx create mode 100644 ui/test/src/components/traces/traces.spec.jsx create mode 100644 ui/test/src/components/trends/details/graphs/durationGraph.spec.jsx create mode 100644 ui/test/src/components/trends/trends.spec.jsx create mode 100644 ui/test/src/components/universalSearch/searchBar/stores/searchBarUiStateStore.spec.js create mode 100644 ui/test/src/components/universalSearch/searchBar/timeRangePicker.spec.jsx create mode 100644 ui/test/src/components/universalSearch/tabs/serviceInsights.spec.jsx create mode 100644 ui/test/src/components/universalSearch/tabs/tabStores/serviceInsightsTabStateStore.spec.js create mode 100644 ui/test/src/components/universalSearch/tabs/tabStores/tracesTabStateStore.spec.js create mode 100644 ui/test/src/components/universalSearch/universalSearch.spec.jsx create mode 100644 ui/test/src/main.spec.jsx create mode 100644 ui/test/src/stores/alertStore.spec.js create mode 100644 ui/test/src/stores/operationStore.spec.js create mode 100644 ui/test/src/stores/serviceGraphStore.spec.js create mode 100644 ui/test/src/stores/serviceStore.spec.js create mode 100644 ui/test/src/stores/tracesStore.spec.js create mode 100644 ui/test/src/stores/trendsStore.spec.js create mode 100644 ui/test/src/test_helper.js create mode 100644 ui/test/src/utils/timeWindow.spec.js create mode 100644 ui/universal/enums.js create mode 100644 ui/webpack.config.js diff --git a/collector/.mvn/wrapper/maven-wrapper.properties b/collector/.mvn/wrapper/maven-wrapper.properties new file mode 100755 index 000000000..56bb0164e --- /dev/null +++ b/collector/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1 @@ +distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.5.0/apache-maven-3.5.0-bin.zip \ No newline at end of file diff --git a/collector/LICENSE b/collector/LICENSE new file mode 100644 index 000000000..6c225e3b7 --- /dev/null +++ b/collector/LICENSE @@ -0,0 +1,202 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + + Copyright 2017 Expedia, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/collector/Makefile b/collector/Makefile new file mode 100644 index 000000000..1e6fe5074 --- /dev/null +++ b/collector/Makefile @@ -0,0 +1,32 @@ +.PHONY: all kinesis + +PWD := $(shell pwd) + +clean: + mvn clean + +build: clean + mvn package + +report-coverage: + docker run -it -v ~/.m2:/root/.m2 -w /src -v `pwd`:/src maven:3.5.0-jdk-8 /bin/sh -c 'mvn scoverage:report-only && mvn clean' + +all: clean kinesis http report-coverage + +kinesis: build_kinesis + cd kinesis && $(MAKE) integration_test + +build_kinesis: + mvn package -DfinalName=haystack-kinesis-span-collector -pl kinesis -am + +http: build_http + cd http && $(MAKE) integration_test + +build_http: + mvn package -DfinalName=haystack-http-span-collector -pl http -am + +# build all and release +release: clean build_kinesis build_http + cd kinesis && $(MAKE) release + cd http && $(MAKE) release + ./.travis/deploy.sh diff --git a/collector/README.md b/collector/README.md new file mode 100644 index 000000000..3189d540d --- /dev/null +++ b/collector/README.md @@ -0,0 +1,52 @@ +[![Build Status](https://travis-ci.org/ExpediaDotCom/haystack-collector.svg?branch=master)](https://travis-ci.org/ExpediaDotCom/haystack-collector) +[![License](https://img.shields.io/badge/license-Apache%20License%202.0-blue.svg)](https://github.com/ExpediaDotCom/haystack/blob/master/LICENSE) + +# haystack-collector +This haystack component collects spans from various sources and publish to kafka. As of today, we support two sources: + +1. Kinesis: Kinesis span collector reads proto serialized spans from a kinesis stream, validates it and write the data to configured kafka topic. +2. Http: Http span collector listens on port 8080 for proto or json serialized spans, validate them and write to configured kafka topic. For more detail read [this](./http/README.md) + +Spans are validated to ensure they dont't contain an empty service and operation name. The startTime and duration should be non-zero. + +## Building + +#### +Since this repo contains haystack-idl as the submodule, so use the following to clone the repo +* git clone --recursive git@github.com:ExpediaDotCom/haystack-collector.git . + +####Prerequisite: + +* Make sure you have Java 1.8 +* Make sure you have maven 3.3.9 or higher +* Make sure you have docker 1.13 or higher + + +Note : For mac users you can download docker for mac to set you up for the last two steps. + +####Build + +For a full build, including unit tests and integration tests, docker image build, you can run - +``` +make all +``` + +####Integration Test + +####Prerequisite: +1. Install docker using Docker Tools or native docker if on mac +2. Verify if docker-compose is installed by running following command else install it. +``` +docker-compose + +``` + +Run the build and integration tests for individual components with +``` +make kinesis + +or + +make http + +``` diff --git a/collector/checkstyles/scalastyle_config.xml b/collector/checkstyles/scalastyle_config.xml new file mode 100644 index 000000000..0b5ba9469 --- /dev/null +++ b/collector/checkstyles/scalastyle_config.xml @@ -0,0 +1,134 @@ + + Scalastyle standard configuration + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/collector/commons/pom.xml b/collector/commons/pom.xml new file mode 100644 index 000000000..8dbc357ac --- /dev/null +++ b/collector/commons/pom.xml @@ -0,0 +1,77 @@ + + + + + haystack-collector + com.expedia.www + 1.0-SNAPSHOT + + + 4.0.0 + haystack-collector-commons + 1.0-SNAPSHOT + jar + + + + + com.expedia.www + haystack-span-decorators + ${project.version} + + + + org.apache.kafka + kafka-clients + + + + org.apache.kafka + kafka_${scala.major.minor.version} + + + org.slf4j + slf4j-log4j12 + + + + + + com.codahale.metrics + metrics-core + + + + com.google.protobuf + protobuf-java-util + + + + + + + + org.scalatest + scalatest-maven-plugin + + com.expedia.www.haystack.collector.commons.unit + + + + + net.alchim31.maven + scala-maven-plugin + + + + org.scalastyle + scalastyle-maven-plugin + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + diff --git a/collector/commons/src/main/scala/com/expedia/www/haystack/collector/commons/MetricsSupport.scala b/collector/commons/src/main/scala/com/expedia/www/haystack/collector/commons/MetricsSupport.scala new file mode 100644 index 000000000..258f6347c --- /dev/null +++ b/collector/commons/src/main/scala/com/expedia/www/haystack/collector/commons/MetricsSupport.scala @@ -0,0 +1,28 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.collector.commons + +import com.codahale.metrics.MetricRegistry + +trait MetricsSupport { + val metricRegistry: MetricRegistry = MetricsRegistries.metricRegistry +} + +object MetricsRegistries { + val metricRegistry = new MetricRegistry() +} diff --git a/collector/commons/src/main/scala/com/expedia/www/haystack/collector/commons/ProtoSpanExtractor.scala b/collector/commons/src/main/scala/com/expedia/www/haystack/collector/commons/ProtoSpanExtractor.scala new file mode 100644 index 000000000..d3688db60 --- /dev/null +++ b/collector/commons/src/main/scala/com/expedia/www/haystack/collector/commons/ProtoSpanExtractor.scala @@ -0,0 +1,221 @@ +/* + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.collector.commons + +import java.nio.charset.Charset +import java.time.Instant +import java.time.temporal.ChronoUnit +import java.util.concurrent.ConcurrentHashMap + +import com.expedia.open.tracing.{Span, Tag} +import com.expedia.www.haystack.collector.commons.ProtoSpanExtractor._ +import com.expedia.www.haystack.collector.commons.config.{ExtractorConfiguration, Format} +import com.expedia.www.haystack.collector.commons.record.{KeyValueExtractor, KeyValuePair} +import com.expedia.www.haystack.span.decorators.SpanDecorator +import com.google.protobuf.util.JsonFormat +import org.slf4j.Logger + +import scala.collection.JavaConverters._ +import scala.util.{Failure, Success, Try} + +object ProtoSpanExtractor { + private val DaysInYear1970 = 365 + private val January_1_1971_00_00_00_GMT: Instant = Instant.EPOCH.plus(DaysInYear1970, ChronoUnit.DAYS) + // A common mistake clients often make is to pass in milliseconds instead of microseconds for start time. + // Insisting that all start times be > January 1 1971 GMT catches this error. + val SmallestAllowedStartTimeMicros: Long = January_1_1971_00_00_00_GMT.getEpochSecond * 1000000 + val ServiceNameIsRequired = "Service Name is required: span=[%s]" + val OperationNameIsRequired = "Operation Name is required: serviceName=[%s]" + val SpanIdIsRequired = "Span ID is required: serviceName=[%s] operationName=[%s]" + val TraceIdIsRequired = "Trace ID is required: serviceName=[%s] operationName=[%s]" + val StartTimeIsInvalid = "Start time [%d] is invalid: serviceName=[%s] operationName=[%s]" + val DurationIsInvalid = "Duration [%d] is invalid: serviceName=[%s] operationName=[%s]" + val SpanSizeLimitExceeded = "Span Size Limit Exceeded: serviceName=[%s] operationName=[%s] traceId=[%s] spanSize=[%d] probableTags=[%s]" + + val ServiceNameVsTtlAndOperationNames = new ConcurrentHashMap[String, TtlAndOperationNames] + val OperationNameCountExceededMeterName = "operation.name.count.exceeded" +} + +class ProtoSpanExtractor(extractorConfiguration: ExtractorConfiguration, + val LOGGER: Logger, spanDecorators: List[SpanDecorator]) + extends KeyValueExtractor with MetricsSupport { + + private val printer = JsonFormat.printer().omittingInsignificantWhitespace() + + private val invalidSpanMeter = metricRegistry.meter("invalid.span") + private val validSpanMeter = metricRegistry.meter("valid.span") + private val spanSizeLimitExceededMeter = metricRegistry.meter("sizeLimitExceeded.span") + + override def configure(): Unit = () + + def validateServiceName(span: Span): Try[Span] = { + validate(span, span.getServiceName, ServiceNameIsRequired, span.toString) + } + + def validateOperationName(span: Span): Try[Span] = { + validate(span, span.getOperationName, OperationNameIsRequired, span.getServiceName) + } + + def validateSpanId(span: Span): Try[Span] = { + validate(span, span.getSpanId, SpanIdIsRequired, span.getServiceName, span.getOperationName) + } + + def validateTraceId(span: Span): Try[Span] = { + validate(span, span.getTraceId, TraceIdIsRequired, span.getServiceName, span.getOperationName) + } + + def validateStartTime(span: Span): Try[Span] = { + validate(span, span.getStartTime, StartTimeIsInvalid, SmallestAllowedStartTimeMicros, span.getServiceName, span.getOperationName) + } + + def validateDuration(span: Span): Try[Span] = { + validate(span, span.getDuration, DurationIsInvalid, 0, span.getServiceName, span.getOperationName) + } + + def validateSpanSize(span: Span): Try[Span] = { + if (extractorConfiguration.spanValidation.spanMaxSize.enable + && !extractorConfiguration.spanValidation.spanMaxSize.skipServices.contains(span.getServiceName.toLowerCase)) { + val spanSize = span.toByteArray.length + val maxSizeLimit = extractorConfiguration.spanValidation.spanMaxSize.maxSizeLimit + validate(span, spanSize, SpanSizeLimitExceeded, maxSizeLimit) + } + else + Success(span) + } + + private def validate(span: Span, + valueToValidate: String, + msg: String, + serviceName: String): Try[Span] = { + if (Option(valueToValidate).getOrElse("").isEmpty) { + Failure(new IllegalArgumentException(msg.format(serviceName))) + } else { + Success(span) + } + } + + private def validate(span: Span, + valueToValidate: String, + msg: String, + serviceName: String, + operationName: String): Try[Span] = { + if (Option(valueToValidate).getOrElse("").isEmpty) { + Failure(new IllegalArgumentException(msg.format(serviceName, operationName))) + } else { + Success(span) + } + } + + private def validate(span: Span, + valueToValidate: Long, + msg: String, + smallestValidValue: Long, + serviceName: String, + operationName: String): Try[Span] = { + if (valueToValidate < smallestValidValue) { + Failure(new IllegalArgumentException(msg.format(valueToValidate, serviceName, operationName))) + } else { + Success(span) + } + } + + private def validate(span: Span, + valueToValidate: Int, + msg: String, + highestValidValue: Int): Try[Span] = { + + if (valueToValidate > highestValidValue) { + spanSizeLimitExceededMeter.mark() + LOGGER.debug(msg.format(span.getServiceName, span.getOperationName, span.getTraceId, valueToValidate, getProbableTagsExceedingSizeLimit(span))) + if (extractorConfiguration.spanValidation.spanMaxSize.logOnly) { + Success(span) + } else { + Success(truncateTags(span)) + } + } + else { + Success(span) + } + } + + private def getProbableTagsExceedingSizeLimit(span: Span): String = { + span.getTagsList.asScala + .filter(tag => tag.getVStrBytes.size > extractorConfiguration.spanValidation.spanMaxSize.maxSizeLimit) + .map(_.getKey) + .mkString(", ") + } + + private def truncateTags(span: Span): Span = { + val skippedTags = span.getTagsList.asScala + .filter(tag => extractorConfiguration.spanValidation.spanMaxSize.skipTags.contains(tag.getKey.toLowerCase)) + + val spanBuilder = span.toBuilder + spanBuilder.clearTags() + + skippedTags.foreach(spanBuilder.addTags) + + val truncateTagKey = extractorConfiguration.spanValidation.spanMaxSize.infoTagKey + val truncateTagValue = extractorConfiguration.spanValidation.spanMaxSize.infoTagValue + spanBuilder.addTags(Tag.newBuilder().setKey(truncateTagKey).setVStr(truncateTagValue)) + + spanBuilder.build() + } + + + override def extractKeyValuePairs(recordBytes: Array[Byte]): List[KeyValuePair[Array[Byte], Array[Byte]]] = { + Try(Span.parseFrom(recordBytes)) + .flatMap(span => validateSpanSize(span)) + .flatMap(span => validateServiceName(span)) + .flatMap(span => validateOperationName(span)) + .flatMap(span => validateSpanId(span)) + .flatMap(span => validateTraceId(span)) + .flatMap(span => validateStartTime(span)) + .flatMap(span => validateDuration(span)) + match { + case Success(span) => + validSpanMeter.mark() + + val updatedSpan = decorateSpan(span) + val kvPair = extractorConfiguration.outputFormat match { + case Format.JSON => KeyValuePair(updatedSpan.getTraceId.getBytes, printer.print(span).getBytes(Charset.forName("UTF-8"))) + case Format.PROTO => KeyValuePair(updatedSpan.getTraceId.getBytes, updatedSpan.toByteArray) + } + List(kvPair) + + case Failure(ex) => + invalidSpanMeter.mark() + ex match { + case ex: IllegalArgumentException => LOGGER.error(ex.getMessage) + case _: java.lang.Exception => LOGGER.error("Fail to deserialize the span proto bytes with exception", ex) + } + Nil + } + } + + private def decorateSpan(span: Span): Span = { + if (spanDecorators.isEmpty) { + return span + } + + var spanBuilder = span.toBuilder + spanDecorators.foreach(decorator => { + spanBuilder = decorator.decorate(spanBuilder) + }) + spanBuilder.build() + } +} diff --git a/collector/commons/src/main/scala/com/expedia/www/haystack/collector/commons/SpanDecoratorFactory.scala b/collector/commons/src/main/scala/com/expedia/www/haystack/collector/commons/SpanDecoratorFactory.scala new file mode 100644 index 000000000..e2bda4b75 --- /dev/null +++ b/collector/commons/src/main/scala/com/expedia/www/haystack/collector/commons/SpanDecoratorFactory.scala @@ -0,0 +1,25 @@ +package com.expedia.www.haystack.collector.commons + +import com.expedia.www.haystack.span.decorators.plugin.config.Plugin +import com.expedia.www.haystack.span.decorators.plugin.loader.SpanDecoratorPluginLoader +import com.expedia.www.haystack.span.decorators.{AdditionalTagsSpanDecorator, SpanDecorator} +import com.typesafe.config.ConfigFactory +import org.slf4j.Logger + +import scala.collection.JavaConverters._ + +object SpanDecoratorFactory { + def get(pluginConfig: Plugin, additionalTagsConfig: Map[String, String], LOGGER: Logger): List[SpanDecorator] = { + var tempList = List[SpanDecorator]() + if (pluginConfig != null) { + val externalSpanDecorators: List[SpanDecorator] = SpanDecoratorPluginLoader.getInstance(LOGGER, pluginConfig).getSpanDecorators().asScala.toList + if (externalSpanDecorators != null) { + tempList = tempList ++: externalSpanDecorators + } + } + + val additionalTagsSpanDecorator = new AdditionalTagsSpanDecorator() + additionalTagsSpanDecorator.init(ConfigFactory.parseMap(additionalTagsConfig.asJava)) + tempList.::(additionalTagsSpanDecorator) + } +} diff --git a/collector/commons/src/main/scala/com/expedia/www/haystack/collector/commons/TtlAndOperationNames.java b/collector/commons/src/main/scala/com/expedia/www/haystack/collector/commons/TtlAndOperationNames.java new file mode 100644 index 000000000..5bf8b751f --- /dev/null +++ b/collector/commons/src/main/scala/com/expedia/www/haystack/collector/commons/TtlAndOperationNames.java @@ -0,0 +1,44 @@ +/* + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.collector.commons; + +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; + +/** + * This class is used by ProtoSpanExtractor to keep track of the number of operation names for a particular service. + * It is written in Java because Java's Atomic classes are the preferred way of handling concurrent maps and sets + * in Scala, and the accesses to the objects that count operation names come from multiple threads. + */ +public class TtlAndOperationNames { + public final Set operationNames = ConcurrentHashMap.newKeySet(); + private final AtomicLong ttlMillis; + + TtlAndOperationNames(long ttlMillis) { + this.ttlMillis = new AtomicLong(ttlMillis); + } + + public long getTtlMillis() { + return ttlMillis.get(); + } + + public void setTtlMillis(long ttlMillis) { + this.ttlMillis.set(ttlMillis); + } +} diff --git a/collector/commons/src/main/scala/com/expedia/www/haystack/collector/commons/config/ConfigurationLoader.scala b/collector/commons/src/main/scala/com/expedia/www/haystack/collector/commons/config/ConfigurationLoader.scala new file mode 100644 index 000000000..7598b61b1 --- /dev/null +++ b/collector/commons/src/main/scala/com/expedia/www/haystack/collector/commons/config/ConfigurationLoader.scala @@ -0,0 +1,217 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.collector.commons.config + +import java.io.File +import java.util +import java.util.Properties + +import com.expedia.www.haystack.span.decorators.plugin.config.{Plugin, PluginConfiguration} +import com.typesafe.config._ +import org.apache.kafka.clients.producer.ProducerConfig +import org.apache.kafka.clients.producer.ProducerConfig.{KEY_SERIALIZER_CLASS_CONFIG, VALUE_SERIALIZER_CLASS_CONFIG} +import org.apache.kafka.common.serialization.ByteArraySerializer +import org.slf4j.LoggerFactory + +import scala.collection.JavaConversions._ +import scala.collection.JavaConverters._ + +object ConfigurationLoader { + + private val LOGGER = LoggerFactory.getLogger(ConfigurationLoader.getClass) + + private[haystack] val ENV_NAME_PREFIX = "HAYSTACK_PROP_" + + /** + * Load and return the configuration + * if overrides_config_path env variable exists, then we load that config file and use base conf as fallback, + * else we load the config from env variables(prefixed with haystack) and use base conf as fallback + * + * @param resourceName name of the resource file to be loaded. Default value is `config/base.conf` + * @param envNamePrefix env variable prefix to override config values. Default is `HAYSTACK_PROP_` + * @return an instance of com.typesafe.Config + */ + def loadConfigFileWithEnvOverrides(resourceName: String = "config/base.conf", + envNamePrefix: String = ENV_NAME_PREFIX): Config = { + + require(resourceName != null && resourceName.length > 0, "resourceName is required") + require(envNamePrefix != null && envNamePrefix.length > 0, "envNamePrefix is required") + + val baseConfig = ConfigFactory.load(resourceName) + + val keysWithArrayValues = baseConfig.entrySet() + .asScala + .filter(_.getValue.valueType() == ConfigValueType.LIST) + .map(_.getKey) + .toSet + + val config = sys.env.get("HAYSTACK_OVERRIDES_CONFIG_PATH") match { + case Some(overrideConfigPath) => + val overrideConfig = ConfigFactory.parseFile(new File(overrideConfigPath)) + ConfigFactory + .parseMap(parsePropertiesFromMap(sys.env, keysWithArrayValues, envNamePrefix).asJava) + .withFallback(overrideConfig) + .withFallback(baseConfig) + .resolve() + case _ => ConfigFactory + .parseMap(parsePropertiesFromMap(sys.env, keysWithArrayValues, envNamePrefix).asJava) + .withFallback(baseConfig) + .resolve() + } + + // In key-value pairs that contain 'password' in the key, replace the value with asterisks + LOGGER.info(config.root() + .render(ConfigRenderOptions.defaults().setOriginComments(false)) + .replaceAll("(?i)(\\\".*password\\\"\\s*:\\s*)\\\".+\\\"", "$1********")) + + config + } + + /** + * @return new config object with haystack specific environment variables + */ + private[haystack] def parsePropertiesFromMap(envVars: Map[String, String], + keysWithArrayValues: Set[String], + envNamePrefix: String): Map[String, Object] = { + envVars.filter { + case (envName, _) => envName.startsWith(envNamePrefix) + } map { + case (envName, envValue) => + val key = transformEnvVarName(envName, envNamePrefix) + if (keysWithArrayValues.contains(key)) (key, transformEnvVarArrayValue(envValue)) else (key, envValue) + } + } + + /** + * converts the env variable to HOCON format + * for e.g. env variable HAYSTACK_KAFKA_STREAMS_NUM_STREAM_THREADS gets converted to kafka.streams.num.stream.threads + * + * @param env environment variable name + * @return variable name that complies with hocon key + */ + private def transformEnvVarName(env: String, envNamePrefix: String): String = { + env.replaceFirst(envNamePrefix, "").toLowerCase.replace("_", ".") + } + + /** + * converts the env variable value to iterable object if it starts and ends with '[' and ']' respectively. + * + * @param env environment variable value + * @return string or iterable object + */ + private def transformEnvVarArrayValue(env: String): java.util.List[String] = { + if (env.startsWith("[") && env.endsWith("]")) { + import scala.collection.JavaConverters._ + env.substring(1, env.length - 1).split(',').filter(str => (str != null) && str.nonEmpty).toList.asJava + } else { + throw new RuntimeException("config key is of array type, so it should start and end with '[', ']' respectively") + } + } + + def kafkaProducerConfig(config: Config): KafkaProduceConfiguration = { + val props = new Properties() + + val kafka = config.getConfig("kafka.producer") + + kafka.getConfig("props").entrySet() foreach { + kv => { + props.setProperty(kv.getKey, kv.getValue.unwrapped().toString) + } + } + + props.put(KEY_SERIALIZER_CLASS_CONFIG, classOf[ByteArraySerializer].getCanonicalName) + props.put(VALUE_SERIALIZER_CLASS_CONFIG, classOf[ByteArraySerializer].getCanonicalName) + + val produceTopic = kafka.getString("topic") + + // verify if at least bootstrap server config is set + require(props.getProperty(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG).nonEmpty) + require(produceTopic.nonEmpty) + + KafkaProduceConfiguration(produceTopic, props) + } + + def extractorConfiguration(config: Config): ExtractorConfiguration = { + val extractor = config.getConfig("extractor") + val spanValidation = extractor.getConfig("spans.validation") + val maxSizeValidationConfig = spanValidation.getConfig("max.size") + ExtractorConfiguration( + outputFormat = if (extractor.hasPath("output.format")) Format.withName(extractor.getString("output.format")) else Format.PROTO, + spanValidation = SpanValidation(SpanMaxSize( + maxSizeValidationConfig.getBoolean("enable"), + maxSizeValidationConfig.getBoolean("log.only"), + maxSizeValidationConfig.getInt("max.size.limit"), + maxSizeValidationConfig.getString("message.tag.key"), + maxSizeValidationConfig.getString("message.tag.value"), + maxSizeValidationConfig.getStringList("skip.tags").map(_.toLowerCase), + maxSizeValidationConfig.getStringList("skip.services").map(_.toLowerCase)) + )) + } + + def externalKafkaConfiguration(config: Config): List[ExternalKafkaConfiguration] = { + if (!config.hasPath("external.kafka")) { + return List[ExternalKafkaConfiguration]() + } + + val kafkaProducerConfig: ConfigObject = config.getObject("external.kafka") + kafkaProducerConfig.unwrapped().map(c => { + val props = new Properties() + val cfg = ConfigFactory.parseMap(c._2.asInstanceOf[util.HashMap[String, Object]]) + val topic = cfg.getString("config.topic") + val tags = cfg.getConfig("tags").entrySet().foldRight(Map[String, String]())((t, tMap) => { + tMap + (t.getKey -> t.getValue.unwrapped().toString) + }) + val temp = cfg.getConfig("config.props").entrySet() foreach { + kv => { + props.setProperty(kv.getKey, kv.getValue.unwrapped().toString) + } + } + + props.put(KEY_SERIALIZER_CLASS_CONFIG, classOf[ByteArraySerializer].getCanonicalName) + props.put(VALUE_SERIALIZER_CLASS_CONFIG, classOf[ByteArraySerializer].getCanonicalName) + + ExternalKafkaConfiguration(tags, KafkaProduceConfiguration(topic, props)) + }).toList + } + + def additionalTagsConfiguration(config: Config): Map[String, String] = { + if (!config.hasPath("additionaltags")) { + return Map[String, String]() + } + val additionalTagsConfig = config.getConfig("additionaltags") + val additionalTags = additionalTagsConfig.entrySet().foldRight(Map[String, String]())((t, tMap) => { + tMap + (t.getKey -> t.getValue.unwrapped().toString) + }) + additionalTags + } + + def pluginConfigurations(config: Config): Plugin = { + if (!config.hasPath("plugins")) { + return null + } + val directory = config.getString("plugins.directory") + val pluginConfigurationsList = config.getObject("plugins").unwrapped().filter(c => !"directory".equals(c._1)).map(c => { + val pluginConfig = ConfigFactory.parseMap(c._2.asInstanceOf[util.HashMap[String, Object]]) + new PluginConfiguration( + pluginConfig.getString("name"), + pluginConfig.getConfig("config") + ) + }).toList + new Plugin(directory, pluginConfigurationsList) + } +} diff --git a/collector/commons/src/main/scala/com/expedia/www/haystack/collector/commons/config/ExtractorConfiguration.scala b/collector/commons/src/main/scala/com/expedia/www/haystack/collector/commons/config/ExtractorConfiguration.scala new file mode 100644 index 000000000..e9db322ce --- /dev/null +++ b/collector/commons/src/main/scala/com/expedia/www/haystack/collector/commons/config/ExtractorConfiguration.scala @@ -0,0 +1,41 @@ +/* + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.collector.commons.config + +import com.expedia.www.haystack.collector.commons.config.Format.Format + + +object Format extends Enumeration { + type Format = Value + val JSON = Value("json") + val PROTO = Value("proto") +} + +case class SpanValidation(spanMaxSize: SpanMaxSize) + +case class SpanMaxSize(enable: Boolean, + logOnly: Boolean, + maxSizeLimit: Int, + infoTagKey: String, + infoTagValue: String, + skipTags: Seq[String], + skipServices: Seq[String]) + +case class ExtractorConfiguration(outputFormat: Format, + spanValidation: SpanValidation) + diff --git a/collector/commons/src/main/scala/com/expedia/www/haystack/collector/commons/config/KafkaProduceConfiguration.scala b/collector/commons/src/main/scala/com/expedia/www/haystack/collector/commons/config/KafkaProduceConfiguration.scala new file mode 100644 index 000000000..444b9d85f --- /dev/null +++ b/collector/commons/src/main/scala/com/expedia/www/haystack/collector/commons/config/KafkaProduceConfiguration.scala @@ -0,0 +1,24 @@ +/* + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.collector.commons.config + +import java.util.Properties + +case class KafkaProduceConfiguration(topic: String, props: Properties) + +case class ExternalKafkaConfiguration(tags: Map[String, String], kafkaProduceConfiguration: KafkaProduceConfiguration) \ No newline at end of file diff --git a/collector/commons/src/main/scala/com/expedia/www/haystack/collector/commons/health/HealthController.scala b/collector/commons/src/main/scala/com/expedia/www/haystack/collector/commons/health/HealthController.scala new file mode 100644 index 000000000..ee194adb3 --- /dev/null +++ b/collector/commons/src/main/scala/com/expedia/www/haystack/collector/commons/health/HealthController.scala @@ -0,0 +1,72 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.collector.commons.health + +import java.util.concurrent.atomic.AtomicReference + +import com.expedia.www.haystack.collector.commons.health.HealthStatus.HealthStatus +import org.slf4j.LoggerFactory + +import scala.collection.mutable + +/** + * provides the health check of app + */ +object HealthController { + + private val LOGGER = LoggerFactory.getLogger(HealthController.getClass) + + // sets the initial health state as 'not set' + private val status = new AtomicReference[HealthStatus](HealthStatus.NOT_SET) + + private var listeners = mutable.ListBuffer[HealthStatusChangeListener]() + + /** + * set the app status as health + */ + def setHealthy(): Unit = { + LOGGER.info("Setting the app status as 'HEALTHY'") + if(status.getAndSet(HealthStatus.HEALTHY) != HealthStatus.HEALTHY) notifyChange(HealthStatus.HEALTHY) + } + + /** + * set the app status as unhealthy + */ + def setUnhealthy(): Unit = { + LOGGER.error("Setting the app status as 'UNHEALTHY'") + if(status.getAndSet(HealthStatus.UNHEALTHY) != HealthStatus.UNHEALTHY) notifyChange(HealthStatus.UNHEALTHY) + } + + /** + * @return true if app is healthy else false + */ + def isHealthy: Boolean = status.get() == HealthStatus.HEALTHY + + /** + * add health change listener that will be called on any change in the health status + * @param l listener + */ + def addListener(l: HealthStatusChangeListener): Unit = listeners += l + + private def notifyChange(status: HealthStatus): Unit = { + listeners foreach { + l => + l.onChange(status) + } + } +} diff --git a/collector/commons/src/main/scala/com/expedia/www/haystack/collector/commons/health/HealthStatus.scala b/collector/commons/src/main/scala/com/expedia/www/haystack/collector/commons/health/HealthStatus.scala new file mode 100644 index 000000000..c58e0c57c --- /dev/null +++ b/collector/commons/src/main/scala/com/expedia/www/haystack/collector/commons/health/HealthStatus.scala @@ -0,0 +1,23 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.collector.commons.health + +object HealthStatus extends Enumeration { + type HealthStatus = Value + val HEALTHY, UNHEALTHY, NOT_SET = Value +} diff --git a/collector/commons/src/main/scala/com/expedia/www/haystack/collector/commons/health/HealthStatusChangeListener.scala b/collector/commons/src/main/scala/com/expedia/www/haystack/collector/commons/health/HealthStatusChangeListener.scala new file mode 100644 index 000000000..4bad6ac32 --- /dev/null +++ b/collector/commons/src/main/scala/com/expedia/www/haystack/collector/commons/health/HealthStatusChangeListener.scala @@ -0,0 +1,33 @@ +/* + * + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.collector.commons.health + +import com.expedia.www.haystack.collector.commons.health.HealthStatus.HealthStatus + +/** + * health status listener + */ +trait HealthStatusChangeListener { + + /** + * called whenever there there is a state change in health + * @param status current health status + */ + def onChange(status: HealthStatus): Unit +} diff --git a/collector/commons/src/main/scala/com/expedia/www/haystack/collector/commons/health/UpdateHealthStatusFile.scala b/collector/commons/src/main/scala/com/expedia/www/haystack/collector/commons/health/UpdateHealthStatusFile.scala new file mode 100644 index 000000000..296c34aaf --- /dev/null +++ b/collector/commons/src/main/scala/com/expedia/www/haystack/collector/commons/health/UpdateHealthStatusFile.scala @@ -0,0 +1,41 @@ +/* + * + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.collector.commons.health + +import java.nio.charset.StandardCharsets +import java.nio.file.{Files, Paths} + +import com.expedia.www.haystack.collector.commons.health.HealthStatus.HealthStatus + +/** + * writes the current health status to a status file. This can be used to provide the health to external system + * like container orchestration frameworks + * @param statusFilePath: file path where health status will be recorded. + */ +class UpdateHealthStatusFile(statusFilePath: String) extends HealthStatusChangeListener { + + /** + * call on the any change in health status of app + * @param status: current health status + */ + override def onChange(status: HealthStatus): Unit = { + val isHealthy = if (status == HealthStatus.HEALTHY) "true" else "false" + Files.write(Paths.get(statusFilePath), isHealthy.getBytes(StandardCharsets.UTF_8)) + } +} diff --git a/collector/commons/src/main/scala/com/expedia/www/haystack/collector/commons/logger/LoggerUtils.scala b/collector/commons/src/main/scala/com/expedia/www/haystack/collector/commons/logger/LoggerUtils.scala new file mode 100644 index 000000000..6ad2d4dc4 --- /dev/null +++ b/collector/commons/src/main/scala/com/expedia/www/haystack/collector/commons/logger/LoggerUtils.scala @@ -0,0 +1,50 @@ +/* + * + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.collector.commons.logger + +import org.slf4j.{ILoggerFactory, LoggerFactory} + +object LoggerUtils { + + /** + * shutdown the logger using reflection. + * for logback, it calls stop() method on loggerContext + * for log4j, it calls close() method on log4j context + */ + def shutdownLogger(): Unit = { + val factory = LoggerFactory.getILoggerFactory + shutdownLoggerWithFactory(factory) + } + + // just visible for testing + def shutdownLoggerWithFactory(factory: ILoggerFactory): Unit = { + val clazz = factory.getClass + try { + clazz.getMethod("stop").invoke(factory) // logback + } catch { + case _: ReflectiveOperationException => + try { + clazz.getMethod("close").invoke(factory) // log4j + } catch { + case _: Exception => + } + case _: Exception => + } + } +} diff --git a/collector/commons/src/main/scala/com/expedia/www/haystack/collector/commons/record/KeyValueExtractor.scala b/collector/commons/src/main/scala/com/expedia/www/haystack/collector/commons/record/KeyValueExtractor.scala new file mode 100644 index 000000000..c869c7a20 --- /dev/null +++ b/collector/commons/src/main/scala/com/expedia/www/haystack/collector/commons/record/KeyValueExtractor.scala @@ -0,0 +1,25 @@ +/* + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.collector.commons.record + +case class KeyValuePair[K, V](key: K, value: V) + +trait KeyValueExtractor { + def configure(): Unit + def extractKeyValuePairs(recordBytes: Array[Byte]): List[KeyValuePair[Array[Byte], Array[Byte]]] +} diff --git a/collector/commons/src/main/scala/com/expedia/www/haystack/collector/commons/sink/RecordSink.scala b/collector/commons/src/main/scala/com/expedia/www/haystack/collector/commons/sink/RecordSink.scala new file mode 100644 index 000000000..b57eff92d --- /dev/null +++ b/collector/commons/src/main/scala/com/expedia/www/haystack/collector/commons/sink/RecordSink.scala @@ -0,0 +1,27 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.collector.commons.sink + +import java.io.Closeable + +import com.expedia.www.haystack.collector.commons.record.KeyValuePair + +trait RecordSink extends Closeable { + def toAsync(kvPair: KeyValuePair[Array[Byte], Array[Byte]], + callback: (KeyValuePair[Array[Byte], Array[Byte]], Exception) => Unit = null): Unit +} diff --git a/collector/commons/src/main/scala/com/expedia/www/haystack/collector/commons/sink/kafka/KafkaRecordSink.scala b/collector/commons/src/main/scala/com/expedia/www/haystack/collector/commons/sink/kafka/KafkaRecordSink.scala new file mode 100644 index 000000000..cd91d796a --- /dev/null +++ b/collector/commons/src/main/scala/com/expedia/www/haystack/collector/commons/sink/kafka/KafkaRecordSink.scala @@ -0,0 +1,92 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.collector.commons.sink.kafka + +import com.expedia.open.tracing.{Span, Tag} +import com.expedia.www.haystack.collector.commons.MetricsSupport +import com.expedia.www.haystack.collector.commons.config.{ExternalKafkaConfiguration, KafkaProduceConfiguration} +import com.expedia.www.haystack.collector.commons.record.KeyValuePair +import com.expedia.www.haystack.collector.commons.sink.RecordSink +import org.apache.kafka.clients.producer.{ProducerRecord, _} +import org.slf4j.LoggerFactory + +import scala.collection.JavaConversions._ +import scala.collection.JavaConverters._ + +class KafkaRecordSink(config: KafkaProduceConfiguration, + additionalKafkaProducerConfigs: List[ExternalKafkaConfiguration]) extends RecordSink with MetricsSupport { + + private val LOGGER = LoggerFactory.getLogger(classOf[KafkaRecordSink]) + + private val defaultProducer: KafkaProducer[Array[Byte], Array[Byte]] = new KafkaProducer[Array[Byte], Array[Byte]](config.props) + private val additionalProducers: List[KafkaProducers] = additionalKafkaProducerConfigs + .map(cfg => { + KafkaProducers(cfg.tags, cfg.kafkaProduceConfiguration.topic, new KafkaProducer[Array[Byte], Array[Byte]](cfg.kafkaProduceConfiguration.props)) + }) + + override def toAsync(kvPair: KeyValuePair[Array[Byte], Array[Byte]], + callback: (KeyValuePair[Array[Byte], Array[Byte]], Exception) => Unit = null): Unit = { + val kafkaMessage = new ProducerRecord(config.topic, kvPair.key, kvPair.value) + + defaultProducer.send(kafkaMessage, new Callback { + override def onCompletion(recordMetadata: RecordMetadata, e: Exception): Unit = { + if (e != null) { + LOGGER.error(s"Fail to produce the message to kafka for topic=${config.topic} with reason", e) + } + if(callback != null) callback(kvPair, e) + } + }) + + getMatchingProducers(additionalProducers, Span.parseFrom(kvPair.value)).foreach(p => { + val tempKafkaMessage = new ProducerRecord(p.topic, kvPair.key, kvPair.value) + p.producer.send(tempKafkaMessage, new Callback { + override def onCompletion(recordMetadata: RecordMetadata, e: Exception): Unit = { + if (e != null) { + LOGGER.error(s"Fail to produce the message to kafka for topic=${p.topic} with reason", e) + } + if(callback != null) callback(kvPair, e) + } + }) + }) + } + + private def getMatchingProducers(producers: List[KafkaProducers], span: Span): List[KafkaProducers] = { + val tagList: List[Tag] = span.getTagsList.asScala.toList + producers.filter(producer => producer.isMatched(tagList)) + } + + override def close(): Unit = { + if(defaultProducer != null) { + defaultProducer.flush() + defaultProducer.close() + } + additionalProducers.foreach(p => p.close()) + } + + case class KafkaProducers(tags: Map[String, String], topic: String, producer: KafkaProducer[Array[Byte], Array[Byte]]) { + def isMatched(spanTags: List[Tag]): Boolean = { + val filteredTags = spanTags.filter(t => t.getVStr.equals(tags.getOrElse(t.getKey, null))) + filteredTags.size.equals(tags.size) + } + + def close(): Unit = { + producer.flush() + producer.close() + } + } +} diff --git a/collector/commons/src/test/resources/logback-test.xml b/collector/commons/src/test/resources/logback-test.xml new file mode 100644 index 000000000..adfa02c68 --- /dev/null +++ b/collector/commons/src/test/resources/logback-test.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/collector/commons/src/test/scala/com/expedia/www/haystack/collector/commons/unit/HealthControllerSpec.scala b/collector/commons/src/test/scala/com/expedia/www/haystack/collector/commons/unit/HealthControllerSpec.scala new file mode 100644 index 000000000..022cdc0ac --- /dev/null +++ b/collector/commons/src/test/scala/com/expedia/www/haystack/collector/commons/unit/HealthControllerSpec.scala @@ -0,0 +1,52 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.collector.commons.unit + +import com.expedia.www.haystack.collector.commons.health.{HealthController, UpdateHealthStatusFile} +import org.scalatest.{FunSpec, Matchers} + +class HealthControllerSpec extends FunSpec with Matchers { + + private val statusFile = "/tmp/app-health.status" + + describe("file based health checker") { + it("should set the state as healthy if previous state is not set or unhealthy") { + val healthChecker = HealthController + healthChecker.addListener(new UpdateHealthStatusFile(statusFile)) + healthChecker.isHealthy shouldBe false + healthChecker.setHealthy() + healthChecker.isHealthy shouldBe true + readStatusLine shouldEqual "true" + } + + it("should set the state as unhealthy if previous state is healthy") { + val healthChecker = HealthController + healthChecker.addListener(new UpdateHealthStatusFile(statusFile)) + + healthChecker.setHealthy() + healthChecker.isHealthy shouldBe true + readStatusLine shouldEqual "true" + + healthChecker.setUnhealthy() + healthChecker.isHealthy shouldBe false + readStatusLine shouldEqual "false" + } + } + + private def readStatusLine = scala.io.Source.fromFile(statusFile).getLines().toList.head +} \ No newline at end of file diff --git a/collector/commons/src/test/scala/com/expedia/www/haystack/collector/commons/unit/KeyExtractorSpec.scala b/collector/commons/src/test/scala/com/expedia/www/haystack/collector/commons/unit/KeyExtractorSpec.scala new file mode 100644 index 000000000..ab8419956 --- /dev/null +++ b/collector/commons/src/test/scala/com/expedia/www/haystack/collector/commons/unit/KeyExtractorSpec.scala @@ -0,0 +1,80 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.collector.commons.unit + +import java.nio.charset.Charset + +import com.expedia.open.tracing.Span +import com.expedia.www.haystack.collector.commons.config.{ExtractorConfiguration, Format, SpanMaxSize, SpanValidation} +import com.expedia.www.haystack.collector.commons.{MetricsSupport, ProtoSpanExtractor} +import com.google.protobuf.util.JsonFormat +import org.scalatest.{FunSpec, Matchers} +import org.slf4j.LoggerFactory + +class KeyExtractorSpec extends FunSpec with Matchers with MetricsSupport { + private val StartTimeMicros = System.currentTimeMillis() * 1000 + private val DurationMicros = 42 + + describe("TransactionId Key Extractor with proto output type") { + it("should read the proto span object and set the right partition key and set value as the proto byte stream") { + val spanMap = Map( + "trace-id-1" -> createSpan("trace-id-1", "spanId_1", "service_1", "operation", StartTimeMicros, DurationMicros), + "trace-id-2" -> createSpan("trace-id-2", "spanId_2", "service_2", "operation", StartTimeMicros, DurationMicros)) + + val spanValidationConfig = SpanValidation(SpanMaxSize(enable = false, logOnly = false, 5000, "", "", Seq(), Seq())) + + spanMap.foreach(sp => { + val kvPairs = new ProtoSpanExtractor(ExtractorConfiguration(Format.PROTO, spanValidationConfig), LoggerFactory.getLogger(classOf[ProtoSpanExtractor]), List()).extractKeyValuePairs(sp._2.toByteArray) + kvPairs.size shouldBe 1 + + kvPairs.head.key shouldBe sp._1.getBytes + kvPairs.head.value shouldBe sp._2.toByteArray + }) + } + } + + describe("TransactionId Key Extractor with json output type") { + it("should read the proto span object and set the right partition key and set value as the json byte stream") { + val spanMap = Map( + "trace-id-1" -> createSpan("trace-id-1", "spanId_1", "service_1", "operation", StartTimeMicros, 1), + "trace-id-2" -> createSpan("trace-id-2", "spanId_2", "service_2", "operation", StartTimeMicros, 1)) + + val spanValidationConfig = SpanValidation(SpanMaxSize(enable = false, logOnly = false, 5000, "", "", Seq(), Seq())) + + spanMap.foreach(sp => { + val kvPairs = new ProtoSpanExtractor(ExtractorConfiguration(Format.JSON, spanValidationConfig), LoggerFactory.getLogger(classOf[ProtoSpanExtractor]), List()).extractKeyValuePairs(sp._2.toByteArray) + kvPairs.size shouldBe 1 + + kvPairs.head.key shouldBe sp._1.getBytes + kvPairs.head.value shouldBe JsonFormat.printer().omittingInsignificantWhitespace().print(sp._2).getBytes(Charset.forName("UTF-8")) + }) + } + } + + private def createSpan(traceId: String, spanId: String, serviceName: String, operationName: String, + startTime: Long, duration: Long) = { + Span.newBuilder() + .setServiceName(serviceName) + .setTraceId(traceId) + .setSpanId(spanId) + .setOperationName(operationName) + .setStartTime(startTime) + .setDuration(duration) + .build() + } +} diff --git a/collector/commons/src/test/scala/com/expedia/www/haystack/collector/commons/unit/LoggerUtilsSpec.scala b/collector/commons/src/test/scala/com/expedia/www/haystack/collector/commons/unit/LoggerUtilsSpec.scala new file mode 100644 index 000000000..081d2a517 --- /dev/null +++ b/collector/commons/src/test/scala/com/expedia/www/haystack/collector/commons/unit/LoggerUtilsSpec.scala @@ -0,0 +1,73 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.collector.commons.unit + +import com.expedia.www.haystack.collector.commons.logger.LoggerUtils +import org.scalatest.easymock.EasyMockSugar +import org.scalatest.{FunSpec, Matchers} +import org.slf4j.{ILoggerFactory, Logger} + +class LoggerUtilsSpec extends FunSpec with Matchers with EasyMockSugar { + + describe("Logger Utils") { + it("should close the logger if it has stop method for e.g. logback") { + val logger = mock[Logger] + var isStopped = false + + val loggerFactory = new ILoggerFactory { + override def getLogger(s: String): Logger = logger + def stop(): Unit = isStopped = true + } + + whenExecuting(logger) { + LoggerUtils.shutdownLoggerWithFactory(loggerFactory) + isStopped shouldBe true + } + } + + it("should close the logger if it has close method for e.g. log4j") { + val logger = mock[Logger] + var isStopped = false + + val loggerFactory = new ILoggerFactory { + override def getLogger(s: String): Logger = logger + def close(): Unit = isStopped = true + } + + whenExecuting(logger) { + LoggerUtils.shutdownLoggerWithFactory(loggerFactory) + isStopped shouldBe true + } + } + + it("should not able to close the logger if it has neither stop/close method") { + val logger = mock[Logger] + var isStopped = false + + val loggerFactory = new ILoggerFactory { + override def getLogger(s: String): Logger = logger + def shutdown(): Unit = isStopped = true + } + + whenExecuting(logger) { + LoggerUtils.shutdownLoggerWithFactory(loggerFactory) + isStopped shouldBe false + } + } + } +} \ No newline at end of file diff --git a/collector/commons/src/test/scala/com/expedia/www/haystack/collector/commons/unit/ProtoSpanExtractorSpec.scala b/collector/commons/src/test/scala/com/expedia/www/haystack/collector/commons/unit/ProtoSpanExtractorSpec.scala new file mode 100644 index 000000000..729d89752 --- /dev/null +++ b/collector/commons/src/test/scala/com/expedia/www/haystack/collector/commons/unit/ProtoSpanExtractorSpec.scala @@ -0,0 +1,145 @@ +package com.expedia.www.haystack.collector.commons.unit + +import com.expedia.open.tracing.{Span, Tag} +import com.expedia.www.haystack.collector.commons.ProtoSpanExtractor +import com.expedia.www.haystack.collector.commons.ProtoSpanExtractor._ +import com.expedia.www.haystack.collector.commons.config.{ExtractorConfiguration, Format, SpanMaxSize, SpanValidation} +import org.mockito.Mockito +import org.mockito.Mockito.verify +import org.scalatest.mockito.MockitoSugar +import org.scalatest.{FunSpec, Matchers} +import org.slf4j.Logger + +import scala.collection.JavaConverters._ +import scala.collection.immutable.ListMap +import scala.collection.mutable.ArrayBuffer + +class ProtoSpanExtractorSpec extends FunSpec with Matchers with MockitoSugar { + + private val EmptyString = "" + private val NullString = null + private val SpanId = "span ID" + private val TraceId = "trace ID" + private val ServiceName1 = "service name 1" + private val ServiceName2 = "service name 2" + private val OperationName1 = "operation name 1" + private val OperationName2 = "operation name 2" + private val StartTime = System.currentTimeMillis() * 1000 + private val Duration = 42 + private val Negative = -42 + private val SampleErrorTag = Tag.newBuilder().setKey("error").setVBool(true).build() + private val SpanSizeLimit = 800 + private val SkipTagTruncationServiceName = "skip_tag_truncation_service" + + describe("Protobuf Span Extractor") { + val mockLogger = mock[Logger] + + val spanSizeValidationConfig = SpanValidation(SpanMaxSize(enable = true, logOnly = false, SpanSizeLimit, "X-HAYSTACK-SPAN-INFO", "Tags Truncated", Seq("error"), Seq(SkipTagTruncationServiceName))) + val protoSpanExtractor = new ProtoSpanExtractor(ExtractorConfiguration(Format.PROTO, spanSizeValidationConfig), mockLogger, List()) + + val largestInvalidStartTime = SmallestAllowedStartTimeMicros - 1 + + + // @formatter:off + val nullSpanIdSpan = createSpan(NullString, TraceId, ServiceName1, OperationName1, StartTime, Duration, createTags(1)) + val emptySpanIdSpan = createSpan(EmptyString, TraceId, ServiceName2, OperationName1, StartTime, Duration, createTags(1)) + val nullTraceIdSpan = createSpan(SpanId, NullString, ServiceName1, OperationName1, StartTime, Duration, createTags(1)) + val emptyTraceIdSpan = createSpan(SpanId, EmptyString, ServiceName2, OperationName1, StartTime, Duration, createTags(1)) + val nullServiceNameSpan = createSpan(SpanId, TraceId, NullString, OperationName1, StartTime, Duration, createTags(1)) + val emptyServiceNameSpan = createSpan(SpanId, TraceId, EmptyString, OperationName2, StartTime, Duration, createTags(1)) + val nullOperationNameSpan = createSpan(SpanId, TraceId, ServiceName1, NullString, StartTime, Duration, createTags(1)) + val emptyOperationNameSpan = createSpan(SpanId, TraceId, ServiceName2, EmptyString, StartTime, Duration, createTags(1)) + val tooSmallStartTimeSpan = createSpan(SpanId, TraceId, ServiceName1, OperationName1, largestInvalidStartTime, Duration, createTags(1)) + val negativeStartTimeSpan = createSpan(SpanId, TraceId, ServiceName2, OperationName1, Negative, Duration, createTags(1)) + val tooSmallDurationSpan = createSpan(SpanId, TraceId, ServiceName1, OperationName1, StartTime, Negative, createTags(1)) + val largeSizeSpan = createSpan(SpanId, TraceId, ServiceName1, OperationName1, StartTime, Duration, createTags(50)) + val largeSizeSpanWithSkippedService + = createSpan(SpanId, TraceId, SkipTagTruncationServiceName, OperationName1, StartTime, Duration, createTags(50)) + val spanMap = ListMap( + "NullSpanId" -> (nullSpanIdSpan, SpanIdIsRequired.format(ServiceName1, OperationName1)), + "EmptySpanId" -> (emptySpanIdSpan, SpanIdIsRequired.format(ServiceName2, OperationName1)), + "NullTraceId" -> (nullTraceIdSpan, TraceIdIsRequired.format(ServiceName1, OperationName1)), + "EmptyTraceId" -> (emptyTraceIdSpan, TraceIdIsRequired.format(ServiceName2, OperationName1)), + "NullServiceName" -> (nullServiceNameSpan, ServiceNameIsRequired.format(nullServiceNameSpan.toString)), + "EmptyServiceName" -> (emptyServiceNameSpan, ServiceNameIsRequired.format(emptyServiceNameSpan.toString)), + "NullOperationName" -> (nullOperationNameSpan, OperationNameIsRequired.format(ServiceName1)), + "EmptyOperationName" -> (emptyOperationNameSpan, OperationNameIsRequired.format(ServiceName2)), + "TooSmallStartTime" -> (tooSmallStartTimeSpan, StartTimeIsInvalid.format(largestInvalidStartTime, ServiceName1, OperationName1)), + "NegativeStartTime" -> (negativeStartTimeSpan, StartTimeIsInvalid.format(Negative, ServiceName2, OperationName1)), + "TooSmallDuration" -> (tooSmallDurationSpan, DurationIsInvalid.format(Negative, ServiceName1, OperationName1)) + ) + // @formatter:on + it("should fail validation for spans with invalid data") { + spanMap.foreach(sp => { + val kvPairs = protoSpanExtractor.extractKeyValuePairs(sp._2._1.toByteArray) + withClue(sp._1) { + kvPairs shouldBe Nil + verify(mockLogger).error(sp._2._2) + } + }) + Mockito.verifyNoMoreInteractions(mockLogger) + } + + it("should truncate tags to reduce span when span size exceeded") { + val kvPairs = protoSpanExtractor.extractKeyValuePairs(largeSizeSpan.toByteArray) + kvPairs.foreach { kv => + val spanRecordBytes = kv.value + val span = Span.parseFrom(spanRecordBytes) + assert(span.getTagsList.asScala.exists(tag => tag.getKey.equalsIgnoreCase("error"))) + span.getTagsCount shouldBe 2 + assert(spanRecordBytes.length < SpanSizeLimit) + } + } + + it("shouldn't truncate tags to for skipped service even when span size exceeds limit") { + val kvPairs = protoSpanExtractor.extractKeyValuePairs(largeSizeSpanWithSkippedService.toByteArray) + kvPairs.foreach { kv => + val spanRecordBytes = kv.value + val span = Span.parseFrom(spanRecordBytes) + + spanRecordBytes.length > SpanSizeLimit shouldEqual true + span.getTagsList.size shouldEqual 50 + span.getTagsList.asScala.exists(tag => tag.getKey.equalsIgnoreCase("error")) shouldEqual true + + } + } + } + + private def createTags(maxTagsLimit: Int): Array[Tag] = { + val tags = ArrayBuffer[Tag]() + // adding Error Tag by default + tags += SampleErrorTag + for (i <- 0 until (maxTagsLimit - 1)) { // creating one less tag since error tag is already added above + tags += Tag.newBuilder().setKey("key" + i).setVStr("value" + i).build() + } + tags.toArray + } + + private def createSpan(spanId: String, + traceId: String, + serviceName: String, + operationName: String, + startTimeMicros: Long, + durationMicros: Long, + tags: Seq[Tag]) = { + val builder = Span.newBuilder() + if (spanId != null) { + builder.setSpanId(spanId) + } + if (traceId != null) { + builder.setTraceId(traceId) + } + if (serviceName != null) { + builder.setServiceName(serviceName) + } + if (operationName != null) { + builder.setOperationName(operationName) + } + if (tags.nonEmpty) { + tags.foreach(tag => builder.addTags(tag)) + } + builder.setStartTime(startTimeMicros) + builder.setDuration(durationMicros) + builder.build() + } +} diff --git a/collector/deployment/scripts/publish-to-docker-hub.sh b/collector/deployment/scripts/publish-to-docker-hub.sh new file mode 100755 index 000000000..9f22ceaf2 --- /dev/null +++ b/collector/deployment/scripts/publish-to-docker-hub.sh @@ -0,0 +1,42 @@ +#!/bin/bash + +set -e + +QUALIFIED_DOCKER_IMAGE_NAME=$DOCKER_ORG/$DOCKER_IMAGE_NAME +echo "DOCKER_ORG=$DOCKER_ORG, DOCKER_IMAGE_NAME=$DOCKER_IMAGE_NAME, QUALIFIED_DOCKER_IMAGE_NAME=$QUALIFIED_DOCKER_IMAGE_NAME" +echo "BRANCH=$BRANCH, TAG=$TAG, SHA=$SHA" + +# login +docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD + +# Add tags +if [[ $TAG =~ ([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then + echo "releasing semantic versions" + + unset MAJOR MINOR PATCH + MAJOR="${BASH_REMATCH[1]}" + MINOR="${BASH_REMATCH[2]}" + PATCH="${BASH_REMATCH[3]}" + + # for tag, add MAJOR, MAJOR.MINOR, MAJOR.MINOR.PATCH and latest as tag + docker tag $DOCKER_IMAGE_NAME $QUALIFIED_DOCKER_IMAGE_NAME:$MAJOR + docker push $QUALIFIED_DOCKER_IMAGE_NAME:$MAJOR + + docker tag $DOCKER_IMAGE_NAME $QUALIFIED_DOCKER_IMAGE_NAME:$MAJOR.$MINOR + docker push $QUALIFIED_DOCKER_IMAGE_NAME:$MAJOR.$MINOR + + docker tag $DOCKER_IMAGE_NAME $QUALIFIED_DOCKER_IMAGE_NAME:$MAJOR.$MINOR.$PATCH + docker push $QUALIFIED_DOCKER_IMAGE_NAME:$MAJOR.$MINOR.$PATCH + + docker tag $DOCKER_IMAGE_NAME $QUALIFIED_DOCKER_IMAGE_NAME:latest + docker push $QUALIFIED_DOCKER_IMAGE_NAME:latest + + elif [[ "$BRANCH" == "master" ]]; then + echo "releasing master branch" + + # for 'master' branch, add SHA as tags + docker tag $DOCKER_IMAGE_NAME $QUALIFIED_DOCKER_IMAGE_NAME:$SHA + + # publish image with tags + docker push $QUALIFIED_DOCKER_IMAGE_NAME +fi diff --git a/collector/deployment/terraform/http-span-collector/main.tf b/collector/deployment/terraform/http-span-collector/main.tf new file mode 100644 index 000000000..cb136096c --- /dev/null +++ b/collector/deployment/terraform/http-span-collector/main.tf @@ -0,0 +1,77 @@ +locals { + app_name = "${var.app_name}" + config_file_path = "${path.module}/templates/http-span-collector_conf.tpl" + deployment_yaml_file_path = "${path.module}/templates/deployment_yaml.tpl" + count = "${var.enabled?1:0}" + checksum = "${sha1("${data.template_file.config_data.rendered}")}" + configmap_name = "${local.app_name}-${local.checksum}" +} + +resource "kubernetes_config_map" "haystack-config" { + metadata { + name = "${local.configmap_name}" + namespace = "${var.namespace}" + } + data { + "http-span-collector.conf" = "${data.template_file.config_data.rendered}" + } + count = "${local.count}" +} + +data "template_file" "config_data" { + template = "${file("${local.config_file_path}")}" + + vars { + kafka_endpoint = "${var.kafka_endpoint}" + container_port = "${var.container_port}" + max_spansize_validation_enabled = "${var.max_spansize_validation_enabled}" + max_spansize_log_only = "${var.max_spansize_log_only}" + max_spansize_limit = "${var.max_spansize_limit}" + message_tag_key = "${var.message_tag_key}" + message_tag_value = "${var.message_tag_value}" + max_spansize_skip_tags = "${var.max_spansize_skip_tags}" + max_spansize_skip_services = "${var.max_spansize_skip_services}" + } +} + + +data "template_file" "deployment_yaml" { + template = "${file("${local.deployment_yaml_file_path}")}" + vars { + app_name = "${local.app_name}" + namespace = "${var.namespace}" + graphite_port = "${var.graphite_port}" + graphite_host = "${var.graphite_hostname}" + node_selecter_label = "${var.node_selecter_label}" + image = "${var.image}" + replicas = "${var.replicas}" + memory_limit = "${var.memory_limit}" + memory_request = "${var.memory_request}" + jvm_memory_limit = "${var.jvm_memory_limit}" + cpu_limit = "${var.cpu_limit}" + cpu_request = "${var.cpu_request}" + configmap_name = "${local.configmap_name}" + env_vars = "${indent(9,"${var.env_vars}")}" + container_port = "${var.container_port}" + service_port = "${var.service_port}" + } +} + +resource "null_resource" "kubectl_apply" { + triggers { + template = "${data.template_file.deployment_yaml.rendered}" + } + provisioner "local-exec" { + command = "echo '${data.template_file.deployment_yaml.rendered}' | ${var.kubectl_executable_name} apply -f - --context ${var.kubectl_context_name}" + } + count = "${local.count}" +} + + +resource "null_resource" "kubectl_destroy" { + provisioner "local-exec" { + command = "echo '${data.template_file.deployment_yaml.rendered}' | ${var.kubectl_executable_name} delete -f - --context ${var.kubectl_context_name}" + when = "destroy" + } + count = "${local.count}" +} diff --git a/collector/deployment/terraform/http-span-collector/outputs.tf b/collector/deployment/terraform/http-span-collector/outputs.tf new file mode 100644 index 000000000..e69de29bb diff --git a/collector/deployment/terraform/http-span-collector/templates/deployment_yaml.tpl b/collector/deployment/terraform/http-span-collector/templates/deployment_yaml.tpl new file mode 100644 index 000000000..2347a1e0c --- /dev/null +++ b/collector/deployment/terraform/http-span-collector/templates/deployment_yaml.tpl @@ -0,0 +1,74 @@ +# ------------------- Deployment ------------------- # + +kind: Deployment +apiVersion: apps/v1beta2 +metadata: + labels: + k8s-app: ${app_name} + name: ${app_name} + namespace: ${namespace} +spec: + replicas: ${replicas} + revisionHistoryLimit: 10 + selector: + matchLabels: + k8s-app: ${app_name} + template: + metadata: + labels: + k8s-app: ${app_name} + spec: + containers: + - name: ${app_name} + image: ${image} + volumeMounts: + # Create on-disk volume to store exec logs + - mountPath: /config + name: config-volume + resources: + limits: + cpu: ${cpu_limit} + memory: ${memory_limit}Mi + requests: + cpu: ${cpu_request} + memory: ${memory_request}Mi + env: + - name: "HAYSTACK_OVERRIDES_CONFIG_PATH" + value: "/config/http-span-collector.conf" + - name: "HAYSTACK_GRAPHITE_HOST" + value: "${graphite_host}" + - name: "HAYSTACK_GRAPHITE_PORT" + value: "${graphite_port}" + - name: "JAVA_XMS" + value: "${jvm_memory_limit}m" + - name: "JAVA_XMX" + value: "${jvm_memory_limit}m" + ${env_vars} + livenessProbe: + httpGet: + path: /isActive + port: ${container_port} + initialDelaySeconds: 30 + periodSeconds: 5 + failureThreshold: 6 + nodeSelector: + ${node_selecter_label} + volumes: + - name: config-volume + configMap: + name: ${configmap_name} +# ------------------- Service ------------------- # +--- +apiVersion: v1 +kind: Service +metadata: + labels: + k8s-app: ${app_name} + name: ${app_name} + namespace: ${namespace} +spec: + ports: + - port: ${service_port} + targetPort: ${container_port} + selector: + k8s-app: ${app_name} diff --git a/collector/deployment/terraform/http-span-collector/templates/http-span-collector_conf.tpl b/collector/deployment/terraform/http-span-collector/templates/http-span-collector_conf.tpl new file mode 100644 index 000000000..b0262c5a0 --- /dev/null +++ b/collector/deployment/terraform/http-span-collector/templates/http-span-collector_conf.tpl @@ -0,0 +1,33 @@ +kafka { + producer { + topic = "proto-spans" + props { + bootstrap.servers = "${kafka_endpoint}" + retries = 50 + batch.size = 153600 + linger.ms = 250 + compression.type = "lz4" + } + } +} + +extractor { + output.format = "proto" + spans.validation { + max.size { + enable = "${max_spansize_validation_enabled}" + log.only = "${max_spansize_log_only}" + max.size.limit = "${max_spansize_limit}" + message.tag.key = "${message_tag_key}" + message.tag.value = "${message_tag_value}" + skip.tags = "${max_spansize_skip_tags}" + skip.services = "${max_spansize_skip_services}" + } + } +} + +http { + host = "0.0.0.0" + port = ${container_port} +} + diff --git a/collector/deployment/terraform/http-span-collector/variables.tf b/collector/deployment/terraform/http-span-collector/variables.tf new file mode 100644 index 000000000..80d59f58b --- /dev/null +++ b/collector/deployment/terraform/http-span-collector/variables.tf @@ -0,0 +1,36 @@ +variable "image" {} +variable "replicas" {} +variable "enabled"{} +variable "namespace" {} +variable "graphite_hostname" {} +variable "graphite_port" {} +variable "kafka_endpoint" {} +variable "kubectl_executable_name" {} +variable "kubectl_context_name" {} +variable "node_selecter_label"{} +variable "memory_request"{} +variable "memory_limit"{} +variable "jvm_memory_limit"{} +variable "cpu_request"{} +variable "cpu_limit"{} +variable "app_name"{ default = "http-span-collector" } +variable "env_vars" {} +variable "service_port" { + default = 80 +} +variable "container_port" { + default = 8080 +} + +variable "termination_grace_period" { + default = 30 +} +variable "haystack_cluster_name" {} + +variable "max_spansize_validation_enabled" {} +variable "max_spansize_log_only" {} +variable "max_spansize_limit" {} +variable "message_tag_key" {} +variable "message_tag_value" {} +variable "max_spansize_skip_tags" {} +variable "max_spansize_skip_services" {} diff --git a/collector/deployment/terraform/kinesis-span-collector/main.tf b/collector/deployment/terraform/kinesis-span-collector/main.tf new file mode 100644 index 000000000..5bdb9f7ac --- /dev/null +++ b/collector/deployment/terraform/kinesis-span-collector/main.tf @@ -0,0 +1,80 @@ +locals { + app_name = "${var.app_name}" + config_file_path = "${path.module}/templates/kinesis-span-collector_conf.tpl" + deployment_yaml_file_path = "${path.module}/templates/deployment_yaml.tpl" + count = "${var.enabled?1:0}" + checksum = "${sha1("${data.template_file.config_data.rendered}")}" + configmap_name = "${local.app_name}-${local.checksum}" +} + +resource "kubernetes_config_map" "haystack-config" { + metadata { + name = "${local.configmap_name}" + namespace = "${var.namespace}" + } + data { + "kinesis-span-collector.conf" = "${data.template_file.config_data.rendered}" + } + count = "${local.count}" +} + +data "template_file" "config_data" { + template = "${file("${local.config_file_path}")}" + + vars { + kinesis_stream_region = "${var.kinesis_stream_region}" + kinesis_stream_name = "${var.kinesis_stream_name}" + kafka_endpoint = "${var.kafka_endpoint}" + sts_role_arn = "${var.sts_role_arn}" + app_group_name = "${var.haystack_cluster_name}-${var.app_name}" + max_spansize_validation_enabled = "${var.max_spansize_validation_enabled}" + max_spansize_log_only = "${var.max_spansize_log_only}" + max_spansize_limit = "${var.max_spansize_limit}" + message_tag_key = "${var.message_tag_key}" + message_tag_value = "${var.message_tag_value}" + max_spansize_skip_tags = "${var.max_spansize_skip_tags}" + max_spansize_skip_services = "${var.max_spansize_skip_services}" + } +} + + +data "template_file" "deployment_yaml" { + template = "${file("${local.deployment_yaml_file_path}")}" + vars { + app_name = "${local.app_name}" + namespace = "${var.namespace}" + graphite_port = "${var.graphite_port}" + graphite_host = "${var.graphite_hostname}" + node_selecter_label = "${var.node_selecter_label}" + image = "${var.image}" + replicas = "${var.replicas}" + memory_limit = "${var.memory_limit}" + memory_request = "${var.memory_request}" + jvm_memory_limit = "${var.jvm_memory_limit}" + cpu_limit = "${var.cpu_limit}" + cpu_request = "${var.cpu_request}" + configmap_name = "${local.configmap_name}" + env_vars = "${indent(9,"${var.env_vars}")}" + + } +} + +resource "null_resource" "kubectl_apply" { + triggers { + template = "${data.template_file.deployment_yaml.rendered}" + } + provisioner "local-exec" { + command = "echo '${data.template_file.deployment_yaml.rendered}' | ${var.kubectl_executable_name} apply -f - --context ${var.kubectl_context_name}" + } + count = "${local.count}" +} + + +resource "null_resource" "kubectl_destroy" { + + provisioner "local-exec" { + command = "echo '${data.template_file.deployment_yaml.rendered}' | ${var.kubectl_executable_name} delete -f - --context ${var.kubectl_context_name}" + when = "destroy" + } + count = "${local.count}" +} diff --git a/collector/deployment/terraform/kinesis-span-collector/outputs.tf b/collector/deployment/terraform/kinesis-span-collector/outputs.tf new file mode 100644 index 000000000..e69de29bb diff --git a/collector/deployment/terraform/kinesis-span-collector/templates/deployment_yaml.tpl b/collector/deployment/terraform/kinesis-span-collector/templates/deployment_yaml.tpl new file mode 100644 index 000000000..3f1ac6072 --- /dev/null +++ b/collector/deployment/terraform/kinesis-span-collector/templates/deployment_yaml.tpl @@ -0,0 +1,62 @@ +# ------------------- Deployment ------------------- # + +kind: Deployment +apiVersion: apps/v1beta2 +metadata: + labels: + k8s-app: ${app_name} + name: ${app_name} + namespace: ${namespace} +spec: + replicas: ${replicas} + revisionHistoryLimit: 10 + selector: + matchLabels: + k8s-app: ${app_name} + template: + metadata: + labels: + k8s-app: ${app_name} + spec: + containers: + - name: ${app_name} + image: ${image} + volumeMounts: + # Create on-disk volume to store exec logs + - mountPath: /config + name: config-volume + resources: + limits: + cpu: ${cpu_limit} + memory: ${memory_limit}Mi + requests: + cpu: ${cpu_request} + memory: ${memory_request}Mi + env: + - name: "HAYSTACK_OVERRIDES_CONFIG_PATH" + value: "/config/kinesis-span-collector.conf" + - name: "HAYSTACK_GRAPHITE_HOST" + value: "${graphite_host}" + - name: "HAYSTACK_GRAPHITE_PORT" + value: "${graphite_port}" + - name: "JAVA_XMS" + value: "${jvm_memory_limit}m" + - name: "JAVA_XMX" + value: "${jvm_memory_limit}m" + ${env_vars} + livenessProbe: + exec: + command: + - grep + - "true" + - /app/isHealthy + initialDelaySeconds: 30 + periodSeconds: 5 + failureThreshold: 6 + nodeSelector: + ${node_selecter_label} + volumes: + - name: config-volume + configMap: + name: ${configmap_name} + diff --git a/collector/deployment/terraform/kinesis-span-collector/templates/kinesis-span-collector_conf.tpl b/collector/deployment/terraform/kinesis-span-collector/templates/kinesis-span-collector_conf.tpl new file mode 100644 index 000000000..e6dadc5f5 --- /dev/null +++ b/collector/deployment/terraform/kinesis-span-collector/templates/kinesis-span-collector_conf.tpl @@ -0,0 +1,54 @@ +kafka { + producer { + topic = "proto-spans" + props { + bootstrap.servers = "${kafka_endpoint}" + retries = 50 + batch.size = 153600 + linger.ms = 250 + compression.type = "lz4" + } + } +} + +extractor { + output.format = "proto" + spans.validation { + max.size { + enable = "${max_spansize_validation_enabled}" + log.only = "${max_spansize_log_only}" + max.size.limit = "${max_spansize_limit}" + message.tag.key = "${message_tag_key}" + message.tag.value = "${message_tag_value}" + skip.tags = "${max_spansize_skip_tags}" + skip.services = "${max_spansize_skip_services}" + } + } +} + +kinesis { + sts.role.arn = "${sts_role_arn}" + aws.region = "${kinesis_stream_region}" + app.group.name = "${app_group_name}" + + stream { + name = "${kinesis_stream_name}" + position = "LATEST" + } + + checkpoint { + interval.ms = 15000 + retries = 50 + retry.interval.ms = 250 + } + + task.backoff.ms = 200 + max.records.read = 2000 + idle.time.between.reads.ms = 500 + shard.sync.interval.ms = 30000 + + metrics { + level = "NONE" + buffer.time.ms = 15000 + } +} diff --git a/collector/deployment/terraform/kinesis-span-collector/variables.tf b/collector/deployment/terraform/kinesis-span-collector/variables.tf new file mode 100644 index 000000000..0ff57d567 --- /dev/null +++ b/collector/deployment/terraform/kinesis-span-collector/variables.tf @@ -0,0 +1,32 @@ +variable "image" {} +variable "replicas" {} +variable "enabled"{} +variable "namespace" {} +variable "kinesis_stream_region" {} +variable "kinesis_stream_name" {} +variable "sts_role_arn" {} +variable "graphite_hostname" {} +variable "graphite_port" {} +variable "kafka_endpoint" {} +variable "kubectl_executable_name" {} +variable "kubectl_context_name" {} +variable "node_selecter_label"{} +variable "memory_request"{} +variable "memory_limit"{} +variable "jvm_memory_limit"{} +variable "cpu_request"{} +variable "cpu_limit"{} +variable "app_name"{ default = "kinesis-span-collector" } +variable "env_vars" {} +variable "max_spansize_validation_enabled" {} +variable "max_spansize_log_only" {} +variable "max_spansize_limit" {} +variable "message_tag_key" {} +variable "message_tag_value" {} +variable "max_spansize_skip_tags" {} +variable "max_spansize_skip_services" {} + +variable "termination_grace_period" { + default = 30 +} +variable "haystack_cluster_name" {} diff --git a/collector/deployment/terraform/main.tf b/collector/deployment/terraform/main.tf new file mode 100644 index 000000000..9d901c290 --- /dev/null +++ b/collector/deployment/terraform/main.tf @@ -0,0 +1,69 @@ +locals { + default_kinesis_stream_name = "${var.kinesis-stream_name}" + default_kinesis_stream_region = "${var.kinesis-stream_region}" +} + +module "kinesis-span-collector" { + source = "kinesis-span-collector" + image = "expediadotcom/haystack-kinesis-span-collector:${var.collector["version"]}" + replicas = "${var.collector["kinesis_span_collector_instances"]}" + enabled = "${var.collector["kinesis_span_collector_enabled"]}" + + kinesis_stream_name = "${var.collector["kinesis_stream_name"] == "" ? local.default_kinesis_stream_name : var.collector["kinesis_stream_name"]}" + kinesis_stream_region = "${var.collector["kinesis_stream_region"] == "" ? local.default_kinesis_stream_region : var.collector["kinesis_stream_region"]}" + + sts_role_arn = "${var.collector["kinesis_span_collector_sts_role_arn"]}" + env_vars = "${var.collector["kinesis_span_collector_environment_overrides"]}" + + namespace = "${var.app_namespace}" + kafka_endpoint = "${var.kafka_hostname}:${var.kafka_port}" + graphite_hostname = "${var.graphite_hostname}" + graphite_port = "${var.graphite_port}" + haystack_cluster_name = "${var.haystack_cluster_name}" + node_selecter_label = "${var.node_selector_label}" + kubectl_executable_name = "${var.kubectl_executable_name}" + kubectl_context_name = "${var.kubectl_context_name}" + cpu_limit = "${var.collector["kinesis_span_collector_cpu_limit"]}" + cpu_request = "${var.collector["kinesis_span_collector_cpu_request"]}" + memory_request = "${var.collector["kinesis_span_collector_memory_request"]}" + memory_limit = "${var.collector["kinesis_span_collector_memory_limit"]}" + jvm_memory_limit = "${var.collector["kinesis_span_collector_jvm_memory_limit"]}" + app_name = "${var.collector["kinesis_span_collector_app_name"]}" + max_spansize_validation_enabled = "${var.collector["kinesis_span_collector_max_spansize_validation_enabled"]}" + max_spansize_log_only = "${var.collector["kinesis_span_collector_max_spansize_log_only"]}" + max_spansize_limit = "${var.collector["kinesis_span_collector_max_spansize_limit"]}" + message_tag_key = "${var.collector["kinesis_span_collector_message_tag_key"]}" + message_tag_value = "${var.collector["kinesis_span_collector_message_tag_value"]}" + max_spansize_skip_tags = "${var.collector["kinesis_span_collector_max_spansize_skip_tags"]}" + max_spansize_skip_services = "${var.collector["kinesis_span_collector_max_spansize_skip_services"]}" +} + +module "http-span-collector" { + source = "http-span-collector" + image = "expediadotcom/haystack-http-span-collector:${var.collector["version"]}" + replicas = "${var.collector["http_span_collector_instances"]}" + enabled = "${var.collector["http_span_collector_enabled"]}" + env_vars = "${var.collector["http_span_collector_environment_overrides"]}" + + namespace = "${var.app_namespace}" + kafka_endpoint = "${var.kafka_hostname}:${var.kafka_port}" + graphite_hostname = "${var.graphite_hostname}" + graphite_port = "${var.graphite_port}" + haystack_cluster_name = "${var.haystack_cluster_name}" + node_selecter_label = "${var.node_selector_label}" + kubectl_executable_name = "${var.kubectl_executable_name}" + kubectl_context_name = "${var.kubectl_context_name}" + cpu_limit = "${var.collector["http_span_collector_cpu_limit"]}" + cpu_request = "${var.collector["http_span_collector_cpu_request"]}" + memory_request = "${var.collector["http_span_collector_memory_request"]}" + memory_limit = "${var.collector["http_span_collector_memory_limit"]}" + jvm_memory_limit = "${var.collector["http_span_collector_jvm_memory_limit"]}" + app_name = "${var.collector["http_span_collector_app_name"]}" + max_spansize_validation_enabled = "${var.collector["http_span_collector_max_spansize_validation_enabled"]}" + max_spansize_log_only = "${var.collector["http_span_collector_max_spansize_log_only"]}" + max_spansize_limit = "${var.collector["http_span_collector_max_spansize_limit"]}" + message_tag_key = "${var.collector["http_span_collector_message_tag_key"]}" + message_tag_value = "${var.collector["http_span_collector_message_tag_value"]}" + max_spansize_skip_tags = "${var.collector["http_span_collector_max_spansize_skip_tags"]}" + max_spansize_skip_services = "${var.collector["http_span_collector_max_spansize_skip_services"]}" +} diff --git a/collector/deployment/terraform/outputs.tf b/collector/deployment/terraform/outputs.tf new file mode 100644 index 000000000..e69de29bb diff --git a/collector/deployment/terraform/variables.tf b/collector/deployment/terraform/variables.tf new file mode 100644 index 000000000..71620860c --- /dev/null +++ b/collector/deployment/terraform/variables.tf @@ -0,0 +1,17 @@ + +variable "kafka_hostname" {} +variable "kafka_port" {} +variable "graphite_hostname" {} +variable "graphite_port" {} +variable "haystack_cluster_name" {} +variable "kubectl_context_name" {} +variable "kubectl_executable_name" {} +variable "app_namespace" {} +variable "node_selector_label"{} +variable "kinesis-stream_name" {} +variable "kinesis-stream_region" {} + +# collectors config +variable "collector" { + type = "map" +} diff --git a/collector/haystack-span-decorators/pom.xml b/collector/haystack-span-decorators/pom.xml new file mode 100644 index 000000000..4ed0cbb9e --- /dev/null +++ b/collector/haystack-span-decorators/pom.xml @@ -0,0 +1,24 @@ + + + + + haystack-collector + com.expedia.www + 1.0-SNAPSHOT + + + 4.0.0 + haystack-span-decorators + 1.0-SNAPSHOT + jar + + + src/main/java + + + net.alchim31.maven + scala-maven-plugin + + + + \ No newline at end of file diff --git a/collector/haystack-span-decorators/src/main/java/com/expedia/www/haystack/span/decorators/AdditionalTagsSpanDecorator.java b/collector/haystack-span-decorators/src/main/java/com/expedia/www/haystack/span/decorators/AdditionalTagsSpanDecorator.java new file mode 100644 index 000000000..a65602f01 --- /dev/null +++ b/collector/haystack-span-decorators/src/main/java/com/expedia/www/haystack/span/decorators/AdditionalTagsSpanDecorator.java @@ -0,0 +1,49 @@ +package com.expedia.www.haystack.span.decorators; + +import com.expedia.open.tracing.Span; +import com.expedia.open.tracing.Tag; +import com.typesafe.config.Config; +import com.typesafe.config.ConfigFactory; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; + +import java.util.Map; +import java.util.stream.Collectors; + +public class AdditionalTagsSpanDecorator implements SpanDecorator { + + private Config tagConfig; + + public AdditionalTagsSpanDecorator() { + } + + @Override + public void init(Config config) { + tagConfig = config; + } + + @Override + public Span.Builder decorate(Span.Builder span) { + return addHaystackMetadataTags(span); + } + + @Override + public String name() { + return AdditionalTagsSpanDecorator.class.getName(); + } + + private Span.Builder addHaystackMetadataTags(Span.Builder spanBuilder) { + final Map spanTags = spanBuilder.getTagsList().stream() + .collect(Collectors.toMap(Tag::getKey, Tag::getVStr)); + + tagConfig.entrySet().forEach(tag -> { + final String tagValue = spanTags.getOrDefault(tag.getKey(), null); + if (StringUtils.isEmpty(tagValue)) { + spanBuilder.addTags(Tag.newBuilder().setKey(tag.getKey()).setVStr(tag.getValue().unwrapped().toString())); + } + }); + + return spanBuilder; + } + +} diff --git a/collector/haystack-span-decorators/src/main/java/com/expedia/www/haystack/span/decorators/SpanDecorator.java b/collector/haystack-span-decorators/src/main/java/com/expedia/www/haystack/span/decorators/SpanDecorator.java new file mode 100644 index 000000000..b8f97bef0 --- /dev/null +++ b/collector/haystack-span-decorators/src/main/java/com/expedia/www/haystack/span/decorators/SpanDecorator.java @@ -0,0 +1,10 @@ +package com.expedia.www.haystack.span.decorators; + +import com.expedia.open.tracing.Span; +import com.typesafe.config.Config; + +public interface SpanDecorator { + void init(Config config); + Span.Builder decorate(Span.Builder span); + String name(); +} diff --git a/collector/haystack-span-decorators/src/main/java/com/expedia/www/haystack/span/decorators/plugin/config/Plugin.java b/collector/haystack-span-decorators/src/main/java/com/expedia/www/haystack/span/decorators/plugin/config/Plugin.java new file mode 100644 index 000000000..929e83f31 --- /dev/null +++ b/collector/haystack-span-decorators/src/main/java/com/expedia/www/haystack/span/decorators/plugin/config/Plugin.java @@ -0,0 +1,29 @@ +package com.expedia.www.haystack.span.decorators.plugin.config; + +import java.util.List; + +public class Plugin { + private String directory; + private List pluginConfigurationList; + + public Plugin(String directory, List pluginConfigurationList) { + this.directory = directory; + this.pluginConfigurationList = pluginConfigurationList; + } + + public String getDirectory() { + return directory; + } + + public List getPluginConfigurationList() { + return pluginConfigurationList; + } + + public void setDirectory(String directory) { + this.directory = directory; + } + + public void setPluginConfigurationList(List pluginConfigurationList) { + this.pluginConfigurationList = pluginConfigurationList; + } +} diff --git a/collector/haystack-span-decorators/src/main/java/com/expedia/www/haystack/span/decorators/plugin/config/PluginConfiguration.java b/collector/haystack-span-decorators/src/main/java/com/expedia/www/haystack/span/decorators/plugin/config/PluginConfiguration.java new file mode 100644 index 000000000..e371c2d7f --- /dev/null +++ b/collector/haystack-span-decorators/src/main/java/com/expedia/www/haystack/span/decorators/plugin/config/PluginConfiguration.java @@ -0,0 +1,33 @@ +package com.expedia.www.haystack.span.decorators.plugin.config; + +import com.typesafe.config.Config; + +public class PluginConfiguration { + private String name; + private Config config; + + public PluginConfiguration(String name, Config config) { + this.name = name; + this.config = config; + } + + public PluginConfiguration() { + + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Config getConfig() { + return config; + } + + public void setConfig(Config config) { + this.config = config; + } +} diff --git a/collector/haystack-span-decorators/src/main/java/com/expedia/www/haystack/span/decorators/plugin/loader/SpanDecoratorPluginLoader.java b/collector/haystack-span-decorators/src/main/java/com/expedia/www/haystack/span/decorators/plugin/loader/SpanDecoratorPluginLoader.java new file mode 100644 index 000000000..3e8caebc0 --- /dev/null +++ b/collector/haystack-span-decorators/src/main/java/com/expedia/www/haystack/span/decorators/plugin/loader/SpanDecoratorPluginLoader.java @@ -0,0 +1,74 @@ +package com.expedia.www.haystack.span.decorators.plugin.loader; + +import com.expedia.www.haystack.span.decorators.SpanDecorator; + +import com.expedia.www.haystack.span.decorators.plugin.config.Plugin; +import com.expedia.www.haystack.span.decorators.plugin.config.PluginConfiguration; +import org.slf4j.Logger; + +import java.io.File; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.ServiceLoader; + +public class SpanDecoratorPluginLoader { + private Logger logger; + private Plugin pluginConfig; + private static SpanDecoratorPluginLoader spanDecoratorPluginLoader; + private ServiceLoader loader; + + private SpanDecoratorPluginLoader(Logger logger, Plugin pluginConfig) { + this.logger = logger; + this.pluginConfig = pluginConfig; + } + + public static synchronized SpanDecoratorPluginLoader getInstance(Logger logger, Plugin pluginConfig) { + if (spanDecoratorPluginLoader == null) { + spanDecoratorPluginLoader = new SpanDecoratorPluginLoader(logger, pluginConfig); + } + spanDecoratorPluginLoader.createLoader(); + + return spanDecoratorPluginLoader; + } + + private void createLoader() { + try { + final File[] pluginFiles = new File(pluginConfig.getDirectory()).listFiles(); + if (pluginFiles != null) { + final List urls = new ArrayList<>(); + for (final File file : pluginFiles) { + urls.add(file.toURI().toURL()); + } + URLClassLoader urlClassLoader = new URLClassLoader(urls.toArray(new URL[0]), SpanDecorator.class.getClassLoader()); + loader = ServiceLoader.load(SpanDecorator.class, urlClassLoader); + } + } catch (Exception ex) { + logger.error("Could not create the class loader for finding jar ", ex); + } catch (NoClassDefFoundError ex) { + logger.error("Could not find the class ", ex); + } + } + + public List getSpanDecorators() { + List spanDecorators = new ArrayList<>(); + try { + loader.forEach((spanDecorator) -> { + final PluginConfiguration validFirstConfig = pluginConfig.getPluginConfigurationList().stream().filter(pluginConfiguration -> + pluginConfiguration.getName().equals(spanDecorator.name())).findFirst().orElse(null); + if (validFirstConfig != null) { + spanDecorator.init(validFirstConfig.getConfig()); + spanDecorators.add(spanDecorator); + logger.info("Successfully loaded the plugin {}", spanDecorator.name()); + } + }); + } catch (Exception ex) { + logger.error("Unable to load the external span decorators ", ex); + } + + return spanDecorators; + } +} diff --git a/collector/haystack-span-decorators/src/test/java/com/expedia/www/haystack/span/decorators/AdditionalTagsSpanDecoratorTest.java b/collector/haystack-span-decorators/src/test/java/com/expedia/www/haystack/span/decorators/AdditionalTagsSpanDecoratorTest.java new file mode 100644 index 000000000..217d73d10 --- /dev/null +++ b/collector/haystack-span-decorators/src/test/java/com/expedia/www/haystack/span/decorators/AdditionalTagsSpanDecoratorTest.java @@ -0,0 +1,75 @@ +package com.expedia.www.haystack.span.decorators; + +import com.expedia.open.tracing.Span; +import com.expedia.open.tracing.Tag; +import com.typesafe.config.ConfigFactory; +import org.apache.commons.lang3.StringUtils; +import org.junit.Before; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.*; + +public class AdditionalTagsSpanDecoratorTest { + private final static Logger logger = LoggerFactory.getLogger(AdditionalTagsSpanDecorator.class); + + @Before + public void setup() { + + } + + @Test + public void decorateWithNoDuplicateTags() { + final Map tagConfig = new HashMap(){{ + put("X-HAYSTACK-TAG1", "VALUE1"); + put("X-HAYSTACK-TAG2", "VALUE2"); + }}; + final AdditionalTagsSpanDecorator additionalTagsSpanDecorator = new AdditionalTagsSpanDecorator(); + additionalTagsSpanDecorator.init(ConfigFactory.parseMap(tagConfig)); + final Span resultSpan = additionalTagsSpanDecorator.decorate(Span.newBuilder()).build(); + + final boolean res = resultSpan.getTagsList().stream().allMatch(tag -> { + final String tagValue = tagConfig.getOrDefault(tag.getKey(), null); + if (StringUtils.isEmpty(tagValue)) { + return true; + } else if(tagValue.equals(tag.getVStr())) { + + return true; + } + return false; + }); + + assertEquals(res, true); + } + + @Test + public void decorateWithExistingDuplicateTags() { + final Map tagConfig = new HashMap(){{ + put("X-HAYSTACK-TAG1", "VALUE1"); + put("X-HAYSTACK-TAG2", "VALUE2"); + }}; + final AdditionalTagsSpanDecorator additionalTagsSpanDecorator = new AdditionalTagsSpanDecorator(); + additionalTagsSpanDecorator.init(ConfigFactory.parseMap(tagConfig)); + final Span.Builder spanBuilder = Span.newBuilder().addTags(Tag.newBuilder().setKey("X-HAYSTACK-TAG1").setVStr("VALUE3")); + final Span resultSpan = additionalTagsSpanDecorator.decorate(spanBuilder).build(); + + final boolean res = resultSpan.getTagsList().stream().allMatch(tag -> { + final String tagValue = tagConfig.getOrDefault(tag.getKey(), null); + if (StringUtils.isEmpty(tagValue)) { + return true; + } else if(tagValue.equals(tag.getVStr())) { + return true; + } else if(tag.getVStr().equals("VALUE3")) { + return true; + } + return false; + }); + + assertEquals(res, true); + } + +} \ No newline at end of file diff --git a/collector/haystack-span-decorators/src/test/resources/logback-test.xml b/collector/haystack-span-decorators/src/test/resources/logback-test.xml new file mode 100644 index 000000000..adfa02c68 --- /dev/null +++ b/collector/haystack-span-decorators/src/test/resources/logback-test.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/collector/http/Makefile b/collector/http/Makefile new file mode 100644 index 000000000..978675c20 --- /dev/null +++ b/collector/http/Makefile @@ -0,0 +1,33 @@ +.PHONY: docker_build release prepare_integration_test_env + +export DOCKER_ORG := expediadotcom +export DOCKER_IMAGE_NAME := haystack-http-span-collector +PWD := $(shell pwd) + +docker_build: + docker build -t $(DOCKER_IMAGE_NAME) -f build/docker/Dockerfile . + +prepare_integration_test_env: docker_build + docker-compose -f build/integration-tests/docker-compose.yml -p sandbox up -d + + # kafka sometimes take time to start + sleep 30 + +integration_test: prepare_integration_test_env + # run tests in a container so that we can join the docker-compose network and talk to kafka + docker run \ + -it \ + --network=sandbox_default \ + -v $(PWD)/..:/src \ + -v ~/.m2:/root/.m2 \ + -w /src \ + maven:3.5.0-jdk-8 \ + mvn scoverage:integration-check -pl http -am + + # stop all the containers + docker-compose -f build/integration-tests/docker-compose.yml -p sandbox stop + docker rm $(shell docker ps -a -q) + docker volume rm $(shell docker volume ls -q) + +release: docker_build + ../deployment/scripts/publish-to-docker-hub.sh diff --git a/collector/http/README.md b/collector/http/README.md new file mode 100644 index 000000000..35ca771ce --- /dev/null +++ b/collector/http/README.md @@ -0,0 +1,40 @@ +# Http Span Collector + +The http collector is a web service built on akka-http. It accepts [proto](https://github.com/ExpediaDotCom/haystack-idl/tree/master/proto) serialized and json serialized spans on port 8080(configurable). + +Collector has two endpoints + 1. `/span`: It is for span ingestion. The 'Content-Type' header is used to understand the data format. Therefore one needs to set it correctly as: + * `application/json`: Json formatted spans + * `application/octet-stream`: Proto-serialized binary format. + + 2. `/isActive`: It can be used as a health check by your load balancer + +### How to publish spans in json format + +Span's json schema should match with the object model described [here](./src/main/scala/com/expedia/www/haystack/http/span/collector/json/Span.scala) + +``` +curl -XPOST -H "Content-Type: application/json" -d ' \ +{ + "traceId": "466848c0-a105-4867-8685-e3d00e3eb254", + "spanId": "8f79f97b-a317-4c8f-bbfd-5fd228550416", + "serviceName": "baz", + "operationName": "foo", + "startTime": 1521482680950000, + "duration": 2000, + "tags": [{ + "key": "span.kind", + "value": "server" + }, { + "key": "error", + "value": false + }] +}' \ +"http://localhost:8080/span" +``` + +### How to publish spans in proto format + +``` +curl -XPOST -H "Content-Type: application/octet-stream" -d '' "http://localhost:8080/span" +``` \ No newline at end of file diff --git a/collector/http/build/docker/Dockerfile b/collector/http/build/docker/Dockerfile new file mode 100644 index 000000000..d5a34f1fb --- /dev/null +++ b/collector/http/build/docker/Dockerfile @@ -0,0 +1,19 @@ +FROM openjdk:8-jre +MAINTAINER Haystack + +ENV APP_NAME haystack-http-span-collector +ENV APP_HOME /app/bin +ENV JMXTRANS_AGENT jmxtrans-agent-1.2.6 + +RUN mkdir -p ${APP_HOME} + +COPY target/${APP_NAME}.jar ${APP_HOME}/ +COPY build/docker/start-app.sh ${APP_HOME}/ +COPY build/docker/jmxtrans-agent.xml ${APP_HOME}/ + +RUN chmod +x ${APP_HOME}/start-app.sh +ADD https://github.com/jmxtrans/jmxtrans-agent/releases/download/${JMXTRANS_AGENT}/${JMXTRANS_AGENT}.jar ${APP_HOME}/ + +WORKDIR ${APP_HOME} + +ENTRYPOINT ["./start-app.sh"] diff --git a/collector/http/build/docker/jmxtrans-agent.xml b/collector/http/build/docker/jmxtrans-agent.xml new file mode 100644 index 000000000..41a320477 --- /dev/null +++ b/collector/http/build/docker/jmxtrans-agent.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + ${HAYSTACK_GRAPHITE_HOST:monitoring-influxdb-graphite.kube-system.svc} + ${HAYSTACK_GRAPHITE_PORT:2003} + ${HAYSTACK_GRAPHITE_ENABLED:true} + haystack.collector.http.#hostname#. + + 60 + diff --git a/collector/http/build/docker/start-app.sh b/collector/http/build/docker/start-app.sh new file mode 100755 index 000000000..9b7a9a415 --- /dev/null +++ b/collector/http/build/docker/start-app.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +[ -z "$JAVA_XMS" ] && JAVA_XMS=1024m +[ -z "$JAVA_XMX" ] && JAVA_XMX=1024m +[ -z "$JAVA_GC_OPTS" ] && JAVA_GC_OPTS="-XX:+UseG1GC -XX:+ExitOnOutOfMemoryError" + +set -e +JAVA_OPTS="${JAVA_OPTS} \ +-javaagent:${APP_HOME}/${JMXTRANS_AGENT}.jar=${APP_HOME}/jmxtrans-agent.xml \ +${JAVA_GC_OPTS} \ +-Xmx${JAVA_XMX} \ +-Xms${JAVA_XMS} \ +-Dcom.sun.management.jmxremote.authenticate=false \ +-Dcom.sun.management.jmxremote.ssl=false \ +-Dcom.sun.management.jmxremote.port=1098 \ +-Dcom.sun.management.jmxremote.rmi.port=1098 \ +-Dapplication.name=${APP_NAME} \ +-Dapplication.home=${APP_HOME}" + +exec java ${JAVA_OPTS} -jar "${APP_HOME}/${APP_NAME}.jar" diff --git a/collector/http/build/integration-tests/app-integration-test.conf b/collector/http/build/integration-tests/app-integration-test.conf new file mode 100644 index 000000000..f3f7e2070 --- /dev/null +++ b/collector/http/build/integration-tests/app-integration-test.conf @@ -0,0 +1,17 @@ +kafka { + producer { + topic = "proto-spans" + props { + bootstrap.servers = "kafkasvc:9092" + } + } +} + +extractor { + output.format = "proto" +} + +http { + host = "0.0.0.0" + port = 8080 +} \ No newline at end of file diff --git a/collector/http/build/integration-tests/docker-compose.yml b/collector/http/build/integration-tests/docker-compose.yml new file mode 100644 index 000000000..d6cba5549 --- /dev/null +++ b/collector/http/build/integration-tests/docker-compose.yml @@ -0,0 +1,20 @@ +version: '3' +services: + zookeeper: + image: wurstmeister/zookeeper + ports: + - "2181" + kafkasvc: + image: wurstmeister/kafka:0.11.0.1 + ports: + - "9092" + depends_on: + - zookeeper + links: + - zookeeper:zk + environment: + KAFKA_ADVERTISED_HOST_NAME: kafkasvc + KAFKA_ADVERTISED_PORT: 9092 + KAFKA_ZOOKEEPER_CONNECT: zk:2181 + volumes: + - /var/run/docker.sock:/var/run/docker.sock diff --git a/collector/http/pom.xml b/collector/http/pom.xml new file mode 100644 index 000000000..207580e10 --- /dev/null +++ b/collector/http/pom.xml @@ -0,0 +1,134 @@ + + + + + haystack-collector + com.expedia.www + 1.0-SNAPSHOT + + + 4.0.0 + haystack-http-span-collector + 1.0-SNAPSHOT + jar + + + com.expedia.www.haystack.http.span.collector.WebServer + ${project.artifactId}-${project.version} + 10.1.5 + 2.5.17 + + + + + com.expedia.www + haystack-collector-commons + ${project.version} + + + + com.typesafe.akka + akka-http_${scala.major.minor.version} + ${akka-http.version} + + + + com.typesafe.akka + akka-stream_${scala.major.minor.version} + ${akka-stream.version} + + + + org.json4s + json4s-jackson_${scala.major.minor.version} + + + + + ${finalName} + + + org.scalatest + scalatest-maven-plugin + + + test + + test + + + com.expedia.www.haystack.http.span.collector.unit + + + + integration-test + integration-test + + test + + + com.expedia.www.haystack.http.span.collector.integration + + /src/http/build/integration-tests/app-integration-test.conf + + + + + + + + org.apache.maven.plugins + maven-shade-plugin + + true + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + + + + package + + shade + + + + + reference.conf + + + ${mainClass} + + + + + + + + + net.alchim31.maven + scala-maven-plugin + + + + org.scalastyle + scalastyle-maven-plugin + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + + + diff --git a/collector/http/src/main/resources/config/base.conf b/collector/http/src/main/resources/config/base.conf new file mode 100644 index 000000000..354554fcf --- /dev/null +++ b/collector/http/src/main/resources/config/base.conf @@ -0,0 +1,37 @@ +kafka { + producer { + topic = "proto-spans" + props { + bootstrap.servers = "localhost:9092" + } + } +} + +extractor { + output.format = "proto" + spans.validation { + + # Validate size of span. Truncate span tags when size exceeds spcified limit. + # enable: true/false + # log.only: if enabled, only logs such spans but doesn't truncate the tags + # max.size.limit: maximum size allowed + # message.tag.key: this tag key will be added when tags are truncated + # message.tag.value: value of the above tag key indicating the truncation + # skip.tags: truncate all span tags except these + # skip.services: truncate span tags for all services except these + max.size { + enable = "false" + log.only = "false" + max.size.limit = 5000 // in bytes + message.tag.key = "X-HAYSTACK-SPAN-INFO" + message.tag.value = "Tags are truncated. REASON: Span Size Limit Exceeded. Please contact Haystack for more details" + skip.tags = ["error"] + skip.services = [] + } + } +} + +http { + host = "0.0.0.0" + port = 8080 +} diff --git a/collector/http/src/main/resources/logback.xml b/collector/http/src/main/resources/logback.xml new file mode 100644 index 000000000..ab4e25a63 --- /dev/null +++ b/collector/http/src/main/resources/logback.xml @@ -0,0 +1,27 @@ + + + + + + + true + + + + + + %d{yyyy-MM-dd HH:mm:ss:SSS} %thread, %level, %logger{70}, "%msg" %replace(%ex){'[\n]+', '\\n'}%nopex%n + + + + + + ${HAYSTACK_LOG_QUEUE_SIZE:-500} + ${HAYSTACK_LOG_DISCARD_THRESHOLD:-0} + + + + + + + diff --git a/collector/http/src/main/scala/com/expedia/www/haystack/http/span/collector/ProjectConfiguration.scala b/collector/http/src/main/scala/com/expedia/www/haystack/http/span/collector/ProjectConfiguration.scala new file mode 100644 index 000000000..a206940c4 --- /dev/null +++ b/collector/http/src/main/scala/com/expedia/www/haystack/http/span/collector/ProjectConfiguration.scala @@ -0,0 +1,65 @@ +/* + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.http.span.collector + +import com.expedia.www.haystack.collector.commons.config.{ConfigurationLoader, ExternalKafkaConfiguration, ExtractorConfiguration, KafkaProduceConfiguration} +import com.expedia.www.haystack.http.span.collector.authenticator.{Authenticator, NoopAuthenticator} +import com.expedia.www.haystack.span.decorators.plugin.config.Plugin +import com.typesafe.config.Config + +import scala.reflect.ClassTag + +case class HttpConfiguration(host: String = "127.0.0.1", port: Int = 8080, authenticator: Authenticator = NoopAuthenticator) + +object ProjectConfiguration { + val config: Config = ConfigurationLoader.loadConfigFileWithEnvOverrides() + + def kafkaProducerConfig(): KafkaProduceConfiguration = ConfigurationLoader.kafkaProducerConfig(config) + def extractorConfig(): ExtractorConfiguration = ConfigurationLoader.extractorConfiguration(config) + def externalKafkaConfig(): List[ExternalKafkaConfiguration] = ConfigurationLoader.externalKafkaConfiguration(config) + def additionalTagConfig(): Map[String, String] = ConfigurationLoader.additionalTagsConfiguration(config) + def pluginConfiguration(): Plugin = ConfigurationLoader.pluginConfigurations(config) + + lazy val httpConfig: HttpConfiguration = { + val authenticator = if(config.hasPath("http.authenticator")) { + toInstance[Authenticator](config.getString("http.authenticator")) + } else { + NoopAuthenticator + } + + // initialize the + authenticator.init(config) + + HttpConfiguration(config.getString("http.host"), config.getInt("http.port"), authenticator) + } + + private def toInstance[T](className: String)(implicit ct: ClassTag[T]): T = { + val c = Class.forName(className) + if (c == null) { + throw new RuntimeException(s"No class found with name $className") + } else { + val o = c.newInstance() + val baseClass = ct.runtimeClass + + if (!baseClass.isInstance(o)) { + throw new RuntimeException(s"${c.getName} is not an instance of ${baseClass.getName}") + } + o.asInstanceOf[T] + } + } +} diff --git a/collector/http/src/main/scala/com/expedia/www/haystack/http/span/collector/WebServer.scala b/collector/http/src/main/scala/com/expedia/www/haystack/http/span/collector/WebServer.scala new file mode 100644 index 000000000..04ea5d0d2 --- /dev/null +++ b/collector/http/src/main/scala/com/expedia/www/haystack/http/span/collector/WebServer.scala @@ -0,0 +1,133 @@ +/* + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.http.span.collector + +import akka.actor.ActorSystem +import akka.http.scaladsl.Http +import akka.http.scaladsl.model._ +import akka.http.scaladsl.server.Directives._ +import akka.http.scaladsl.server.{AuthorizationFailedRejection, Route} +import akka.stream.ActorMaterializer +import akka.util.ByteString +import com.codahale.metrics.JmxReporter +import com.expedia.www.haystack.collector.commons.sink.kafka.KafkaRecordSink +import com.expedia.www.haystack.collector.commons.{MetricsSupport, ProtoSpanExtractor, SpanDecoratorFactory} +import com.expedia.www.haystack.http.span.collector.json.Span +import org.json4s.DefaultFormats +import org.json4s.jackson.Serialization +import org.slf4j.LoggerFactory + +import scala.concurrent.duration._ +import scala.concurrent.{Await, ExecutionContextExecutor, Future} +import scala.sys._ +import scala.util.Try + +object WebServer extends App with MetricsSupport { + val LOGGER = LoggerFactory.getLogger(WebServer.getClass) + + // setup kafka sink + private val kafkaSink = new KafkaRecordSink(ProjectConfiguration.kafkaProducerConfig(), ProjectConfiguration.externalKafkaConfig()) + private val kvExtractor = new ProtoSpanExtractor(ProjectConfiguration.extractorConfig(), + LoggerFactory.getLogger(classOf[ProtoSpanExtractor]), + SpanDecoratorFactory.get(ProjectConfiguration.pluginConfiguration(), ProjectConfiguration.additionalTagConfig(), LOGGER)) + + private val http = ProjectConfiguration.httpConfig + + // setup actor system + implicit val system: ActorSystem = ActorSystem("span-collector", ProjectConfiguration.config) + implicit val materializer: ActorMaterializer = ActorMaterializer() + implicit val executionContext: ExecutionContextExecutor = system.dispatcher + implicit val formats: DefaultFormats.type = DefaultFormats + + // start jmx reporter + private val jmxReporter = JmxReporter.forRegistry(metricRegistry).build() + jmxReporter.start() + + // start http server on given host and port + val bindingFuture = Http(system).bindAndHandle(routes(), http.host, http.port) + LOGGER.info(s"Server is now listening at http://${http.host}:${http.port}") + + addShutdownHook { shutdownHook() } + + def routes(): Route = { + // build the routes + path("span") { + post { + extractRequest { + req => + if (http.authenticator(req)) { + val spanBytes = req.entity + .dataBytes + .runFold(ByteString.empty) { case (acc, b) => acc ++ b } + .map(_.compact.toArray[Byte]) + + req.entity.contentType match { + case ContentTypes.`application/json` => + complete { + processJsonSpan(spanBytes) + } + case _ => + complete { + processProtoSpan(spanBytes) + } + } + } else { + reject(AuthorizationFailedRejection) + } + } + } + } ~ + path("isActive") { + get { + complete(HttpEntity(ContentTypes.`text/plain(UTF-8)`, "ACTIVE")) + } + } + } + + def processProtoSpan(spanBytes: Future[Array[Byte]]): Future[StatusCode] = { + spanBytes + .map(kvExtractor.extractKeyValuePairs) + .map(kvPairs => { + kvPairs foreach { kv => kafkaSink.toAsync(kv) } + StatusCode.int2StatusCode(StatusCodes.Accepted.intValue) + }) + } + + def processJsonSpan(dataBytes: Future[Array[Byte]]): Future[StatusCode] = { + processProtoSpan( + dataBytes + .map(bytes => Serialization.read[Span](new String(bytes))) + .map(span => span.toProto)) + } + + def shutdownHook(): Unit = { + LOGGER.info("Terminating Server ...") + bindingFuture + .flatMap(_.unbind()) + .onComplete { _ => close() } + Await.result(system.whenTerminated, 30.seconds) + } + + def close(): Unit = { + Try(kafkaSink.close()) + Try(http.authenticator.close()) + materializer.shutdown() + system.terminate() + jmxReporter.close() + } +} diff --git a/collector/http/src/main/scala/com/expedia/www/haystack/http/span/collector/authenticator/Authenticator.scala b/collector/http/src/main/scala/com/expedia/www/haystack/http/span/collector/authenticator/Authenticator.scala new file mode 100644 index 000000000..1f3ea6c02 --- /dev/null +++ b/collector/http/src/main/scala/com/expedia/www/haystack/http/span/collector/authenticator/Authenticator.scala @@ -0,0 +1,29 @@ +/* + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.http.span.collector.authenticator + +import java.io.Closeable + +import akka.http.scaladsl.model.HttpRequest +import com.typesafe.config.Config + +trait Authenticator extends Closeable { + def apply(req: HttpRequest): Boolean + + def init(config: Config): Unit +} diff --git a/collector/http/src/main/scala/com/expedia/www/haystack/http/span/collector/authenticator/NoopAuthenticator.scala b/collector/http/src/main/scala/com/expedia/www/haystack/http/span/collector/authenticator/NoopAuthenticator.scala new file mode 100644 index 000000000..11aca77a3 --- /dev/null +++ b/collector/http/src/main/scala/com/expedia/www/haystack/http/span/collector/authenticator/NoopAuthenticator.scala @@ -0,0 +1,29 @@ +/* + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.http.span.collector.authenticator + +import akka.http.scaladsl.model.HttpRequest +import com.typesafe.config.Config + +object NoopAuthenticator extends Authenticator { + override def apply(req: HttpRequest): Boolean = true + + override def init(config: Config): Unit = () + + override def close(): Unit = () +} diff --git a/collector/http/src/main/scala/com/expedia/www/haystack/http/span/collector/json/Span.scala b/collector/http/src/main/scala/com/expedia/www/haystack/http/span/collector/json/Span.scala new file mode 100644 index 000000000..0c6f9b0ef --- /dev/null +++ b/collector/http/src/main/scala/com/expedia/www/haystack/http/span/collector/json/Span.scala @@ -0,0 +1,71 @@ +/* + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.http.span.collector.json + +import com.expedia.open.tracing.Tag.TagType +import com.expedia.open.tracing.{Log => PLog, Span => PSpan, Tag => PTag} +case class Tag(key: String, value: Any) +case class Log(timestamp: Long, fields: List[Tag]) + +case class Span(traceId: String, + spanId: String, + parentSpanId: Option[String], + serviceName: String, + operationName: String, + startTime: Long, + duration: Int, + tags: List[Tag], + logs: List[Log]) { + def toProto: Array[Byte] = { + val span = PSpan.newBuilder() + .setTraceId(traceId) + .setSpanId(spanId) + .setServiceName(serviceName) + .setOperationName(operationName) + .setParentSpanId(parentSpanId.getOrElse("")) + .setStartTime(startTime) + .setDuration(duration) + + tags.map(tag => span.addTags(createProtoTag(tag))) + logs.map(log => { + val l = PLog.newBuilder().setTimestamp(log.timestamp) + log.fields.foreach(tag => l.addFields(createProtoTag(tag))) + span.addLogs(l) + }) + + span.build().toByteArray + } + + private def createProtoTag(tag: Tag): PTag.Builder = { + tag.value match { + case _: Int => + PTag.newBuilder().setKey(tag.key).setVLong(tag.value.asInstanceOf[Int]).setType(TagType.LONG) + case _: BigInt => + PTag.newBuilder().setKey(tag.key).setVLong(tag.value.asInstanceOf[BigInt].longValue()).setType(TagType.LONG) + case _: Long => + PTag.newBuilder().setKey(tag.key).setVLong(tag.value.asInstanceOf[Long]).setType(TagType.LONG) + case _: Double => + PTag.newBuilder().setKey(tag.key).setVDouble(tag.value.asInstanceOf[Double]).setType(TagType.DOUBLE) + case _: Float => + PTag.newBuilder().setKey(tag.key).setVDouble(tag.value.asInstanceOf[Float].toDouble).setType(TagType.DOUBLE) + case _: Boolean => + PTag.newBuilder().setKey(tag.key).setVBool(tag.value.asInstanceOf[Boolean]).setType(TagType.BOOL) + case _ => PTag.newBuilder().setKey(tag.key).setVStr(tag.value.toString).setType(TagType.STRING) + } + } +} diff --git a/collector/http/src/test/resources/logback-test.xml b/collector/http/src/test/resources/logback-test.xml new file mode 100644 index 000000000..8f39588cc --- /dev/null +++ b/collector/http/src/test/resources/logback-test.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/collector/http/src/test/scala/com/expedia/www/haystack/http/span/collector/integration/HttpProducer.scala b/collector/http/src/test/scala/com/expedia/www/haystack/http/span/collector/integration/HttpProducer.scala new file mode 100644 index 000000000..e2acad694 --- /dev/null +++ b/collector/http/src/test/scala/com/expedia/www/haystack/http/span/collector/integration/HttpProducer.scala @@ -0,0 +1,52 @@ +/* + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.http.span.collector.integration + +import akka.actor.ActorSystem +import akka.http.scaladsl.Http +import akka.http.scaladsl.model._ +import akka.stream.ActorMaterializer + +import scala.concurrent.Await +import scala.concurrent.duration._ +import scala.util.{Failure, Success} + +trait HttpProducer { + protected implicit val system = ActorSystem() + protected implicit val materializer = ActorMaterializer() + protected implicit val executionContext = system.dispatcher + private val http = Http(system) + + def postHttp(records: List[Array[Byte]], contentType: ContentType = ContentTypes.`application/octet-stream`): Unit = { + records foreach { record => + val entity = HttpEntity(contentType, record) + val request = HttpRequest(method = HttpMethods.POST, uri = "http://localhost:8080/span", entity = entity) + http.singleRequest(request) onComplete { + case Failure(ex) => println(s"Failed to post, reason: $ex") + case Success(response) => println(s"Server responded with $response") + } + } + } + + def isActiveHttpCall(): String = { + val responseFuture = http.singleRequest(HttpRequest(uri = "http://localhost:8080/isActive")) + .flatMap(response => response.entity.toStrict(5.seconds).map(_.data)) + .map(p => new String(p.compact.toArray[Byte])) + Await.result(responseFuture, 5.seconds) + } +} diff --git a/collector/http/src/test/scala/com/expedia/www/haystack/http/span/collector/integration/IntegrationTestSpec.scala b/collector/http/src/test/scala/com/expedia/www/haystack/http/span/collector/integration/IntegrationTestSpec.scala new file mode 100644 index 000000000..c63423f79 --- /dev/null +++ b/collector/http/src/test/scala/com/expedia/www/haystack/http/span/collector/integration/IntegrationTestSpec.scala @@ -0,0 +1,39 @@ +/* + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.http.span.collector.integration + +import java.util.concurrent.Executors + +import com.expedia.www.haystack.http.span.collector.WebServer +import org.scalatest._ + +class IntegrationTestSpec extends WordSpec with GivenWhenThen with Matchers with HttpProducer with LocalKafkaConsumer + with OptionValues with BeforeAndAfterAll { + + private val executor = Executors.newSingleThreadExecutor() + + override def beforeAll(): Unit = { + executor.submit(new Runnable { + override def run(): Unit = WebServer.main(null) + }) + // wait for few sec to let app start + Thread.sleep(15000) + } + + override def afterAll(): Unit = { } +} \ No newline at end of file diff --git a/collector/http/src/test/scala/com/expedia/www/haystack/http/span/collector/integration/LocalKafkaConsumer.scala b/collector/http/src/test/scala/com/expedia/www/haystack/http/span/collector/integration/LocalKafkaConsumer.scala new file mode 100644 index 000000000..0b37faf11 --- /dev/null +++ b/collector/http/src/test/scala/com/expedia/www/haystack/http/span/collector/integration/LocalKafkaConsumer.scala @@ -0,0 +1,72 @@ +/* + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.http.span.collector.integration + +import java.util.Properties + +import com.expedia.www.haystack.http.span.collector.integration.config.TestConfiguration +import org.apache.kafka.clients.consumer.internals.NoOpConsumerRebalanceListener +import org.apache.kafka.clients.consumer.{ConsumerConfig, KafkaConsumer} +import org.apache.kafka.common.serialization.ByteArrayDeserializer + +import scala.collection.JavaConversions._ +import scala.collection.JavaConverters._ +import scala.collection.mutable +import scala.concurrent.duration._ + +trait LocalKafkaConsumer { + + private val kafkaConsumer = { + val consumerProperties = new Properties() + consumerProperties.setProperty(ConsumerConfig.GROUP_ID_CONFIG, "http-to-kafka-test") + consumerProperties.setProperty(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, TestConfiguration.remoteKafkaHost + ":" + TestConfiguration.kafkaPort) + consumerProperties.setProperty(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, classOf[ByteArrayDeserializer].getCanonicalName) + consumerProperties.setProperty(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, classOf[ByteArrayDeserializer].getCanonicalName) + new KafkaConsumer[Array[Byte], Array[Byte]](consumerProperties) + } + + kafkaConsumer.subscribe(List(TestConfiguration.kafkaStreamName).asJava, new NoOpConsumerRebalanceListener()) + + def readRecordsFromKafka(minExpectedCount: Int, maxWait: FiniteDuration): List[Array[Byte]] = { + val records = mutable.ListBuffer[Array[Byte]]() + var received: Int = 0 + + var waitTimeLeft = maxWait.toMillis + var done = true + while (done) { + kafkaConsumer.poll(250).records(TestConfiguration.kafkaStreamName).map(rec => { + received += 1 + records += rec.value() + }) + if(received < minExpectedCount && waitTimeLeft > 0) { + Thread.sleep(1000) + waitTimeLeft -= 1000 + } else { + done = false + } + } + + if(records.size < minExpectedCount) throw new RuntimeException("Fail to read the expected records from kafka") + + records.toList + } + + def shutdownKafkaConsumer(): Unit = { + if(kafkaConsumer != null) kafkaConsumer.close() + } +} diff --git a/collector/http/src/test/scala/com/expedia/www/haystack/http/span/collector/integration/config/TestConfiguration.scala b/collector/http/src/test/scala/com/expedia/www/haystack/http/span/collector/integration/config/TestConfiguration.scala new file mode 100644 index 000000000..099a6605d --- /dev/null +++ b/collector/http/src/test/scala/com/expedia/www/haystack/http/span/collector/integration/config/TestConfiguration.scala @@ -0,0 +1,24 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.http.span.collector.integration.config + +object TestConfiguration { + val remoteKafkaHost = "kafkasvc" + val kafkaPort = 9092 + val kafkaStreamName = "proto-spans" +} diff --git a/collector/http/src/test/scala/com/expedia/www/haystack/http/span/collector/integration/tests/HttpSpanCollectorSpec.scala b/collector/http/src/test/scala/com/expedia/www/haystack/http/span/collector/integration/tests/HttpSpanCollectorSpec.scala new file mode 100644 index 000000000..e39f59a3a --- /dev/null +++ b/collector/http/src/test/scala/com/expedia/www/haystack/http/span/collector/integration/tests/HttpSpanCollectorSpec.scala @@ -0,0 +1,139 @@ +/* + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.http.span.collector.integration.tests + +import akka.http.scaladsl.model.ContentTypes +import com.expedia.open.tracing.Tag.TagType +import com.expedia.open.tracing.{Span, Tag} +import com.expedia.www.haystack.http.span.collector.integration.IntegrationTestSpec +import com.expedia.www.haystack.http.span.collector.json.{Log => JLog, Span => JSpan, Tag => JTag} +import org.json4s.DefaultFormats +import org.json4s.jackson.Serialization + +import scala.collection.JavaConverters._ +import scala.concurrent.duration._ + +class HttpSpanCollectorSpec extends IntegrationTestSpec { + + implicit val formats = DefaultFormats + private val StartTimeMicros = System.currentTimeMillis() * 1000 + private val DurationMicros = 42 + + "Http span collector" should { + + // this test is primarily to work around issue with Kafka docker image + // it fails for first put for some reasons + "connect with http and kafka" in { + + Given("a valid span") + val spanBytes = Span.newBuilder().setTraceId("traceid").setSpanId("span-id-1").build().toByteArray + + When("the span is sent over http") + postHttp(List(spanBytes, spanBytes)) + + Then("it should be pushed to kafka") + readRecordsFromKafka(0, 1.second).headOption + } + + "read valid proto spans from kafka if produced proto spans on http" in { + + Given("valid proto spans") + val span_1 = Span.newBuilder().setTraceId("trace-id-1").setSpanId("span-id-1").setOperationName("operation") + .setServiceName("service").setStartTime(StartTimeMicros).setDuration(DurationMicros).build().toByteArray + val span_2 = Span.newBuilder().setTraceId("trace-id-1").setSpanId("span-id-2").setOperationName("operation") + .setServiceName("service").setStartTime(StartTimeMicros).setDuration(DurationMicros).build().toByteArray + val span_3 = Span.newBuilder().setTraceId("trace-id-2").setSpanId("span-id-3").setOperationName("operation") + .setServiceName("service").setStartTime(StartTimeMicros).setDuration(DurationMicros).build().toByteArray + val span_4 = Span.newBuilder().setTraceId("trace-id-2").setSpanId("span-id-4").setOperationName("operation") + .setServiceName("service").setStartTime(StartTimeMicros).setDuration(DurationMicros).build().toByteArray + + When("the span is sent to http span collector") + postHttp(List(span_1, span_2, span_3, span_4)) + + Then("it should be pushed to kafka with partition key as its trace id") + val records = readRecordsFromKafka(4, 5.seconds) + records should not be empty + val spans = records.map(Span.parseFrom) + spans.map(_.getTraceId).toSet should contain allOf("trace-id-1", "trace-id-2") + spans.map(_.getSpanId) should contain allOf("span-id-1", "span-id-2", "span-id-3", "span-id-4") + } + + + "read valid proto spans from kafka if produced json spans" in { + Given("valid json spans") + val tags = List(JTag("number", 100), JTag("some-string", "str"), JTag("some-boolean", true), JTag("some-double", 10.5)) + val logs = List(JLog(StartTimeMicros, List(JTag("errorcode", 1)))) + val spanJsonBytesList = List( + JSpan("trace-id-1", "span-id-1", None, "service1", "operation1", StartTimeMicros, DurationMicros, tags, Nil), + JSpan("trace-id-2", "span-id-2", Some("parent-span-id-2"), "service2", "operation2", StartTimeMicros, DurationMicros, tags, logs), + JSpan("trace-id-3", "span-id-3", None, "service3", "operation3", StartTimeMicros, DurationMicros, tags, Nil), + JSpan("trace-id-1", "span-id-4", None, "service1", "operation1", StartTimeMicros, DurationMicros, Nil, Nil) + ).map(jsonSpan => Serialization.write(jsonSpan).getBytes("utf-8")) + + When("the span is sent to http span collector") + postHttp(spanJsonBytesList, ContentTypes.`application/json`) + + Then("it should be pushed to kafka with partition key as its trace id") + val records = readRecordsFromKafka(4, 5.seconds) + records should not be empty + val spans = records.map(Span.parseFrom) + spans.map(_.getServiceName).toSet should contain allOf("service1", "service2", "service3") + spans.map(_.getOperationName).toSet should contain allOf("operation1", "operation2", "operation3") + spans.map(_.getStartTime).toSet.head shouldBe StartTimeMicros + spans.map(_.getDuration).toSet.head shouldBe DurationMicros + spans.map(_.getTraceId).toSet should contain allOf("trace-id-1", "trace-id-2", "trace-id-3") + spans.map(_.getSpanId) should contain allOf("span-id-1", "span-id-2", "span-id-3", "span-id-4") + + val span2 = spans.find(_.getTraceId == "trace-id-2").get + span2.getParentSpanId shouldBe "parent-span-id-2" + span2.getTagsCount shouldBe tags.size + + tags.foreach { jTag => + val tag = span2.getTagsList.asScala.find(_.getKey.equalsIgnoreCase(jTag.key)).get + protoTagValue(tag) shouldBe jTag.value + } + + span2.getLogsCount shouldBe logs.size + logs foreach { jLog => + val logFields = span2.getLogsList.asScala.find(_.getTimestamp == jLog.timestamp).get.getFieldsList.asScala + jLog.fields.foreach { jTag => + val tag = logFields.find(_.getKey.equalsIgnoreCase(jTag.key)).get + protoTagValue(tag) shouldBe jTag.value + } + } + } + + "isActive endpoint should work" in { + Given("an empty http request") + When("/isActive endpoint is called") + val response = isActiveHttpCall() + Then("response should be active") + response shouldBe "ACTIVE" + } + } + + private def protoTagValue(tag: Tag): Any = { + tag.getType match { + case TagType.STRING => tag.getVStr + case TagType.BOOL => tag.getVBool + case TagType.LONG => tag.getVLong + case TagType.DOUBLE => tag.getVDouble + case _ => fail("fail to find the proto tag value") + } + } +} diff --git a/collector/kinesis/Makefile b/collector/kinesis/Makefile new file mode 100644 index 000000000..8083d14c6 --- /dev/null +++ b/collector/kinesis/Makefile @@ -0,0 +1,50 @@ +.PHONY: docker_build prepare_integration_test_env integration_test release + +export DOCKER_ORG := expediadotcom +export DOCKER_IMAGE_NAME := haystack-kinesis-span-collector +PWD := $(shell pwd) + +docker_build: + docker build -t $(DOCKER_IMAGE_NAME) -f build/docker/Dockerfile . + +prepare_integration_test_env: docker_build + #copy plugin jars to test + mvn -f ${PWD}/../ clean package -pl sample-span-decorator -am + mkdir -p ${PWD}/plugins/decorators + cp ${PWD}/../sample-span-decorator/target/sample-span-decorator.jar ${PWD}/plugins/decorators/. + + docker-compose -f build/integration-tests/docker-compose.yml -p sandbox up -d + + # kafka sometimes take time to start + sleep 30 + + # create the stream and dynamodb table + docker run \ + -it --network=sandbox_default \ + -e "AWS_CBOR_DISABLE=1" \ + -v $(PWD)/build/integration-tests/scripts:/scripts \ + -w /scripts \ + node:6.11.3 \ + ./setup.sh + +integration_test: prepare_integration_test_env + # run tests in a container so that we can join the docker-compose network and talk to kafka and kinesis + docker run \ + -it \ + --network=sandbox_default \ + -e "AWS_CBOR_DISABLE=1" \ + -e "AWS_ACCESS_KEY=fake" \ + -e "AWS_SECRET_KEY=fake" \ + -v $(PWD)/..:/src \ + -v ~/.m2:/root/.m2 \ + -w /src \ + maven:3.5.0-jdk-8 \ + mvn scoverage:integration-check -pl kinesis -am + + # stop all the containers + docker-compose -f build/integration-tests/docker-compose.yml -p sandbox stop + docker rm $(shell docker ps -a -q) + docker volume rm $(shell docker volume ls -q) + +release: docker_build + ../deployment/scripts/publish-to-docker-hub.sh diff --git a/collector/kinesis/README.md b/collector/kinesis/README.md new file mode 100644 index 000000000..e50780e67 --- /dev/null +++ b/collector/kinesis/README.md @@ -0,0 +1,13 @@ +# haystack-kinesis-span-collector +This haystack component reads the batch of spans from kinesis stream and publish them to kafka topic. +It expects the [Batch](https://github.com/ExpediaDotCom/haystack-idl/blob/master/proto/span.proto) protobuf object in the stream. +It deserializes this proto object and use each span's TraceId as the partition key for writing to the kafka topic. +This component uses the [KCL](http://docs.aws.amazon.com/streams/latest/dev/developing-consumers-with-kcl.html#kinesis-record-processor-overview-kcl) +library to build the pipeline for reading from kinesis stream and writing to kafka using high level kafka consumer api. + +##Required Reading + +In order to understand the haystack, we recommend to read the details of [haystack](https://github.com/ExpediaDotCom/haystack) project. + +##Technical Details +Fill this as we go along.. diff --git a/collector/kinesis/build/docker/Dockerfile b/collector/kinesis/build/docker/Dockerfile new file mode 100644 index 000000000..d918f54cb --- /dev/null +++ b/collector/kinesis/build/docker/Dockerfile @@ -0,0 +1,19 @@ +FROM openjdk:8-jre +MAINTAINER Haystack + +ENV APP_NAME haystack-kinesis-span-collector +ENV APP_HOME /app/bin +ENV JMXTRANS_AGENT jmxtrans-agent-1.2.6 + +RUN mkdir -p ${APP_HOME} + +COPY target/${APP_NAME}.jar ${APP_HOME}/ +COPY build/docker/start-app.sh ${APP_HOME}/ +COPY build/docker/jmxtrans-agent.xml ${APP_HOME}/ + +RUN chmod +x ${APP_HOME}/start-app.sh +ADD https://github.com/jmxtrans/jmxtrans-agent/releases/download/${JMXTRANS_AGENT}/${JMXTRANS_AGENT}.jar ${APP_HOME}/ + +WORKDIR ${APP_HOME} + +ENTRYPOINT ["./start-app.sh"] diff --git a/collector/kinesis/build/docker/jmxtrans-agent.xml b/collector/kinesis/build/docker/jmxtrans-agent.xml new file mode 100644 index 000000000..a7af20fce --- /dev/null +++ b/collector/kinesis/build/docker/jmxtrans-agent.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + ${HAYSTACK_GRAPHITE_HOST:monitoring-influxdb-graphite.kube-system.svc} + ${HAYSTACK_GRAPHITE_PORT:2003} + ${HAYSTACK_GRAPHITE_ENABLED:true} + ${HAYSTACK_GRAPHITE_PREFIX:haystack.collector.kinesis}.#hostname#. + + 60 + diff --git a/collector/kinesis/build/docker/start-app.sh b/collector/kinesis/build/docker/start-app.sh new file mode 100755 index 000000000..9b7a9a415 --- /dev/null +++ b/collector/kinesis/build/docker/start-app.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +[ -z "$JAVA_XMS" ] && JAVA_XMS=1024m +[ -z "$JAVA_XMX" ] && JAVA_XMX=1024m +[ -z "$JAVA_GC_OPTS" ] && JAVA_GC_OPTS="-XX:+UseG1GC -XX:+ExitOnOutOfMemoryError" + +set -e +JAVA_OPTS="${JAVA_OPTS} \ +-javaagent:${APP_HOME}/${JMXTRANS_AGENT}.jar=${APP_HOME}/jmxtrans-agent.xml \ +${JAVA_GC_OPTS} \ +-Xmx${JAVA_XMX} \ +-Xms${JAVA_XMS} \ +-Dcom.sun.management.jmxremote.authenticate=false \ +-Dcom.sun.management.jmxremote.ssl=false \ +-Dcom.sun.management.jmxremote.port=1098 \ +-Dcom.sun.management.jmxremote.rmi.port=1098 \ +-Dapplication.name=${APP_NAME} \ +-Dapplication.home=${APP_HOME}" + +exec java ${JAVA_OPTS} -jar "${APP_HOME}/${APP_NAME}.jar" diff --git a/collector/kinesis/build/integration-tests/app-integration-test.conf b/collector/kinesis/build/integration-tests/app-integration-test.conf new file mode 100644 index 000000000..b910d0ba7 --- /dev/null +++ b/collector/kinesis/build/integration-tests/app-integration-test.conf @@ -0,0 +1,40 @@ +kafka { + producer { + topic = "proto-spans" + props { + bootstrap.servers = "kafkasvc:9092" + } + } +} + +extractor { + output.format = "proto" +} + +kinesis { + endpoint = "http://localstack:4568" + app.group.name = "haystack-kinesis-proto-span-collector" + + dynamodb.endpoint = "http://localstack:4569" + + stream { + name = "haystack-proto-spans" + position = "LATEST" + } + + checkpoint { + interval.ms = 15000 + retries = 50 + retry.interval.ms = 250 + } + + task.backoff.ms = 200 + max.records.read = 2000 + idle.time.between.reads.ms = 500 + shard.sync.interval.ms = 30000 + + metrics { + level = "NONE" + buffer.time.ms = 15000 + } +} \ No newline at end of file diff --git a/collector/kinesis/build/integration-tests/docker-compose.yml b/collector/kinesis/build/integration-tests/docker-compose.yml new file mode 100644 index 000000000..d6f78d00b --- /dev/null +++ b/collector/kinesis/build/integration-tests/docker-compose.yml @@ -0,0 +1,34 @@ +version: '3' +services: + zookeeper: + image: wurstmeister/zookeeper + ports: + - "2181" + kafkasvc: + image: wurstmeister/kafka:0.10.2.1 + ports: + - "9092" + depends_on: + - zookeeper + links: + - zookeeper:zk + environment: + KAFKA_ADVERTISED_HOST_NAME: kafkasvc + KAFKA_ADVERTISED_PORT: 9092 + KAFKA_ZOOKEEPER_CONNECT: zk:2181 + volumes: + - /var/run/docker.sock:/var/run/docker.sock + localstack: + image: localstack/localstack:0.11.0 + ports: + - '4563-4599:4563-4599' + - '8055:8080' + environment: + - SERVICES=kinesis,dynamodb + - KINESIS_SHARD_LIMIT=1 + - KINESIS_LATENCY=0 + - DATA_DIR=/tmp/localstack/data + - START_WEB=0 + volumes: + - './.localstack:/tmp/localstack' + - '/var/run/docker.sock:/var/run/docker.sock' diff --git a/collector/kinesis/build/integration-tests/scripts/create-dynamo-table.js b/collector/kinesis/build/integration-tests/scripts/create-dynamo-table.js new file mode 100644 index 000000000..80b937462 --- /dev/null +++ b/collector/kinesis/build/integration-tests/scripts/create-dynamo-table.js @@ -0,0 +1,37 @@ +var AWS = require('aws-sdk'); + +var config = { + "accessKeyId": "FAKE", + "secretAccessKey": "FAKE", + "region": "us-east-1", + "dynamoEndpoint": "http://localstack:4569", + "tableName": "haystack-kinesis-proto-span-collector", + "ShardCount": 1 +}; + +var dynamodb = new AWS.DynamoDB({ endpoint: new AWS.Endpoint(config.dynamoEndpoint), + accessKeyId: config.accessKeyId, + secretAccessKey: config.secretAccessKey, + region: config.region}); + +var params = { + TableName : config.tableName, + KeySchema: [ + { AttributeName: "leaseKey", KeyType: "HASH"} //Partition key + ], + AttributeDefinitions: [ + { AttributeName: "leaseKey", AttributeType: "S" } + ], + ProvisionedThroughput: { + ReadCapacityUnits: 10, + WriteCapacityUnits: 10 + } +}; + +dynamodb.createTable(params, function(err, data) { + if (err) { + console.error("Unable to create table. Error JSON:", JSON.stringify(err, null, 2)); + } else { + console.log("Created table. Table description JSON:", JSON.stringify(data, null, 2)); + } +}); \ No newline at end of file diff --git a/collector/kinesis/build/integration-tests/scripts/create-kinesis-stream.js b/collector/kinesis/build/integration-tests/scripts/create-kinesis-stream.js new file mode 100644 index 000000000..779e4752e --- /dev/null +++ b/collector/kinesis/build/integration-tests/scripts/create-kinesis-stream.js @@ -0,0 +1,41 @@ +var AWS = require('aws-sdk'); + +var config = { + "accessKeyId": "FAKE", + "secretAccessKey": "FAKE", + "region": "us-east-1", + "kinesisEndpoint": "http://localstack:4568", + "kinesisPort": 4568, + "StreamName": "haystack-proto-spans", + "ShardCount": 1 +}; + +var kinesis = new AWS.Kinesis({ + endpoint: config.kinesisEndpoint, + accessKeyId: config.accessKeyId, + secretAccessKey: config.secretAccessKey, + region: config.region +}); + +AWS.config.update({}); + + +kinesis.listStreams({ }, function(err, data) { + if (err) throw err; + + console.log('Stream ready: ', data); + + if(data.StreamNames.includes(config.StreamName)) { + console.log('Stream already exists'); + } else { + kinesis.createStream({ StreamName: config.StreamName, ShardCount: config.ShardCount }, function (err) { + if (err) throw err; + + kinesis.describeStream({ StreamName: config.StreamName }, function(err, data) { + if (err) throw err; + console.log('Stream ready: ', data); + }); + }); + } +}); + diff --git a/collector/kinesis/build/integration-tests/scripts/package.json b/collector/kinesis/build/integration-tests/scripts/package.json new file mode 100644 index 000000000..53856247d --- /dev/null +++ b/collector/kinesis/build/integration-tests/scripts/package.json @@ -0,0 +1,8 @@ +{ + "name": "kinesis-stream-bootstrap", + "version": "1.0.0", + "private": true, + "dependencies": { + "aws-sdk": "2.7.9" + } +} diff --git a/collector/kinesis/build/integration-tests/scripts/setup.sh b/collector/kinesis/build/integration-tests/scripts/setup.sh new file mode 100755 index 000000000..71f39af14 --- /dev/null +++ b/collector/kinesis/build/integration-tests/scripts/setup.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +npm install +node create-kinesis-stream.js +node create-dynamo-table.js \ No newline at end of file diff --git a/collector/kinesis/pom.xml b/collector/kinesis/pom.xml new file mode 100644 index 000000000..ae1c213a9 --- /dev/null +++ b/collector/kinesis/pom.xml @@ -0,0 +1,176 @@ + + + + + haystack-collector + com.expedia.www + 1.0-SNAPSHOT + + + 4.0.0 + haystack-kinesis-span-collector + 1.0-SNAPSHOT + jar + + + 1.13.2 + 0.12.3 + 1.11.670 + com.expedia.www.haystack.kinesis.span.collector.App + ${project.artifactId}-${project.version} + + + + + com.expedia.www + haystack-collector-commons + ${project.version} + + + + com.expedia.www + haystack-span-decorators + ${project.version} + + + + org.apache.httpcomponents + httpclient + + + + + com.amazonaws + amazon-kinesis-client + ${kinesis.client.version} + + + + org.slf4j + jcl-over-slf4j + + + + com.amazonaws + aws-java-sdk-sts + ${aws-sdk.version} + + + + com.expedia.www + haystack-logback-metrics-appender + + + + + com.amazonaws + amazon-kinesis-producer + ${kinesis.producer.version} + test + + + com.fasterxml.jackson.dataformat + jackson-dataformat-cbor + + + com.amazonaws + aws-java-sdk-core + + + + + + + ${finalName} + + + org.scalatest + scalatest-maven-plugin + + + test + + test + + + + haystack-test + + com.expedia.www.haystack.kinesis.span.collector.unit + + + + integration-test + integration-test + + test + + + com.expedia.www.haystack.kinesis.span.collector.integration + + /src/kinesis/build/integration-tests/app-integration-test.conf + fake + 1 + fake + + + + + + + + org.apache.maven.plugins + maven-shade-plugin + + true + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + + + + package + + shade + + + + + reference.conf + + + ${mainClass} + + + + + + + + + net.alchim31.maven + scala-maven-plugin + + + + org.scalastyle + scalastyle-maven-plugin + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + + + diff --git a/collector/kinesis/src/main/resources/config/base.conf b/collector/kinesis/src/main/resources/config/base.conf new file mode 100644 index 000000000..5e8171e5e --- /dev/null +++ b/collector/kinesis/src/main/resources/config/base.conf @@ -0,0 +1,70 @@ +health.status.path = "/app/isHealthy" + +haystack.graphite.host = "monitoring-influxdb-graphite.kube-system.svc" + +kafka { + producer { + topic = "proto-spans" + props { + bootstrap.servers = "kafkasvc:9092" + } + } +} + +extractor { + output.format = "proto" + + spans.validation { + + # Validate size of span. Truncate span tags when size exceeds spcified limit. + # enable: true/false + # log.only: if enabled, only logs such spans but doesn't truncate the tags + # max.size.limit: maximum size allowed + # message.tag.key: this tag key will be added when tags are truncated + # message.tag.value: value of the above tag key indicating the truncation + # skip.tags: truncate all span tags except these + # skip.services: truncate span tags for all services except these + max.size { + enable = "false" + log.only = "false" + max.size.limit = 5000 // in bytes + message.tag.key = "X-HAYSTACK-SPAN-INFO" + message.tag.value = "Tags are truncated. REASON: Span Size Limit Exceeded. Please contact Haystack for more details" + skip.tags = ["error"] + skip.services = [] + } + } +} + +kinesis { + #optional, uncomment following if you want to connect to kinesis using sts role arn + #sts.role.arn = "provide the arn here" + + aws.region = "us-west-2" + app.group.name = "haystack-kinesis-proto-span-collector" + + # optional, use endpoint property along with aws.region for cross region reading of data. Otherwise, you might see + # latency issues for cross region access + #endpoint = "vpce-xxxxxxxxx-yyyyyy.kinesis.us-east-1.vpce.amazonaws.com" + + stream { + name = "haystack-proto-spans" + position = "LATEST" + } + + checkpoint { + interval.ms = 15000 + retries = 50 + retry.interval.ms = 250 + } + + task.backoff.ms = 200 + max.records.read = 2000 + idle.time.between.reads.ms = 500 + shard.sync.interval.ms = 30000 + + metrics { + level = "NONE" + buffer.time.ms = 15000 + } +} diff --git a/collector/kinesis/src/main/resources/logback.xml b/collector/kinesis/src/main/resources/logback.xml new file mode 100644 index 000000000..971c590ab --- /dev/null +++ b/collector/kinesis/src/main/resources/logback.xml @@ -0,0 +1,27 @@ + + + + + + + true + + + + + + %d{yyyy-MM-dd HH:mm:ss:SSS} %thread, %level, %logger{70}, "%msg" %replace(%ex){'[\n]+', '\\n'}%nopex%n + + + + + + ${HAYSTACK_LOG_QUEUE_SIZE:-500} + ${HAYSTACK_LOG_DISCARD_THRESHOLD:-0} + + + + + + + diff --git a/collector/kinesis/src/main/scala/com/expedia/www/haystack/kinesis/span/collector/App.scala b/collector/kinesis/src/main/scala/com/expedia/www/haystack/kinesis/span/collector/App.scala new file mode 100644 index 000000000..60e725bbc --- /dev/null +++ b/collector/kinesis/src/main/scala/com/expedia/www/haystack/kinesis/span/collector/App.scala @@ -0,0 +1,73 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.kinesis.span.collector + +import com.codahale.metrics.JmxReporter +import com.expedia.www.haystack.collector.commons.MetricsSupport +import com.expedia.www.haystack.collector.commons.health.{HealthController, UpdateHealthStatusFile} +import com.expedia.www.haystack.collector.commons.logger.LoggerUtils +import com.expedia.www.haystack.kinesis.span.collector.config.ProjectConfiguration +import com.expedia.www.haystack.kinesis.span.collector.pipeline.KinesisToKafkaPipeline +import org.slf4j.LoggerFactory + +object App extends MetricsSupport { + private val LOGGER = LoggerFactory.getLogger(App.getClass) + + private var pipeline: KinesisToKafkaPipeline = _ + private var jmxReporter: JmxReporter = _ + + def main(args: Array[String]): Unit = { + startJmxReporter() + + addShutdownHook() + + import ProjectConfiguration._ + try { + + healthStatusFile().foreach(statusFile => HealthController.addListener(new UpdateHealthStatusFile(statusFile))) + + pipeline = new KinesisToKafkaPipeline(kafkaProducerConfig(), externalKafkaConfig(), kinesisConsumerConfig(), extractorConfiguration(), additionalTagConfig(), pluginConfiguration()) + pipeline.run() + } catch { + case ex: Exception => + LOGGER.error("Observed fatal exception while running the app", ex) + shutdown() + System.exit(1) + } + } + + private def addShutdownHook(): Unit = { + Runtime.getRuntime.addShutdownHook(new Thread(new Runnable { + override def run(): Unit = { + LOGGER.info("Shutdown hook is invoked, tearing down the application.") + shutdown() + } + })) + } + + private def shutdown(): Unit = { + if (pipeline != null) pipeline.close() + if (jmxReporter != null) jmxReporter.stop() + LoggerUtils.shutdownLogger() + } + + private def startJmxReporter() = { + jmxReporter = JmxReporter.forRegistry(metricRegistry).build() + jmxReporter.start() + } +} diff --git a/collector/kinesis/src/main/scala/com/expedia/www/haystack/kinesis/span/collector/config/ProjectConfiguration.scala b/collector/kinesis/src/main/scala/com/expedia/www/haystack/kinesis/span/collector/config/ProjectConfiguration.scala new file mode 100644 index 000000000..901d3e9ca --- /dev/null +++ b/collector/kinesis/src/main/scala/com/expedia/www/haystack/kinesis/span/collector/config/ProjectConfiguration.scala @@ -0,0 +1,69 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.kinesis.span.collector.config + +import java.util.concurrent.TimeUnit + +import com.amazonaws.services.kinesis.clientlibrary.lib.worker.InitialPositionInStream +import com.amazonaws.services.kinesis.metrics.interfaces.MetricsLevel +import com.expedia.www.haystack.collector.commons.config.{ConfigurationLoader, ExternalKafkaConfiguration, ExtractorConfiguration, KafkaProduceConfiguration} +import com.expedia.www.haystack.kinesis.span.collector.config.entities.KinesisConsumerConfiguration +import com.expedia.www.haystack.span.decorators.plugin.config.Plugin + +import scala.concurrent.duration._ + +object ProjectConfiguration { + + private val config = ConfigurationLoader.loadConfigFileWithEnvOverrides() + + def healthStatusFile(): Option[String] = if (config.hasPath("health.status.path")) Some(config.getString("health.status.path")) else None + + def kafkaProducerConfig(): KafkaProduceConfiguration = ConfigurationLoader.kafkaProducerConfig(config) + + def extractorConfiguration(): ExtractorConfiguration = ConfigurationLoader.extractorConfiguration(config) + + def kinesisConsumerConfig(): KinesisConsumerConfiguration = { + val kinesis = config.getConfig("kinesis") + val stsRoleArn = if (kinesis.hasPath("sts.role.arn")) Some(kinesis.getString("sts.role.arn")) else None + + KinesisConsumerConfiguration( + awsRegion = kinesis.getString("aws.region"), + stsRoleArn = stsRoleArn, + appGroupName = kinesis.getString("app.group.name"), + streamName = kinesis.getString("stream.name"), + streamPosition = InitialPositionInStream.valueOf(kinesis.getString("stream.position")), + kinesis.getDuration("checkpoint.interval.ms", TimeUnit.MILLISECONDS).millis, + kinesis.getInt("checkpoint.retries"), + kinesis.getDuration("checkpoint.retry.interval.ms", TimeUnit.MILLISECONDS).millis, + kinesisEndpoint = if (kinesis.hasPath("endpoint")) Some(kinesis.getString("endpoint")) else None, + dynamoEndpoint = if (kinesis.hasPath("dynamodb.endpoint")) Some(kinesis.getString("dynamodb.endpoint")) else None, + dynamoTableName = if (kinesis.hasPath("dynamodb.table")) Some(kinesis.getString("dynamodb.table")) else None, + maxRecordsToRead = kinesis.getInt("max.records.read"), + idleTimeBetweenReads = kinesis.getDuration("idle.time.between.reads.ms", TimeUnit.MILLISECONDS).millis, + shardSyncInterval = kinesis.getDuration("shard.sync.interval.ms", TimeUnit.MILLISECONDS).millis, + metricsLevel = MetricsLevel.fromName(kinesis.getString("metrics.level")), + metricsBufferTime = kinesis.getDuration("metrics.buffer.time.ms", TimeUnit.MILLISECONDS).millis, + taskBackoffTime = kinesis.getDuration("task.backoff.ms", TimeUnit.MILLISECONDS).millis) + } + + def externalKafkaConfig(): List[ExternalKafkaConfiguration] = ConfigurationLoader.externalKafkaConfiguration(config) + + def additionalTagConfig(): Map[String, String] = ConfigurationLoader.additionalTagsConfiguration(config) + + def pluginConfiguration(): Plugin = ConfigurationLoader.pluginConfigurations(config) +} diff --git a/collector/kinesis/src/main/scala/com/expedia/www/haystack/kinesis/span/collector/config/entities/KinesisConsumerConfiguration.scala b/collector/kinesis/src/main/scala/com/expedia/www/haystack/kinesis/span/collector/config/entities/KinesisConsumerConfiguration.scala new file mode 100644 index 000000000..97fdc03ac --- /dev/null +++ b/collector/kinesis/src/main/scala/com/expedia/www/haystack/kinesis/span/collector/config/entities/KinesisConsumerConfiguration.scala @@ -0,0 +1,41 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.kinesis.span.collector.config.entities + +import com.amazonaws.services.kinesis.clientlibrary.lib.worker.InitialPositionInStream +import com.amazonaws.services.kinesis.metrics.interfaces.MetricsLevel + +import scala.concurrent.duration.FiniteDuration + +case class KinesisConsumerConfiguration(awsRegion: String, + stsRoleArn: Option[String], + appGroupName: String, + streamName: String, + streamPosition: InitialPositionInStream, + checkpointInterval: FiniteDuration, + checkpointRetries: Int, + checkpointRetryInterval: FiniteDuration, + kinesisEndpoint: Option[String], + dynamoEndpoint: Option[String], + dynamoTableName: Option[String], + maxRecordsToRead: Int, + idleTimeBetweenReads: FiniteDuration, + shardSyncInterval: FiniteDuration, + metricsLevel: MetricsLevel, + metricsBufferTime: FiniteDuration, + taskBackoffTime: FiniteDuration) diff --git a/collector/kinesis/src/main/scala/com/expedia/www/haystack/kinesis/span/collector/kinesis/RecordProcessor.scala b/collector/kinesis/src/main/scala/com/expedia/www/haystack/kinesis/span/collector/kinesis/RecordProcessor.scala new file mode 100644 index 000000000..14e0e8c01 --- /dev/null +++ b/collector/kinesis/src/main/scala/com/expedia/www/haystack/kinesis/span/collector/kinesis/RecordProcessor.scala @@ -0,0 +1,137 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.kinesis.span.collector.kinesis + +import java.util.Date + +import com.amazonaws.services.kinesis.clientlibrary.exceptions.ShutdownException +import com.amazonaws.services.kinesis.clientlibrary.interfaces.IRecordProcessorCheckpointer +import com.amazonaws.services.kinesis.clientlibrary.interfaces.v2.IRecordProcessor +import com.amazonaws.services.kinesis.clientlibrary.lib.worker.ShutdownReason +import com.amazonaws.services.kinesis.clientlibrary.types.{InitializationInput, ProcessRecordsInput, ShutdownInput} +import com.expedia.www.haystack.collector.commons.MetricsSupport +import com.expedia.www.haystack.collector.commons.health.HealthController +import com.expedia.www.haystack.collector.commons.record.{KeyValueExtractor, KeyValuePair} +import com.expedia.www.haystack.collector.commons.sink.RecordSink +import com.expedia.www.haystack.kinesis.span.collector.config.entities.KinesisConsumerConfiguration +import com.expedia.www.haystack.kinesis.span.collector.metrics.AppMetricNames +import org.slf4j.LoggerFactory + +import scala.annotation.tailrec +import scala.collection.JavaConversions._ +import scala.concurrent.duration.FiniteDuration +import scala.util.{Failure, Success, Try} + +object RecordProcessor extends MetricsSupport { + private val LOGGER = LoggerFactory.getLogger(classOf[RecordProcessor]) + private val ingestionSuccessMeter = metricRegistry.meter(AppMetricNames.KINESIS_INGESTION_SUCCESS) + private val processingLagHistogram = metricRegistry.histogram(AppMetricNames.KINESIS_PROCESSING_LAG) + private val checkpointFailureMeter = metricRegistry.meter(AppMetricNames.KINESIS_CHECKPOINT_FAILURE) +} + +class RecordProcessor(config: KinesisConsumerConfiguration, keyValueExtractor: KeyValueExtractor, sink: RecordSink) + extends IRecordProcessor { + + import RecordProcessor._ + + private var shardId: String = _ + private var nextCheckpointTimeInMillis: Long = 0L + + private def checkpoint(checkpointer: IRecordProcessorCheckpointer): Unit = { + LOGGER.debug(s"Performing the checkpointing for shardId=$shardId") + + retryWithBackOff(config.checkpointRetries, config.checkpointRetryInterval)(() => { + checkpointer.checkpoint() + }) match { + case Failure(r) => + checkpointFailureMeter.mark() + LOGGER.error(s"Fail to checkpoint after all retries for shardId=$shardId with reason", r) + case _ => LOGGER.info(s"Successfully checkpointing done for shardId=$shardId") + } + } + + /** + * process the incoming kinesis records. This processor extracts the traceId (partition key for kafka) and + * span as byte array. + * @param records kinesis records + */ + override def processRecords(records: ProcessRecordsInput): Unit = { + var lastRecordArrivalTimestamp:Date = null + + records + .getRecords + .foreach(record => { + lastRecordArrivalTimestamp = record.getApproximateArrivalTimestamp + Try(keyValueExtractor.extractKeyValuePairs(record.getData.array())) match { + case Success(spans) => spans.foreach(sp => sink.toAsync(sp, sinkResponseHandler)) + case _ => /* skip logging as extractor does it*/ + } + }) + + // this is somewhat similar to the IteratorAgeMilliseconds metric reported by Cloudwatch for Kinesis stream + if(lastRecordArrivalTimestamp != null) { + processingLagHistogram.update(System.currentTimeMillis() - lastRecordArrivalTimestamp.getTime) + } + + ingestionSuccessMeter.mark(records.getRecords.size()) + + if (System.currentTimeMillis > nextCheckpointTimeInMillis) { + checkpoint(records.getCheckpointer) + nextCheckpointTimeInMillis = System.currentTimeMillis + config.checkpointInterval.toMillis + } + } + + /** + * initialize the kinesis record processor + * @param input: initialization input contains the shardId and sequenceNumber + */ + override def initialize(input: InitializationInput): Unit = { + LOGGER.info(s"Initializing the processor for shardId=${input.getShardId} and SeqNumber=${input.getExtendedSequenceNumber}") + this.shardId = input.getShardId + } + + /** + * shutdown the processor, it shutdown reason is terminate, then perform the pending checkpointing. + * @param shutdownInput: shutdown input that contains the reason + */ + override def shutdown(shutdownInput: ShutdownInput): Unit = { + LOGGER.info(s"Shutting down record processor for shardId=$shardId") + + // Important to checkpoint after reaching end of shard, so we can start processing data from child shards. + if (shutdownInput.getShutdownReason == ShutdownReason.TERMINATE) { + checkpoint(shutdownInput.getCheckpointer) + } + } + + @tailrec + final def retryWithBackOff[T](maxRetry: Int, backOff: FiniteDuration)(f: () => T): Try[T] = { + Try { + f() + } match { + case Failure(reason) if maxRetry > 0 && !reason.isInstanceOf[InterruptedException] && !reason.isInstanceOf[ShutdownException] => + LOGGER.error(s"Fail to perform the checkpointing operation with retries left=$maxRetry ", reason) + Thread.sleep(backOff.toMillis) + retryWithBackOff(maxRetry - 1, backOff)(f) + case result@_ => result + } + } + + private val sinkResponseHandler = (_: KeyValuePair[Array[Byte], Array[Byte]], ex: Exception) => { + if (ex != null) HealthController.setUnhealthy() + } +} diff --git a/collector/kinesis/src/main/scala/com/expedia/www/haystack/kinesis/span/collector/kinesis/client/KinesisConsumer.scala b/collector/kinesis/src/main/scala/com/expedia/www/haystack/kinesis/span/collector/kinesis/client/KinesisConsumer.scala new file mode 100644 index 000000000..36222aa13 --- /dev/null +++ b/collector/kinesis/src/main/scala/com/expedia/www/haystack/kinesis/span/collector/kinesis/client/KinesisConsumer.scala @@ -0,0 +1,110 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.kinesis.span.collector.kinesis.client + +import java.util.UUID + +import com.amazonaws.auth.DefaultAWSCredentialsProviderChain +import com.amazonaws.auth.profile.internal.securitytoken.{RoleInfo, STSProfileCredentialsServiceProvider} +import com.amazonaws.regions.Regions +import com.amazonaws.services.kinesis.clientlibrary.interfaces.v2.IRecordProcessorFactory +import com.amazonaws.services.kinesis.clientlibrary.lib.worker.{KinesisClientLibConfiguration, Worker} +import com.expedia.www.haystack.collector.commons.health.HealthController +import com.expedia.www.haystack.collector.commons.record.KeyValueExtractor +import com.expedia.www.haystack.collector.commons.sink.RecordSink +import com.expedia.www.haystack.kinesis.span.collector.config.entities.KinesisConsumerConfiguration +import com.expedia.www.haystack.kinesis.span.collector.kinesis.RecordProcessor +import org.apache.commons.lang3.StringUtils +import org.slf4j.LoggerFactory + +class KinesisConsumer(config: KinesisConsumerConfiguration, + keyValueExtractor: KeyValueExtractor, + sink: RecordSink) extends AutoCloseable { + private val LOGGER = LoggerFactory.getLogger(classOf[KinesisConsumer]) + + private var worker: Worker = _ + + // this is a blocking call + def startWorker(): Unit = { + worker = buildWorker(createProcessorFactory()) + + LOGGER.info("Starting the kinesis worker now.") + + // mark collector as healthy + HealthController.setHealthy() + + // the run method will block this thread, process loop will start now.. + worker.run() + } + + private def createProcessorFactory() = { + new IRecordProcessorFactory { + override def createProcessor() = new RecordProcessor(config, keyValueExtractor, sink) + } + } + + /** + * build single kinesis consumer worker. This worker creates the processors for shards + * + * @param processorFactory factory to create processor + * @return + */ + private def buildWorker(processorFactory: IRecordProcessorFactory): Worker = { + val region = Regions.fromName(config.awsRegion) + + val workerId = UUID.randomUUID.toString + + + val kinesisCredsProvider = config.stsRoleArn match { + case Some(arn) if StringUtils.isNotEmpty(arn) => new STSProfileCredentialsServiceProvider(new RoleInfo().withRoleArn(arn).withRoleSessionName(config.appGroupName)) + case _ => DefaultAWSCredentialsProviderChain.getInstance + } + + val kinesisClientConfig = new KinesisClientLibConfiguration( + config.appGroupName, + config.streamName, + kinesisCredsProvider, + DefaultAWSCredentialsProviderChain.getInstance, + DefaultAWSCredentialsProviderChain.getInstance, + workerId) + + kinesisClientConfig + .withMaxRecords(config.maxRecordsToRead) + .withIdleTimeBetweenReadsInMillis(config.idleTimeBetweenReads.toMillis) + .withShardSyncIntervalMillis(config.shardSyncInterval.toMillis) + .withInitialPositionInStream(config.streamPosition) + .withMetricsLevel(config.metricsLevel) + .withMetricsBufferTimeMillis(config.metricsBufferTime.toMillis) + .withRegionName(region.getName) + .withTableName(config.dynamoTableName.getOrElse(config.appGroupName)) + .withTaskBackoffTimeMillis(config.taskBackoffTime.toMillis) + + config.dynamoEndpoint.map(kinesisClientConfig.withDynamoDBEndpoint) + config.kinesisEndpoint.map(kinesisClientConfig.withKinesisEndpoint) + + new Worker.Builder() + .config(kinesisClientConfig) + .recordProcessorFactory(processorFactory) + .build() + } + + /** + * close the kinesis worker. The shutdown will also cleanup resources allocated by the worker + */ + override def close(): Unit = worker.shutdown() +} diff --git a/collector/kinesis/src/main/scala/com/expedia/www/haystack/kinesis/span/collector/kinesis/record/TtlAndOperationNames.java b/collector/kinesis/src/main/scala/com/expedia/www/haystack/kinesis/span/collector/kinesis/record/TtlAndOperationNames.java new file mode 100644 index 000000000..4a22bdcf6 --- /dev/null +++ b/collector/kinesis/src/main/scala/com/expedia/www/haystack/kinesis/span/collector/kinesis/record/TtlAndOperationNames.java @@ -0,0 +1,44 @@ +/* + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.kinesis.span.collector.kinesis.record; + +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; + +/** + * This class is used by ProtoSpanExtractor to keep track of the number of operation names for a particular service. + * It is written in Java because Java's Atomic classes are the preferred way of handling concurrent maps and sets + * in Scala, and the accesses to the objects that count operation names come from multiple threads. + */ +public class TtlAndOperationNames { + public final Set operationNames = ConcurrentHashMap.newKeySet(); + private final AtomicLong ttlMillis; + + TtlAndOperationNames(long ttlMillis) { + this.ttlMillis = new AtomicLong(ttlMillis); + } + + public long getTtlMillis() { + return ttlMillis.get(); + } + + void setTtlMillis(long ttlMillis) { + this.ttlMillis.set(ttlMillis); + } +} diff --git a/collector/kinesis/src/main/scala/com/expedia/www/haystack/kinesis/span/collector/metrics/AppMetricNames.scala b/collector/kinesis/src/main/scala/com/expedia/www/haystack/kinesis/span/collector/metrics/AppMetricNames.scala new file mode 100644 index 000000000..13fc763ca --- /dev/null +++ b/collector/kinesis/src/main/scala/com/expedia/www/haystack/kinesis/span/collector/metrics/AppMetricNames.scala @@ -0,0 +1,25 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.kinesis.span.collector.metrics + +object AppMetricNames { + + val KINESIS_PROCESSING_LAG = "kinesis.processing.lag" + val KINESIS_INGESTION_SUCCESS = "kinesis.ingestion-success" + val KINESIS_CHECKPOINT_FAILURE = "kinesis.checkpoint.failure" +} diff --git a/collector/kinesis/src/main/scala/com/expedia/www/haystack/kinesis/span/collector/pipeline/KinesisToKafkaPipeline.scala b/collector/kinesis/src/main/scala/com/expedia/www/haystack/kinesis/span/collector/pipeline/KinesisToKafkaPipeline.scala new file mode 100644 index 000000000..cb7fe64a7 --- /dev/null +++ b/collector/kinesis/src/main/scala/com/expedia/www/haystack/kinesis/span/collector/pipeline/KinesisToKafkaPipeline.scala @@ -0,0 +1,61 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.kinesis.span.collector.pipeline + +import com.expedia.www.haystack.collector.commons.config.{ExternalKafkaConfiguration, ExtractorConfiguration, KafkaProduceConfiguration} +import com.expedia.www.haystack.collector.commons.sink.kafka.KafkaRecordSink +import com.expedia.www.haystack.collector.commons.{MetricsSupport, ProtoSpanExtractor, SpanDecoratorFactory} +import com.expedia.www.haystack.kinesis.span.collector.config.entities.KinesisConsumerConfiguration +import com.expedia.www.haystack.kinesis.span.collector.kinesis.client.KinesisConsumer +import com.expedia.www.haystack.span.decorators.SpanDecorator +import com.expedia.www.haystack.span.decorators.plugin.config.Plugin +import org.slf4j.LoggerFactory + +import scala.util.Try + +class KinesisToKafkaPipeline(kafkaProducerConfig: KafkaProduceConfiguration, + listExternalKafkaConfig: List[ExternalKafkaConfiguration], + kinesisConsumerConfig: KinesisConsumerConfiguration, + extractorConfiguration: ExtractorConfiguration, + additionalTagsConfig: Map[String, String], + pluginConfig: Plugin + ) + extends AutoCloseable with MetricsSupport { + + private val LOGGER = LoggerFactory.getLogger(classOf[KinesisToKafkaPipeline]) + + private var kafkaSink: KafkaRecordSink = _ + private var consumer: KinesisConsumer = _ + private var listSpanDecorator: List[SpanDecorator] = List() + + /** + * run the pipeline. start the kinesis consumer worker and produce the read spans to kafka + * the run is a blocking call. kinesis consumer blocks after spinning off the workers + */ + def run(): Unit = { + listSpanDecorator = SpanDecoratorFactory.get(pluginConfig, additionalTagsConfig, LOGGER) + kafkaSink = new KafkaRecordSink(kafkaProducerConfig, listExternalKafkaConfig) + consumer = new KinesisConsumer(kinesisConsumerConfig, new ProtoSpanExtractor(extractorConfiguration, LoggerFactory.getLogger(classOf[ProtoSpanExtractor]), listSpanDecorator), kafkaSink) + consumer.startWorker() + } + + override def close(): Unit = { + Try(consumer.close()) + Try(kafkaSink.close()) + } +} diff --git a/collector/kinesis/src/test/resources/config/base.conf b/collector/kinesis/src/test/resources/config/base.conf new file mode 100644 index 000000000..3037f1d95 --- /dev/null +++ b/collector/kinesis/src/test/resources/config/base.conf @@ -0,0 +1,86 @@ +health.status.path = "/app/isHealthy" + +kafka { + producer { + topic = "proto-spans" + props { + bootstrap.servers = "kafkasvc:9092" + } + } +} + +extractor { + output.format = "proto" + spans.validation { + + # Validate size of span. Truncate span tags when size exceeds spcified limit. + # enable: true/false + # log.only: if enabled, only logs such spans but doesn't truncate the tags + # max.size.limit: maximum size allowed + # message.tag.key: this tag key will be added when tags are truncated + # message.tag.value: value of the above tag key indicating the truncation + # skip.tags: truncate all span tags except these + # skip.services: truncate span tags for all services except these + max.size { + enable = "false" + log.only = "false" + max.size.limit = 5000 // in bytes + message.tag.key = "X-HAYSTACK-SPAN-INFO" + message.tag.value = "Tags are truncated. REASON: Span Size Limit Exceeded. Please contact Haystack for more details" + skip.tags = ["error"] + skip.services = [] + } + } +} + +kinesis { + aws.region = "us-west-2" + + app.group.name = "haystack-kinesis-proto-span-collector" + + stream { + name = "haystack-proto-spans" + position = "LATEST" + } + + checkpoint { + interval.ms = 15000 + retries = 50 + retry.interval.ms = 250 + } + + task.backoff.ms = 200 + max.records.read = 2000 + idle.time.between.reads.ms = 500 + shard.sync.interval.ms = 30000 + + metrics { + level = "NONE" + buffer.time.ms = 10000 + } +} + +additionaltags.X-HAYSTACK-SPAN-ADDITIONAL-TAG = ADDITIONAL-TAG + +external.kafka.kafka1 { + tags { + X-HAYSTACK-SPAN-OWNER = OWNER1 + X-HAYSTACK-SPAN-SENDER = SENDER1 + } + config { + topic = "external-proto-spans" + props { + bootstrap.servers = "kafkasvc:9092" + } + } +} + +plugins { + directory = plugins/decorators + plugin1 { + name="SAMPLE_SPAN_DECORATOR" + config { + tag.key = "X-HAYSTACK-PLUGIN-SPAN-DECORATOR" + } + } +} diff --git a/collector/kinesis/src/test/resources/logback-test.xml b/collector/kinesis/src/test/resources/logback-test.xml new file mode 100644 index 000000000..6d6308afa --- /dev/null +++ b/collector/kinesis/src/test/resources/logback-test.xml @@ -0,0 +1,27 @@ + + + + + + + true + + + + + + %d{yyyy-MM-dd HH:mm:ss:SSS} %thread, %level, %logger{70}, "%msg" %replace(%ex){'[\n]+', '\\n'}%nopex%n + + + + + + ${HAYSTACK_LOG_QUEUE_SIZE:-500} + ${HAYSTACK_LOG_DISCARD_THRESHOLD:-0} + + + + + + + diff --git a/collector/kinesis/src/test/scala/com/expedia/www/haystack/kinesis/span/collector/integration/IntegrationTestSpec.scala b/collector/kinesis/src/test/scala/com/expedia/www/haystack/kinesis/span/collector/integration/IntegrationTestSpec.scala new file mode 100644 index 000000000..ffe240bb0 --- /dev/null +++ b/collector/kinesis/src/test/scala/com/expedia/www/haystack/kinesis/span/collector/integration/IntegrationTestSpec.scala @@ -0,0 +1,48 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.kinesis.span.collector.integration + +import java.io.File +import java.util.concurrent.Executors + +import com.expedia.www.haystack.kinesis.span.collector.App +import org.scalatest._ + +class IntegrationTestSpec extends WordSpec with GivenWhenThen with Matchers with LocalKinesisProducer with LocalKafkaConsumer + with OptionValues with BeforeAndAfterAll { + + private val executor = Executors.newSingleThreadExecutor() + + override def beforeAll(): Unit = { + // check if the stream exists, if not create one + createStreamIfNotExists() + + new File("/app").mkdir() + + executor.submit(new Runnable { + override def run(): Unit = App.main(null) + }) + // wait for few sec to let app start + Thread.sleep(60000) + } + + override def afterAll(): Unit = { + // check if the stream exists, if not create one + shutdownKinesisClient() + } +} \ No newline at end of file diff --git a/collector/kinesis/src/test/scala/com/expedia/www/haystack/kinesis/span/collector/integration/LocalKafkaConsumer.scala b/collector/kinesis/src/test/scala/com/expedia/www/haystack/kinesis/span/collector/integration/LocalKafkaConsumer.scala new file mode 100644 index 000000000..3f59a9569 --- /dev/null +++ b/collector/kinesis/src/test/scala/com/expedia/www/haystack/kinesis/span/collector/integration/LocalKafkaConsumer.scala @@ -0,0 +1,117 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.kinesis.span.collector.integration + +import java.util.Properties +import java.util.stream.Collectors + +import com.expedia.www.haystack.collector.commons.config.ExternalKafkaConfiguration +import com.expedia.www.haystack.kinesis.span.collector.config.ProjectConfiguration +import com.expedia.www.haystack.kinesis.span.collector.integration.config.TestConfiguration +import org.apache.kafka.clients.consumer.internals.NoOpConsumerRebalanceListener +import org.apache.kafka.clients.consumer.{ConsumerConfig, KafkaConsumer} +import org.apache.kafka.common.serialization.ByteArrayDeserializer + +import scala.collection.JavaConversions._ +import scala.collection.JavaConverters._ +import scala.collection.mutable +import scala.concurrent.duration._ + +trait LocalKafkaConsumer { + + private val kafkaConsumer = { + val consumerProperties = new Properties() + consumerProperties.setProperty(ConsumerConfig.GROUP_ID_CONFIG, "kinesis-to-kafka-test") + consumerProperties.setProperty(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, TestConfiguration.remoteKafkaHost + ":" + TestConfiguration.kafkaPort) + consumerProperties.setProperty(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, classOf[ByteArrayDeserializer].getCanonicalName) + consumerProperties.setProperty(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, classOf[ByteArrayDeserializer].getCanonicalName) + new KafkaConsumer[Array[Byte], Array[Byte]](consumerProperties) + } + + private val externalKafkaConsumerMap: Map[String, KafkaConsumer[Array[Byte], Array[Byte]]] = { + val externalKafkaList: List[ExternalKafkaConfiguration] = ProjectConfiguration.externalKafkaConfig() + externalKafkaList.zipWithIndex.map { case (c, i) => { + val consumerProperties = new Properties() + consumerProperties.setProperty(ConsumerConfig.GROUP_ID_CONFIG, s"kinesis-to-kafka-test-${i}") + consumerProperties.setProperty(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, c.kafkaProduceConfiguration.props.getProperty("bootstrap.servers")) + consumerProperties.setProperty(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, classOf[ByteArrayDeserializer].getCanonicalName) + consumerProperties.setProperty(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, classOf[ByteArrayDeserializer].getCanonicalName) + val consumer = new KafkaConsumer[Array[Byte], Array[Byte]](consumerProperties) + consumer.subscribe(List(c.kafkaProduceConfiguration.topic).asJava, new NoOpConsumerRebalanceListener()) + c.kafkaProduceConfiguration.topic -> consumer + }}.toMap + } + + kafkaConsumer.subscribe(List(TestConfiguration.kafkaStreamName).asJava, new NoOpConsumerRebalanceListener()) + + def readRecordsFromKafka(minExpectedCount: Int, maxWait: FiniteDuration): List[Array[Byte]] = { + val records = mutable.ListBuffer[Array[Byte]]() + var received: Int = 0 + + var waitTimeLeft = maxWait.toMillis + var done = true + while (done) { + kafkaConsumer.poll(250).records(TestConfiguration.kafkaStreamName).map(rec => { + received += 1 + records += rec.value() + }) + if(received < minExpectedCount && waitTimeLeft > 0) { + Thread.sleep(1000) + waitTimeLeft -= 1000 + } else { + done = false + } + } + + if(records.size < minExpectedCount) throw new RuntimeException("Fail to read the expected records from kafka") + + records.toList + } + + def readRecordsFromExternalKafka(minExpectedCount: Int, maxWait: FiniteDuration): List[Array[Byte]] = { + val records = mutable.ListBuffer[Array[Byte]]() + var received: Int = 0 + + var waitTimeLeft = maxWait.toMillis + + externalKafkaConsumerMap.foreach(externalKafkaConsumer => { + var done = true + while (done) { + externalKafkaConsumer._2.poll(250).records(externalKafkaConsumer._1).map(rec => { + received += 1 + records += rec.value() + }) + if(received < minExpectedCount && waitTimeLeft > 0) { + Thread.sleep(1000) + waitTimeLeft -= 1000 + } else { + done = false + } + } + }) + + if(records.size < minExpectedCount) throw new RuntimeException("Fail to read the expected records from kafka") + + records.toList + } + + def shutdownKafkaConsumer(): Unit = { + if(kafkaConsumer != null) kafkaConsumer.close() + externalKafkaConsumerMap.foreach(c => c._2.close()) + } +} diff --git a/collector/kinesis/src/test/scala/com/expedia/www/haystack/kinesis/span/collector/integration/LocalKinesisProducer.scala b/collector/kinesis/src/test/scala/com/expedia/www/haystack/kinesis/span/collector/integration/LocalKinesisProducer.scala new file mode 100644 index 000000000..6e0dd8897 --- /dev/null +++ b/collector/kinesis/src/test/scala/com/expedia/www/haystack/kinesis/span/collector/integration/LocalKinesisProducer.scala @@ -0,0 +1,59 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.kinesis.span.collector.integration + +import java.nio.ByteBuffer +import java.util.UUID + +import com.amazonaws.client.builder.AwsClientBuilder +import com.amazonaws.services.kinesis.AmazonKinesisClientBuilder +import com.amazonaws.services.kinesis.model.ResourceNotFoundException +import com.amazonaws.{ClientConfiguration, Protocol} +import com.expedia.www.haystack.kinesis.span.collector.integration.config.TestConfiguration + +trait LocalKinesisProducer { + + private val client = { + val endpointConfig = new AwsClientBuilder.EndpointConfiguration(s"http://${TestConfiguration.remoteKinesisHost}:${TestConfiguration.kinesisPort}", "us-west-2") + val clientConfig = new ClientConfiguration().withProtocol(Protocol.HTTP) + + AmazonKinesisClientBuilder + .standard() + .withClientConfiguration(clientConfig) + .withEndpointConfiguration(endpointConfig) + .build() + } + + protected def createStreamIfNotExists(): Unit = { + try { + client.describeStream(TestConfiguration.kinesisStreamName) + } catch { + case _: ResourceNotFoundException => + println(s"Creating kinesis stream ${TestConfiguration.kinesisStreamName}") + client.createStream(TestConfiguration.kinesisStreamName, 1) + } + } + + def produceRecordsToKinesis(records: List[Array[Byte]]): Unit = { + records.foreach(record => { + client.putRecord(TestConfiguration.kinesisStreamName, ByteBuffer.wrap(record), UUID.randomUUID().toString) + }) + } + + protected def shutdownKinesisClient(): Unit = if(client != null) client.shutdown() +} diff --git a/collector/kinesis/src/test/scala/com/expedia/www/haystack/kinesis/span/collector/integration/config/TestConfiguration.scala b/collector/kinesis/src/test/scala/com/expedia/www/haystack/kinesis/span/collector/integration/config/TestConfiguration.scala new file mode 100644 index 000000000..7e95cec29 --- /dev/null +++ b/collector/kinesis/src/test/scala/com/expedia/www/haystack/kinesis/span/collector/integration/config/TestConfiguration.scala @@ -0,0 +1,27 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.kinesis.span.collector.integration.config + +object TestConfiguration { + val remoteKafkaHost = "kafkasvc" + val kafkaPort = 9092 + val remoteKinesisHost = "localstack" + val kinesisPort = 4568 + val kafkaStreamName = "proto-spans" + val kinesisStreamName = "haystack-proto-spans" +} diff --git a/collector/kinesis/src/test/scala/com/expedia/www/haystack/kinesis/span/collector/integration/tests/KinesisSpanCollectorSpec.scala b/collector/kinesis/src/test/scala/com/expedia/www/haystack/kinesis/span/collector/integration/tests/KinesisSpanCollectorSpec.scala new file mode 100644 index 000000000..e9d3d9f36 --- /dev/null +++ b/collector/kinesis/src/test/scala/com/expedia/www/haystack/kinesis/span/collector/integration/tests/KinesisSpanCollectorSpec.scala @@ -0,0 +1,130 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.kinesis.span.collector.integration.tests + +import com.expedia.open.tracing.{Span, Tag} +import com.expedia.www.haystack.kinesis.span.collector.config.ProjectConfiguration +import com.expedia.www.haystack.kinesis.span.collector.integration._ + +import scala.concurrent.duration._ + +class KinesisSpanCollectorSpec extends IntegrationTestSpec { + + private val StartTimeMicros = System.currentTimeMillis() * 1000 + private val DurationMicros = 42 + + "Kinesis span collector" should { + + // this test is primarily to work around issue with Kafka docker image + // it fails for first put for some reasons + "connect with kinesis and kafka" in { + + Given("a valid span") + val spanBytes = Span.newBuilder().setTraceId("traceid").setSpanId("span-id-1").build().toByteArray + + When("the span is sent to kinesis") + produceRecordsToKinesis(List(spanBytes, spanBytes)) + + Then("it should be pushed to kafka") + readRecordsFromKafka(0, 1.second).headOption + } + + "read valid spans from kinesis and store individual spans in kafka" in { + + Given("valid spans") + val span_1 = Span.newBuilder().setTraceId("trace-id-1").setSpanId("span-id-1").setOperationName("operation") + .setServiceName("service").setStartTime(StartTimeMicros).setDuration(DurationMicros).build().toByteArray + val span_2 = Span.newBuilder().setTraceId("trace-id-1").setSpanId("span-id-2").setOperationName("operation") + .setServiceName("service").setStartTime(StartTimeMicros).setDuration(DurationMicros).build().toByteArray + val span_3 = Span.newBuilder().setTraceId("trace-id-2").setSpanId("span-id-3").setOperationName("operation") + .setServiceName("service").setStartTime(StartTimeMicros).setDuration(DurationMicros).build().toByteArray + val span_4 = Span.newBuilder().setTraceId("trace-id-2").setSpanId("span-id-4").setOperationName("operation") + .setServiceName("service").setStartTime(StartTimeMicros).setDuration(DurationMicros).build().toByteArray + + When("the span is sent to kinesis") + produceRecordsToKinesis(List(span_1, span_2, span_3, span_4)) + + Then("it should be pushed to kafka with partition key as its trace id") + val records = readRecordsFromKafka(4, 5.seconds) + val externalrecords = readRecordsFromExternalKafka(0, 10.seconds) + externalrecords.size shouldEqual 0 + records.size shouldEqual 4 + + val spans = records.map(Span.parseFrom) + spans.map(_.getTraceId).toSet should contain allOf("trace-id-1", "trace-id-2") + spans.map(_.getSpanId) should contain allOf("span-id-1", "span-id-2", "span-id-3", "span-id-4") + } + + "read valid spans from kinesis and store individual spans in kafka and external kafka" in { + + Given("valid spans") + val tags: List[Tag] = List( + Tag.newBuilder().setKey("X-HAYSTACK-SPAN-OWNER").setVStr("OWNER1").build(), + Tag.newBuilder().setKey("X-HAYSTACK-SPAN-SENDER").setVStr("SENDER1").build() + ) + val span_1 = Span.newBuilder().setTraceId("trace-id-1").setSpanId("span-id-1").setOperationName("operation") + .setServiceName("service").setStartTime(StartTimeMicros).setDuration(DurationMicros) + .addTags(tags(0)).addTags(tags(1)).build().toByteArray + val span_2 = Span.newBuilder().setTraceId("trace-id-1").setSpanId("span-id-2").setOperationName("operation") + .setServiceName("service").setStartTime(StartTimeMicros).setDuration(DurationMicros) + .addTags(tags(0)).addTags(tags(1)).build().toByteArray + val span_3 = Span.newBuilder().setTraceId("trace-id-2").setSpanId("span-id-3").setOperationName("operation") + .setServiceName("service").setStartTime(StartTimeMicros).setDuration(DurationMicros) + .addTags(tags(0)).addTags(tags(1)).build().toByteArray + val span_4 = Span.newBuilder().setTraceId("trace-id-2").setSpanId("span-id-4").setOperationName("operation") + .setServiceName("service").setStartTime(StartTimeMicros).setDuration(DurationMicros) + .addTags(tags(0)).addTags(tags(1)).build().toByteArray + + When("the span is sent to kinesis") + produceRecordsToKinesis(List(span_1, span_2, span_3, span_4)) + + Then("it should be pushed to default kafka and external kafka with partition key as its trace id") + val records = readRecordsFromKafka(4, 5.seconds) + val numConsumers = ProjectConfiguration.externalKafkaConfig().size + val externalrecords = readRecordsFromExternalKafka(4 * numConsumers, (10 * numConsumers).seconds) + externalrecords.size should equal(4) + records.size should equal(4) + val spans = records.map(Span.parseFrom) + val externalSpans = externalrecords.map(Span.parseFrom) + numConsumers should equal(1) + spans.map(_.getTraceId).toSet should contain allOf("trace-id-1", "trace-id-2") + externalSpans.map(_.getTraceId).toSet should contain allOf("trace-id-1", "trace-id-2") + spans.map(_.getSpanId) should contain allOf("span-id-1", "span-id-2", "span-id-3", "span-id-4") + externalSpans.map(_.getSpanId) should contain allOf("span-id-1", "span-id-2", "span-id-3", "span-id-4") + } + + "load appropriate span decorator plugin using configuration provided " in { + + Given("Jar file for SAMPLE_SPAN_DECORATOR plugin in plugins/decorators directory") + val span_1 = Span.newBuilder().setTraceId("trace-id-1").setSpanId("span-id-1").setOperationName("operation") + .setServiceName("service").setStartTime(StartTimeMicros).setDuration(DurationMicros).build().toByteArray + + When("the app is initialised") + produceRecordsToKinesis(List(span_1)) + + Then("the appropriate span decorator plugin should be loaded using spi") + val records = readRecordsFromKafka(1, 5.seconds) + records should not be empty + + val spans = records.map(Span.parseFrom) + spans.map(_.getTraceId).toSet should contain("trace-id-1") + spans.map(_.getSpanId) should contain("span-id-1") + spans(0).getTagsList should contain(Tag.newBuilder().setKey("X-HAYSTACK-PLUGIN-SPAN-DECORATOR").setVStr("SAMPLE-TAG").build()) + } + } +} diff --git a/collector/kinesis/src/test/scala/com/expedia/www/haystack/kinesis/span/collector/unit/tests/ConfigurationLoaderSpec.scala b/collector/kinesis/src/test/scala/com/expedia/www/haystack/kinesis/span/collector/unit/tests/ConfigurationLoaderSpec.scala new file mode 100644 index 000000000..5c486ee1f --- /dev/null +++ b/collector/kinesis/src/test/scala/com/expedia/www/haystack/kinesis/span/collector/unit/tests/ConfigurationLoaderSpec.scala @@ -0,0 +1,86 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.kinesis.span.collector.unit.tests + +import com.amazonaws.services.kinesis.clientlibrary.lib.worker.InitialPositionInStream +import com.amazonaws.services.kinesis.metrics.interfaces.MetricsLevel +import com.expedia.www.haystack.collector.commons.config.ExternalKafkaConfiguration +import com.expedia.www.haystack.kinesis.span.collector.config.ProjectConfiguration +import com.expedia.www.haystack.span.decorators.plugin.config.Plugin +import org.apache.kafka.clients.producer.ProducerConfig +import org.scalatest.{FunSpec, Matchers} + +class ConfigurationLoaderSpec extends FunSpec with Matchers { + + val project = ProjectConfiguration + + describe("Configuration com.expedia.www.haystack.span.loader") { + it("should load the kinesis config from base.conf") { + val kinesis = project.kinesisConsumerConfig() + kinesis.metricsLevel shouldEqual MetricsLevel.NONE + kinesis.awsRegion shouldEqual "us-west-2" + kinesis.appGroupName shouldEqual "haystack-kinesis-proto-span-collector" + kinesis.checkpointRetries shouldBe 50 + kinesis.dynamoTableName shouldBe None + kinesis.checkpointInterval.toMillis shouldBe 15000L + kinesis.streamPosition shouldEqual InitialPositionInStream.LATEST + kinesis.streamName shouldEqual "haystack-proto-spans" + kinesis.maxRecordsToRead shouldBe 2000 + kinesis.metricsBufferTime.toMillis shouldBe 10000 + kinesis.shardSyncInterval.toMillis shouldBe 30000 + kinesis.kinesisEndpoint.isEmpty shouldBe true + kinesis.dynamoEndpoint.isEmpty shouldBe true + kinesis.taskBackoffTime.toMillis shouldBe 200 + } + + it("should load the kafka config only from base.conf") { + val kafka = project.kafkaProducerConfig() + kafka.topic shouldEqual "proto-spans" + kafka.props.getProperty(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG) shouldEqual "kafkasvc:9092" + } + + it("should load the extractor config only from base.conf") { + val extractorConfig = project.extractorConfiguration() + extractorConfig.outputFormat.toString shouldEqual "proto" + extractorConfig.spanValidation.spanMaxSize.maxSizeLimit shouldEqual 5000 + extractorConfig.spanValidation.spanMaxSize.enable shouldEqual false + extractorConfig.spanValidation.spanMaxSize.skipTags.contains("error") shouldEqual true + extractorConfig.spanValidation.spanMaxSize.skipServices.size shouldEqual 0 + + } + + it("should load the external kafka config from the base.conf") { + val externalKafka: List[ExternalKafkaConfiguration] = project.externalKafkaConfig() + externalKafka.head.tags("X-HAYSTACK-SPAN-OWNER") shouldEqual "OWNER1" + externalKafka.head.tags("X-HAYSTACK-SPAN-SENDER") shouldEqual "SENDER1" + externalKafka.head.kafkaProduceConfiguration.topic shouldEqual "external-proto-spans" + externalKafka.head.kafkaProduceConfiguration.props.getProperty("bootstrap.servers") shouldEqual "kafkasvc:9092" + } + + it("should load the plugins config from the base.conf") { + val plugin: Plugin = project.pluginConfiguration() + plugin.getDirectory shouldEqual "plugins/decorators" + plugin.getPluginConfigurationList.get(0).getName shouldEqual "SAMPLE_SPAN_DECORATOR" + plugin.getPluginConfigurationList.get(0).getConfig.getString("tag.key") shouldEqual "X-HAYSTACK-PLUGIN-SPAN-DECORATOR" + } + + it("should load the health status file") { + project.healthStatusFile() shouldEqual Some("/app/isHealthy") + } + } +} diff --git a/collector/kinesis/src/test/scala/com/expedia/www/haystack/kinesis/span/collector/unit/tests/RecordProcessorSpec.scala b/collector/kinesis/src/test/scala/com/expedia/www/haystack/kinesis/span/collector/unit/tests/RecordProcessorSpec.scala new file mode 100644 index 000000000..dddbbcc35 --- /dev/null +++ b/collector/kinesis/src/test/scala/com/expedia/www/haystack/kinesis/span/collector/unit/tests/RecordProcessorSpec.scala @@ -0,0 +1,176 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.kinesis.span.collector.unit.tests + +import java.nio.ByteBuffer +import java.util.Date + +import com.amazonaws.services.kinesis.clientlibrary.interfaces.IRecordProcessorCheckpointer +import com.amazonaws.services.kinesis.clientlibrary.lib.worker.InitialPositionInStream +import com.amazonaws.services.kinesis.clientlibrary.types.ProcessRecordsInput +import com.amazonaws.services.kinesis.metrics.interfaces.MetricsLevel +import com.amazonaws.services.kinesis.model.Record +import com.expedia.open.tracing.Span +import com.expedia.www.haystack.collector.commons.config.{ExtractorConfiguration, Format, SpanMaxSize, SpanValidation} +import com.expedia.www.haystack.collector.commons.record.KeyValuePair +import com.expedia.www.haystack.collector.commons.sink.RecordSink +import com.expedia.www.haystack.collector.commons.{MetricsSupport, ProtoSpanExtractor} +import com.expedia.www.haystack.kinesis.span.collector.config.entities.KinesisConsumerConfiguration +import com.expedia.www.haystack.kinesis.span.collector.kinesis.RecordProcessor +import org.easymock.EasyMock +import org.easymock.EasyMock._ +import org.scalatest.easymock.EasyMockSugar +import org.scalatest.{FunSpec, Matchers} +import org.slf4j.LoggerFactory + +import scala.collection.JavaConverters._ +import scala.concurrent.duration._ + +class RecordProcessorSpec extends FunSpec with Matchers with EasyMockSugar with MetricsSupport { + private val StartTimeMicros = System.currentTimeMillis() * 1000 + private val DurationMicros = 42 + describe("Record Processor") { + + val kinesisConfig = KinesisConsumerConfiguration("us-west-2", None, + "app-group", "stream-1", InitialPositionInStream.LATEST, 10.seconds, 10, 10.seconds, None, None, None, + 10000, 500.millis, 10000.millis, MetricsLevel.NONE, 10000.millis, 200.millis) + + it("should process the record, sends to sink and perform checkpointing") { + val sink = mock[RecordSink] + val checkpointer = mock[IRecordProcessorCheckpointer] + + val span_1 = Span.newBuilder() + .setSpanId("span-id-1") + .setTraceId("trace-id") + .setServiceName("service") + .setOperationName("operation") + .setStartTime(StartTimeMicros) + .setDuration(DurationMicros) + .build() + val record = new Record() + .withApproximateArrivalTimestamp(new Date()) + .withData(ByteBuffer.wrap(span_1.toByteArray)) + + val capturedKVPair = EasyMock.newCapture[KeyValuePair[Array[Byte], Array[Byte]]]() + + expecting { + sink.toAsync( + capture(capturedKVPair), + anyObject(classOf[(KeyValuePair[Array[Byte], Array[Byte]], Exception) => Unit])) + }.times(2) + + expecting { + checkpointer.checkpoint() + }.once() + + whenExecuting(sink, checkpointer) { + val spanValidationConfig = SpanValidation(SpanMaxSize(enable = false, logOnly = false, 5000, "", "", Seq(), Seq())) + val processor = new RecordProcessor(kinesisConfig, new ProtoSpanExtractor(ExtractorConfiguration(Format.PROTO, spanValidationConfig), LoggerFactory.getLogger(classOf[ProtoSpanExtractor]), List()), sink) + val input = new ProcessRecordsInput().withRecords(List(record).asJava).withCheckpointer(checkpointer) + processor.processRecords(input) + + capturedKVPair.getValue.key shouldEqual "trace-id".getBytes("UTF-8") + capturedKVPair.getValue.value shouldEqual span_1.toByteArray + + // check-pointing should be called just once + processor.processRecords(input) + } + } + + it("should process a span without transactionid, but send to sink and perform checkpointing") { + val sink = mock[RecordSink] + val checkpointer = mock[IRecordProcessorCheckpointer] + + val span_1 = Span.newBuilder() + .setSpanId("span-id-1") + .setTraceId("trace-id-1") + .setServiceName("service") + .setOperationName("operation") + .setStartTime(StartTimeMicros) + .setDuration(DurationMicros) + .build() + + val span_2 = Span.newBuilder() + .setSpanId("span-id-2") + .setTraceId("trace-id-2") + .setServiceName("service") + .setOperationName("operation") + .setStartTime(StartTimeMicros) + .setDuration(DurationMicros) + .build() + + val record_1 = new Record() + .withPartitionKey(null) + .withApproximateArrivalTimestamp(new Date()) + .withData(ByteBuffer.wrap(span_1.toByteArray)) + + val record_2 = new Record() + .withPartitionKey(null) + .withApproximateArrivalTimestamp(new Date()) + .withData(ByteBuffer.wrap(span_2.toByteArray)) + + val captureKvPair = EasyMock.newCapture[KeyValuePair[Array[Byte], Array[Byte]]]() + + expecting { + sink.toAsync( + capture(captureKvPair), + anyObject(classOf[(KeyValuePair[Array[Byte], Array[Byte]], Exception) => Unit])) + }.times(2) + + expecting { + checkpointer.checkpoint() + }.once() + + whenExecuting(sink, checkpointer) { + val spanValidationConfig = SpanValidation(SpanMaxSize(enable = false, logOnly = false, 5000, "", "", Seq(), Seq())) + val processor = new RecordProcessor(kinesisConfig, new ProtoSpanExtractor(ExtractorConfiguration(Format.PROTO, spanValidationConfig), LoggerFactory.getLogger(classOf[ProtoSpanExtractor]), List()), sink) + val input_1 = new ProcessRecordsInput().withRecords(List(record_1).asJava).withCheckpointer(checkpointer) + processor.processRecords(input_1) + + captureKvPair.getValue.key shouldEqual "trace-id-1".getBytes("UTF-8") + captureKvPair.getValue.value shouldEqual span_1.toByteArray + + val input = new ProcessRecordsInput().withRecords(List(record_2).asJava).withCheckpointer(checkpointer) + processor.processRecords(input) + + captureKvPair.getValue.key shouldEqual "trace-id-2".getBytes("UTF-8") + captureKvPair.getValue.value shouldEqual span_2.toByteArray + + } + } + + it("should not emit an illegal json span to sink but perform checkpointing") { + val sink = mock[RecordSink] + val checkpointer = mock[IRecordProcessorCheckpointer] + + val spanData = "random-span-proto-bytes".getBytes() + val record = new Record().withPartitionKey(null).withApproximateArrivalTimestamp(new Date()).withData(ByteBuffer.wrap(spanData)) + + expecting { + checkpointer.checkpoint() + }.once + + whenExecuting(sink, checkpointer) { + val spanValidationConfig = SpanValidation(SpanMaxSize(enable = false, logOnly = false, 5000, "", "", Seq(), Seq())) + val processor = new RecordProcessor(kinesisConfig, new ProtoSpanExtractor(ExtractorConfiguration(Format.PROTO, spanValidationConfig), LoggerFactory.getLogger(classOf[ProtoSpanExtractor]), List()), sink) + val input = new ProcessRecordsInput().withRecords(List(record).asJava).withCheckpointer(checkpointer) + processor.processRecords(input) + } + } + } +} diff --git a/collector/mvnw b/collector/mvnw new file mode 100755 index 000000000..e96ccd5fb --- /dev/null +++ b/collector/mvnw @@ -0,0 +1,227 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven2 Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + export JAVA_HOME="`/usr/libexec/java_home`" + else + export JAVA_HOME="/Library/Java/Home" + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" + # TODO classpath? +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="`which java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +BASE_DIR=`find_maven_basedir "$(pwd)"` +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +if [ "$MVNW_VERBOSE" = true ]; then + echo $MAVEN_PROJECTBASEDIR +fi +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +fi + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/collector/mvnw.cmd b/collector/mvnw.cmd new file mode 100755 index 000000000..019bd74d7 --- /dev/null +++ b/collector/mvnw.cmd @@ -0,0 +1,143 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven2 Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" +if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" + +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" +if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%" == "on" pause + +if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% + +exit /B %ERROR_CODE% diff --git a/collector/pom.xml b/collector/pom.xml new file mode 100644 index 000000000..8f24d96a7 --- /dev/null +++ b/collector/pom.xml @@ -0,0 +1,464 @@ + + + + 4.0.0 + com.expedia.www + haystack-collector + 1.0-SNAPSHOT + pom + + + haystack-span-decorators + commons + kinesis + http + sample-span-decorator + + + + scm:git:git://github.com/ExpediaDotCom/haystack-collector.git + scm:git:ssh://github.com/ExpediaDotCom/haystack-collector.git + http://github.com/ExpediaDotCom/haystack-collector + + + ${project.groupId}:${project.artifactId} + Haystack component that collects spans from various sources and publish to kafka + https://github.com/ExpediaDotCom/haystack-collector/tree/master + + + + + Apache License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + + haystack + Haystack Team + haystack@expedia.com + https://github.com/ExpediaDotCom/haystack + + + + + 1.8 + 3.4.0 + + 1.2.3 + 1.7.25 + 3.4 + 3.5.3 + 1.3.1 + + 0.11.0.0 + + 4.5.3 + 1.7.7 + + 0.1.12 + 2 + 11 + 8 + ${scala.major.version}.${scala.minor.version} + ${scala.major.version}.${scala.minor.version}.${scala.tiny.version} + + 6.8 + 1.6.0 + 3.0.3 + 1.3.0 + true + 3.0.1 + 1.6.8 + 1.6 + + + + + + com.expedia.www + haystack-idl-java + 1.0.64 + + + + + com.google.protobuf + protobuf-java + ${protobuf.version} + + + + com.google.protobuf + protobuf-java-util + ${protobuf.version} + + + + + org.scala-lang + scala-library + ${scala-library.version} + + + org.scala-lang + scala-reflect + ${scala-library.version} + + + + + com.typesafe + config + ${typesafe-config.version} + + + + com.codahale.metrics + metrics-core + 3.0.2 + + + + + ch.qos.logback + logback-classic + ${logback.version} + + + ch.qos.logback + logback-core + ${logback.version} + + + org.slf4j + slf4j-api + ${slf4j-api.version} + + + + org.json4s + json4s-jackson_${scala.major.minor.version} + ${json4s.version} + + + + org.apache.kafka + kafka-clients + ${kafka.version} + + + + org.apache.kafka + kafka_${scala.major.minor.version} + ${kafka.version} + + + org.slf4j + slf4j-log4j12 + + + + + + org.apache.commons + commons-lang3 + ${commons-lang.version} + + + + org.apache.httpcomponents + httpclient + ${httpclient.version} + + + + org.slf4j + jcl-over-slf4j + ${jcl-slf4j.version} + + + com.expedia.www + haystack-logback-metrics-appender + ${haystack.logback.metrics.appender.version} + + + + + + + com.expedia.www + haystack-idl-java + + + + com.typesafe + config + + + + com.google.protobuf + protobuf-java + + + + org.scala-lang + scala-library + + + + org.scala-lang + scala-reflect + + + + ch.qos.logback + logback-classic + + + + ch.qos.logback + logback-core + + + + org.slf4j + slf4j-api + + + + org.apache.commons + commons-lang3 + + + + + org.scalatest + scalatest_${scala.major.minor.version} + ${scalatest.version} + test + + + org.pegdown + pegdown + ${pegdown.version} + test + + + junit + junit + 4.12 + test + + + org.easymock + easymock + 3.4 + test + + + org.mockito + mockito-core + 2.23.0 + test + + + + + ${basedir}/src/main/scala + + + ${basedir}/src/main/resources + true + + + + + + org.scalatest + scalatest-maven-plugin + 1.0 + + + test + + test + + + + + + + com.github.os72 + protoc-jar-maven-plugin + 3.3.0.1 + + + + org.apache.maven.plugins + maven-shade-plugin + 1.6 + + + + org.scalastyle + scalastyle-maven-plugin + 0.8.0 + + true + false + ${basedir}/../checkstyles/scalastyle_config.xml + ${basedir}/src/main/scala + ${basedir}/src/test/scala + ${project.build.directory}/scalastyle-output.xml + UTF-8 + + + + compile-scalastyle + + check + + compile + + + + + + net.alchim31.maven + scala-maven-plugin + 3.2.1 + + + scala-compile-first + process-resources + + add-source + compile + + + + scala-test-compile + process-test-resources + + testCompile + + + + attach-javadocs + + doc-jar + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.6.1 + + ${project.jdk.version} + ${project.jdk.version} + + + + org.scoverage + scoverage-maven-plugin + ${scoverage.plugin.version} + + + 34 + true + true + ${scala-library.version} + true + + + + org.sonatype.plugins + nexus-staging-maven-plugin + ${nexus-staging-maven-plugin.version} + + + org.apache.maven.plugins + maven-gpg-plugin + ${maven-gpg-plugin.version} + + + + + + + org.apache.maven.plugins + maven-gpg-plugin + + true + ${skipGpg} + + + + sign-artifacts + verify + + sign + + + + + + org.sonatype.plugins + nexus-staging-maven-plugin + true + + ossrh + https://oss.sonatype.org/ + true + + + + org.apache.maven.plugins + maven-source-plugin + ${maven-source-plugin.version} + + + attach-sources + + jar-no-fork + + + + + + + + + + ossrh + https://oss.sonatype.org/content/repositories/snapshots + + + ossrh + http://oss.sonatype.org/service/local/staging/deploy/maven2/ + + + diff --git a/collector/sample-span-decorator/pom.xml b/collector/sample-span-decorator/pom.xml new file mode 100644 index 000000000..d7074d2f6 --- /dev/null +++ b/collector/sample-span-decorator/pom.xml @@ -0,0 +1,45 @@ + + + + + haystack-collector + com.expedia.www + 1.0-SNAPSHOT + + + 4.0.0 + sample-span-decorator + 1.0-SNAPSHOT + jar + + + + com.expedia.www + haystack-span-decorators + ${project.version} + + + + + src/main/java + ${project.artifactId} + + + net.alchim31.maven + scala-maven-plugin + + + org.apache.maven.plugins + maven-shade-plugin + + + package + + shade + + + + + + + diff --git a/collector/sample-span-decorator/src/main/java/com/expedia/www/sample/span/decorator/SampleSpanDecorator.java b/collector/sample-span-decorator/src/main/java/com/expedia/www/sample/span/decorator/SampleSpanDecorator.java new file mode 100644 index 000000000..8bb959ad7 --- /dev/null +++ b/collector/sample-span-decorator/src/main/java/com/expedia/www/sample/span/decorator/SampleSpanDecorator.java @@ -0,0 +1,32 @@ +package com.expedia.www.sample.span.decorator; + +import com.expedia.open.tracing.Span; +import com.expedia.open.tracing.Tag; +import com.expedia.www.haystack.span.decorators.SpanDecorator; +import com.typesafe.config.Config; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class SampleSpanDecorator implements SpanDecorator { + private static final Logger logger = LoggerFactory.getLogger(SampleSpanDecorator.class); + private Config config; + + public void init(Config config) { + this.config = config; + } + + public SampleSpanDecorator() { + } + + @Override + public Span.Builder decorate(Span.Builder spanBuilder) { + spanBuilder.addTags(Tag.newBuilder().setKey(config.getString("tag.key")) + .setVStr("SAMPLE-TAG").build()); + return spanBuilder; + } + + @Override + public String name() { + return "SAMPLE_SPAN_DECORATOR"; + } +} diff --git a/collector/sample-span-decorator/src/main/resources/META-INF/services/com.expedia.www.haystack.span.decorators.SpanDecorator b/collector/sample-span-decorator/src/main/resources/META-INF/services/com.expedia.www.haystack.span.decorators.SpanDecorator new file mode 100644 index 000000000..4b596420b --- /dev/null +++ b/collector/sample-span-decorator/src/main/resources/META-INF/services/com.expedia.www.haystack.span.decorators.SpanDecorator @@ -0,0 +1 @@ +com.expedia.www.sample.span.decorator.SampleSpanDecorator \ No newline at end of file diff --git a/collector/sample-span-decorator/src/main/resources/logback.xml b/collector/sample-span-decorator/src/main/resources/logback.xml new file mode 100644 index 000000000..6d6308afa --- /dev/null +++ b/collector/sample-span-decorator/src/main/resources/logback.xml @@ -0,0 +1,27 @@ + + + + + + + true + + + + + + %d{yyyy-MM-dd HH:mm:ss:SSS} %thread, %level, %logger{70}, "%msg" %replace(%ex){'[\n]+', '\\n'}%nopex%n + + + + + + ${HAYSTACK_LOG_QUEUE_SIZE:-500} + ${HAYSTACK_LOG_DISCARD_THRESHOLD:-0} + + + + + + + diff --git a/commons/.gitignore b/commons/.gitignore new file mode 100644 index 000000000..3054462c2 --- /dev/null +++ b/commons/.gitignore @@ -0,0 +1,12 @@ +*.log +*.ipr +*.iws +.classpath +.project +target/ +lib/ +logs/ +**/.idea/ +*.iml +*.DS_Store +**/target/ diff --git a/commons/.gitmodules b/commons/.gitmodules new file mode 100644 index 000000000..3b40a897a --- /dev/null +++ b/commons/.gitmodules @@ -0,0 +1,3 @@ +[submodule "haystack-idl"] + path = haystack-idl + url = https://github.com/ExpediaDotCom/haystack-idl.git diff --git a/commons/.mvn/wrapper/MavenWrapperDownloader.java b/commons/.mvn/wrapper/MavenWrapperDownloader.java new file mode 100755 index 000000000..fa4f7b499 --- /dev/null +++ b/commons/.mvn/wrapper/MavenWrapperDownloader.java @@ -0,0 +1,110 @@ +/* +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +*/ + +import java.net.*; +import java.io.*; +import java.nio.channels.*; +import java.util.Properties; + +public class MavenWrapperDownloader { + + /** + * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. + */ + private static final String DEFAULT_DOWNLOAD_URL = + "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar"; + + /** + * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to + * use instead of the default one. + */ + private static final String MAVEN_WRAPPER_PROPERTIES_PATH = + ".mvn/wrapper/maven-wrapper.properties"; + + /** + * Path where the maven-wrapper.jar will be saved to. + */ + private static final String MAVEN_WRAPPER_JAR_PATH = + ".mvn/wrapper/maven-wrapper.jar"; + + /** + * Name of the property which should be used to override the default download url for the wrapper. + */ + private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; + + public static void main(String args[]) { + System.out.println("- Downloader started"); + File baseDirectory = new File(args[0]); + System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); + + // If the maven-wrapper.properties exists, read it and check if it contains a custom + // wrapperUrl parameter. + File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); + String url = DEFAULT_DOWNLOAD_URL; + if(mavenWrapperPropertyFile.exists()) { + FileInputStream mavenWrapperPropertyFileInputStream = null; + try { + mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); + Properties mavenWrapperProperties = new Properties(); + mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); + url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); + } catch (IOException e) { + System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); + } finally { + try { + if(mavenWrapperPropertyFileInputStream != null) { + mavenWrapperPropertyFileInputStream.close(); + } + } catch (IOException e) { + // Ignore ... + } + } + } + System.out.println("- Downloading from: : " + url); + + File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); + if(!outputFile.getParentFile().exists()) { + if(!outputFile.getParentFile().mkdirs()) { + System.out.println( + "- ERROR creating output direcrory '" + outputFile.getParentFile().getAbsolutePath() + "'"); + } + } + System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); + try { + downloadFileFromURL(url, outputFile); + System.out.println("Done"); + System.exit(0); + } catch (Throwable e) { + System.out.println("- Error downloading"); + e.printStackTrace(); + System.exit(1); + } + } + + private static void downloadFileFromURL(String urlString, File destination) throws Exception { + URL website = new URL(urlString); + ReadableByteChannel rbc; + rbc = Channels.newChannel(website.openStream()); + FileOutputStream fos = new FileOutputStream(destination); + fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); + fos.close(); + rbc.close(); + } + +} diff --git a/commons/.mvn/wrapper/maven-wrapper.jar b/commons/.mvn/wrapper/maven-wrapper.jar new file mode 100755 index 0000000000000000000000000000000000000000..01e67997377a393fd672c7dcde9dccbedf0cb1e9 GIT binary patch literal 48337 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 + + + + + 4.0.0 + + + com.expedia.www + haystack-commons-parent + 1.0.66-SNAPSHOT + + + haystack-commons + jar + + + + com.expedia.www + haystack-idl-java + ${project.version} + + + + com.google.guava + guava + provided + + + + + javax.annotation + javax.annotation-api + provided + + + + com.typesafe + config + provided + + + + + org.json4s + json4s-jackson_${scala.major.minor.version} + provided + + + + org.codehaus.mojo + build-helper-maven-plugin + provided + + + org.slf4j + slf4j-api + + + org.slf4j + slf4j-nop + + + org.slf4j + slf4j-jdk14 + + + org.slf4j + jcl-over-slf4j + + + + + + org.scala-lang + scala-library + + + + org.slf4j + slf4j-api + + + + com.google.protobuf + protobuf-java + + + + com.expedia + metrics-java + + + + io.grpc + grpc-protobuf + provided + + + io.grpc + grpc-stub + provided + + + + com.codahale.metrics + metrics-core + provided + + + + commons-codec + commons-codec + + + + + org.apache.kafka + kafka_${scala.major.minor.version} + provided + + + + org.apache.kafka + kafka-streams + provided + + + + org.msgpack + msgpack-core + provided + + + + + + org.scalatest + scalatest_${scala.major.minor.version} + test + + + + org.easymock + easymock + test + + + + + + org.pegdown + pegdown + test + + + + + + ${basedir}/src/main/scala + + + + ${basedir}/src/main/resources + true + + + + + + org.codehaus.mojo + build-helper-maven-plugin + + + org.apache.maven.plugins + maven-compiler-plugin + + + + net.alchim31.maven + scala-maven-plugin + + + + org.scalatest + scalatest-maven-plugin + + + + org.scalastyle + scalastyle-maven-plugin + + + + org.scoverage + scoverage-maven-plugin + + + + org.apache.maven.plugins + maven-source-plugin + + + org.apache.maven.plugins + maven-javadoc-plugin + + + org.apache.maven.plugins + maven-jar-plugin + + + org.apache.maven.plugins + maven-gpg-plugin + + + org.sonatype.plugins + nexus-staging-maven-plugin + true + + ossrh + https://oss.sonatype.org/ + true + + + + + diff --git a/commons/commons/src/main/scala/com/expedia/www/haystack/commons/config/ConfigurationLoader.scala b/commons/commons/src/main/scala/com/expedia/www/haystack/commons/config/ConfigurationLoader.scala new file mode 100644 index 000000000..569c8acb5 --- /dev/null +++ b/commons/commons/src/main/scala/com/expedia/www/haystack/commons/config/ConfigurationLoader.scala @@ -0,0 +1,117 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.commons.config + +import java.io.File + +import com.typesafe.config.{Config, ConfigFactory, ConfigRenderOptions, ConfigValueType} +import org.slf4j.LoggerFactory + +import scala.collection.JavaConverters._ + +object ConfigurationLoader { + + private val LOGGER = LoggerFactory.getLogger(ConfigurationLoader.getClass) + + private[haystack] val ENV_NAME_PREFIX = "HAYSTACK_PROP_" + + /** + * Load and return the configuration + * if overrides_config_path env variable exists, then we load that config file and use base conf as fallback, + * else we load the config from env variables(prefixed with haystack) and use base conf as fallback + * + * @param resourceName name of the resource file to be loaded. Default value is `config/base.conf` + * @param envNamePrefix env variable prefix to override config values. Default is `HAYSTACK_PROP_` + * + * @return an instance of com.typesafe.Config + */ + def loadConfigFileWithEnvOverrides(resourceName : String = "config/base.conf", + envNamePrefix : String = ENV_NAME_PREFIX) : Config = { + + require(resourceName != null && resourceName.length > 0 , "resourceName is required") + require(envNamePrefix != null && envNamePrefix.length > 0 , "envNamePrefix is required") + + val baseConfig = ConfigFactory.load(resourceName) + + val keysWithArrayValues = baseConfig.entrySet() + .asScala + .filter(_.getValue.valueType() == ConfigValueType.LIST) + .map(_.getKey) + .toSet + + val config = sys.env.get("HAYSTACK_OVERRIDES_CONFIG_PATH") match { + case Some(overrideConfigPath) => + val overrideConfig = ConfigFactory.parseFile(new File(overrideConfigPath)) + ConfigFactory + .parseMap(parsePropertiesFromMap(sys.env, keysWithArrayValues, envNamePrefix).asJava) + .withFallback(overrideConfig) + .withFallback(baseConfig) + .resolve() + case _ => ConfigFactory + .parseMap(parsePropertiesFromMap(sys.env, keysWithArrayValues, envNamePrefix).asJava) + .withFallback(baseConfig) + .resolve() + } + + // In key-value pairs that contain 'password' in the key, replace the value with asterisks + LOGGER.info(config.root() + .render(ConfigRenderOptions.defaults().setOriginComments(false)) + .replaceAll("(?i)(\\\".*password\\\"\\s*:\\s*)\\\".+\\\"", "$1********")) + + config + } + + /** + * @return new config object with haystack specific environment variables + */ + private[haystack] def parsePropertiesFromMap(envVars: Map[String, String], + keysWithArrayValues: Set[String], + envNamePrefix: String): Map[String, Object] = { + envVars.filter { + case (envName, _) => envName.startsWith(envNamePrefix) + } map { + case (envName, envValue) => + val key = transformEnvVarName(envName, envNamePrefix) + if (keysWithArrayValues.contains(key)) (key, transformEnvVarArrayValue(envValue)) else (key, envValue) + } + } + + /** + * converts the env variable to HOCON format + * for e.g. env variable HAYSTACK_KAFKA_STREAMS_NUM_STREAM_THREADS gets converted to kafka.streams.num.stream.threads + * @param env environment variable name + * @return variable name that complies with hocon key + */ + private def transformEnvVarName(env: String, envNamePrefix: String): String = { + env.replaceFirst(envNamePrefix, "").toLowerCase.replace("_", ".") + } + + /** + * converts the env variable value to iterable object if it starts and ends with '[' and ']' respectively. + * @param env environment variable value + * @return string or iterable object + */ + private def transformEnvVarArrayValue(env: String): java.util.List[String] = { + if (env.startsWith("[") && env.endsWith("]")) { + import scala.collection.JavaConverters._ + env.substring(1, env.length - 1).split(',').filter(str => (str != null) && str.nonEmpty).toList.asJava + } else { + throw new RuntimeException("config key is of array type, so it should start and end with '[', ']' respectively") + } + } +} diff --git a/commons/commons/src/main/scala/com/expedia/www/haystack/commons/entities/GraphEdge.scala b/commons/commons/src/main/scala/com/expedia/www/haystack/commons/entities/GraphEdge.scala new file mode 100644 index 000000000..373772aaa --- /dev/null +++ b/commons/commons/src/main/scala/com/expedia/www/haystack/commons/entities/GraphEdge.scala @@ -0,0 +1,28 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.commons.entities + + +/** + * Case class with enough information to build a relationship between two service graph nodes + * @param source identifier for the source graph node + * @param destination identifier for the destination graph node + * @param operation identifier for the graph edge + * @param sourceTimestamp timestamp of source in millis + */ +case class GraphEdge(source: GraphVertex, destination: GraphVertex, operation: String, sourceTimestamp: Long = System.currentTimeMillis()) \ No newline at end of file diff --git a/commons/commons/src/main/scala/com/expedia/www/haystack/commons/entities/GraphVertex.scala b/commons/commons/src/main/scala/com/expedia/www/haystack/commons/entities/GraphVertex.scala new file mode 100644 index 000000000..b9c3ce259 --- /dev/null +++ b/commons/commons/src/main/scala/com/expedia/www/haystack/commons/entities/GraphVertex.scala @@ -0,0 +1,27 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.commons.entities + + +/** + * Vertex of a graph that includes the name of the vertex and tags associated with the vertex + * @param name: Name of the service vertex + * @param tags: List of tag names associated with the service vertex + */ +case class GraphVertex(name: String, tags: Map[String, String] = Map.empty[String, String]) + diff --git a/commons/commons/src/main/scala/com/expedia/www/haystack/commons/entities/Interval.scala b/commons/commons/src/main/scala/com/expedia/www/haystack/commons/entities/Interval.scala new file mode 100644 index 000000000..6b516aa6f --- /dev/null +++ b/commons/commons/src/main/scala/com/expedia/www/haystack/commons/entities/Interval.scala @@ -0,0 +1,57 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.commons.entities + +/** + * This enum contains the metric intervals supported by the app + */ +object Interval extends Enumeration { + type Interval = IntervalVal + val ONE_MINUTE = IntervalVal("OneMinute", 60) + val FIVE_MINUTE = IntervalVal("FiveMinute", 300) + val FIFTEEN_MINUTE = IntervalVal("FifteenMinute", 900) + val ONE_HOUR = IntervalVal("OneHour", 3600) + + def all: List[Interval] = { + List(ONE_MINUTE, FIVE_MINUTE, FIFTEEN_MINUTE, ONE_HOUR) + } + + def fromName(name: String): IntervalVal = { + name match { + case "OneMinute" => ONE_MINUTE + case "FiveMinute" => FIVE_MINUTE + case "FifteenMinute" => FIFTEEN_MINUTE + case "OneHour" => ONE_HOUR + case _ => ONE_MINUTE + } + } + + def fromVal(value: Long): IntervalVal = { + value match { + case 60 => ONE_MINUTE + case 300 => FIVE_MINUTE + case 900 => FIFTEEN_MINUTE + case 3600 => ONE_HOUR + case _ => ONE_MINUTE + } + } + + sealed case class IntervalVal(name: String, timeInSeconds: Int) extends Val(name) { + } + +} + diff --git a/commons/commons/src/main/scala/com/expedia/www/haystack/commons/entities/TagKeys.scala b/commons/commons/src/main/scala/com/expedia/www/haystack/commons/entities/TagKeys.scala new file mode 100644 index 000000000..8b415ef05 --- /dev/null +++ b/commons/commons/src/main/scala/com/expedia/www/haystack/commons/entities/TagKeys.scala @@ -0,0 +1,56 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.commons.entities + +/** +The Tag keys are according to metrics 2.0 specifications see http://metrics20.org/spec/#tag-keys + */ +object TagKeys { + /** + * OPERATION_NAME_KEY is a identifier for the operation name specified in haystack. + */ + val OPERATION_NAME_KEY = "operationName" + /** + * SERVICE_NAME_KEY is a identifier for the service name specified in haystack. + */ + val SERVICE_NAME_KEY = "serviceName" + /** + * RESULT_KEY is a identifier for the values: such as ok, fail + */ + val RESULT_KEY = "result" + /** + * STATS_KEY is a identifier to clarify the statistical view + */ + val STATS_KEY = "stat" + /** + * ERROR_KEY is a identifier to specify whether a span is a success or failure. Useful in trending for success or + * failure count + */ + val ERROR_KEY = "error" + /** + * INTERVAL_KEY is a identifier to specify whether the interval of a trend. For eg: OneMinute, FiveMinute etc + */ + val INTERVAL_KEY = "interval" + /** + * ORG_ID_KEY is a identifier to specify the organization sending the span/trend. + */ + val ORG_ID_KEY = "orgId" + /** + * PRODUCT_KEY is a identifier to specify the namespace of the trend. + */ + val PRODUCT_KEY = "product" +} diff --git a/commons/commons/src/main/scala/com/expedia/www/haystack/commons/entities/encoders/Base64Encoder.scala b/commons/commons/src/main/scala/com/expedia/www/haystack/commons/entities/encoders/Base64Encoder.scala new file mode 100644 index 000000000..4276f44b9 --- /dev/null +++ b/commons/commons/src/main/scala/com/expedia/www/haystack/commons/entities/encoders/Base64Encoder.scala @@ -0,0 +1,32 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.commons.entities.encoders + +import java.nio.charset.StandardCharsets + +import com.google.common.base.Charsets +import com.google.common.io.BaseEncoding + +class Base64Encoder extends Encoder { + def encode(value: String): String = { + BaseEncoding.base64().withPadChar('_').encode(value.getBytes(Charsets.UTF_8)) + } + + def decode(value: String): String = { + new String(BaseEncoding.base64().withPadChar('_').decode(value), StandardCharsets.UTF_8) + } +} diff --git a/commons/commons/src/main/scala/com/expedia/www/haystack/commons/entities/encoders/Encoder.scala b/commons/commons/src/main/scala/com/expedia/www/haystack/commons/entities/encoders/Encoder.scala new file mode 100644 index 000000000..88557970c --- /dev/null +++ b/commons/commons/src/main/scala/com/expedia/www/haystack/commons/entities/encoders/Encoder.scala @@ -0,0 +1,24 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.commons.entities.encoders + +trait Encoder { + + def encode(value: String): String + + def decode(value: String): String +} diff --git a/commons/commons/src/main/scala/com/expedia/www/haystack/commons/entities/encoders/EncoderFactory.scala b/commons/commons/src/main/scala/com/expedia/www/haystack/commons/entities/encoders/EncoderFactory.scala new file mode 100644 index 000000000..eab806a10 --- /dev/null +++ b/commons/commons/src/main/scala/com/expedia/www/haystack/commons/entities/encoders/EncoderFactory.scala @@ -0,0 +1,32 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.commons.entities.encoders + +object EncoderFactory { + final val BASE_64 = "base64" + final val PERIOD_REPLACEMENT = "periodReplacement" + + def newInstance(key: String): Encoder = { + if (BASE_64.equalsIgnoreCase(key)) { + new Base64Encoder() + } else if (PERIOD_REPLACEMENT.equalsIgnoreCase(key)) { + new PeriodReplacementEncoder() + } else { + new NoopEncoder() + } + } +} diff --git a/commons/commons/src/main/scala/com/expedia/www/haystack/commons/entities/encoders/NoopEncoder.scala b/commons/commons/src/main/scala/com/expedia/www/haystack/commons/entities/encoders/NoopEncoder.scala new file mode 100644 index 000000000..156214481 --- /dev/null +++ b/commons/commons/src/main/scala/com/expedia/www/haystack/commons/entities/encoders/NoopEncoder.scala @@ -0,0 +1,27 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.commons.entities.encoders + +class NoopEncoder extends Encoder { + def encode(value: String): String = { + value + } + + def decode(value: String): String = { + value + } +} diff --git a/commons/commons/src/main/scala/com/expedia/www/haystack/commons/entities/encoders/PeriodReplacementEncoder.scala b/commons/commons/src/main/scala/com/expedia/www/haystack/commons/entities/encoders/PeriodReplacementEncoder.scala new file mode 100644 index 000000000..774cd5e40 --- /dev/null +++ b/commons/commons/src/main/scala/com/expedia/www/haystack/commons/entities/encoders/PeriodReplacementEncoder.scala @@ -0,0 +1,27 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.commons.entities.encoders + +class PeriodReplacementEncoder extends Encoder { + def encode(value: String): String = { + value.replace(".", "___") + } + + def decode(value: String): String = { + value.replace("___", ".") + } +} diff --git a/commons/commons/src/main/scala/com/expedia/www/haystack/commons/graph/GraphEdgeTagCollector.scala b/commons/commons/src/main/scala/com/expedia/www/haystack/commons/graph/GraphEdgeTagCollector.scala new file mode 100644 index 000000000..fd4127eed --- /dev/null +++ b/commons/commons/src/main/scala/com/expedia/www/haystack/commons/graph/GraphEdgeTagCollector.scala @@ -0,0 +1,59 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.commons.graph + +import com.expedia.open.tracing.Span +import com.expedia.open.tracing.Tag.TagType +import com.expedia.www.haystack.commons.entities.TagKeys + +import scala.collection.JavaConverters._ +import scala.collection.mutable + +/** + * Define tag names that should be collected when building a GraphEdge. + * @param tags: Set of tag names to be collected for building the graph edge + */ +class GraphEdgeTagCollector(tags: Set[String] = Set()) { + + /** + * Default tags that will always be collected. + */ + private val defaultTags: Set[String] = Set(TagKeys.ERROR_KEY) + + private val filteredTags = defaultTags ++ tags + + /** + * @param span: Span containing all the tags + * @return Filtered list of tag keys and values in the span that match the pre defined tag names. + */ + def collectTags(span: Span): Map[String, String] = { + val edgeTags = mutable.Map[String, String]() + span.getTagsList.asScala.filter(t => filteredTags.contains(t.getKey)).foreach { tag => + tag.getType match { + case TagType.STRING => edgeTags += (tag.getKey -> tag.getVStr) + case TagType.BOOL => edgeTags += (tag.getKey -> tag.getVBool.toString) + case TagType.DOUBLE => edgeTags += (tag.getKey -> tag.getVDouble.toString) + case TagType.LONG => edgeTags += (tag.getKey -> tag.getVLong.toString) + case _ => throw new IllegalArgumentException("Invalid tag type detected.") + } + } + edgeTags.toMap + } +} + + diff --git a/commons/commons/src/main/scala/com/expedia/www/haystack/commons/health/HealthController.scala b/commons/commons/src/main/scala/com/expedia/www/haystack/commons/health/HealthController.scala new file mode 100644 index 000000000..f3299709d --- /dev/null +++ b/commons/commons/src/main/scala/com/expedia/www/haystack/commons/health/HealthController.scala @@ -0,0 +1,90 @@ +/* + * + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.commons.health + +import java.util.concurrent.atomic.AtomicReference + +import com.expedia.www.haystack.commons.health.HealthStatus.HealthStatus +import org.slf4j.LoggerFactory + +import scala.collection.mutable + +object HealthStatus extends Enumeration { + type HealthStatus = Value + val HEALTHY, UNHEALTHY, NOT_SET = Value +} + +/** + * provides the health check of app + */ +class HealthStatusController { + private val LOGGER = LoggerFactory.getLogger(classOf[HealthStatusController]) + private val status = new AtomicReference[HealthStatus](HealthStatus.NOT_SET) + private var listeners = mutable.ListBuffer[HealthStatusChangeListener]() + + def setHealthy(): Unit = { + LOGGER.info("Setting the app status as 'HEALTHY'") + if(status.getAndSet(HealthStatus.HEALTHY) != HealthStatus.HEALTHY) notifyChange(HealthStatus.HEALTHY) + } + + def setUnhealthy(): Unit = { + LOGGER.error("Setting the app status as 'UNHEALTHY'") + if(status.getAndSet(HealthStatus.UNHEALTHY) != HealthStatus.UNHEALTHY) notifyChange(HealthStatus.UNHEALTHY) + } + + def isHealthy: Boolean = status.get() == HealthStatus.HEALTHY + + def addListener(l: HealthStatusChangeListener): Unit = listeners += l + + private def notifyChange(status: HealthStatus): Unit = { + listeners foreach { + l => + l.onChange(status) + } + } +} + +object HealthController { + private val healthController = new HealthStatusController + + /** + * set the app status as health + */ + def setHealthy(): Unit = { + healthController.setHealthy() + } + + /** + * set the app status as unhealthy + */ + def setUnhealthy(): Unit = { + healthController.setUnhealthy() + } + + /** + * @return true if app is healthy else false + */ + def isHealthy: Boolean = healthController.isHealthy + + /** + * add health change listener that will be called on any change in the health status + * @param l listener + */ + def addListener(l: HealthStatusChangeListener): Unit = healthController.addListener(l) +} diff --git a/commons/commons/src/main/scala/com/expedia/www/haystack/commons/health/HealthStatusChangeListener.scala b/commons/commons/src/main/scala/com/expedia/www/haystack/commons/health/HealthStatusChangeListener.scala new file mode 100644 index 000000000..da71e2ce7 --- /dev/null +++ b/commons/commons/src/main/scala/com/expedia/www/haystack/commons/health/HealthStatusChangeListener.scala @@ -0,0 +1,33 @@ +/* + * + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.commons.health + +import com.expedia.www.haystack.commons.health.HealthStatus.HealthStatus + +/** + * health status listener + */ +trait HealthStatusChangeListener { + + /** + * called whenever there there is a state change in health + * @param status + */ + def onChange(status: HealthStatus): Unit +} diff --git a/commons/commons/src/main/scala/com/expedia/www/haystack/commons/health/UpdateHealthStatusFile.scala b/commons/commons/src/main/scala/com/expedia/www/haystack/commons/health/UpdateHealthStatusFile.scala new file mode 100644 index 000000000..c8329322a --- /dev/null +++ b/commons/commons/src/main/scala/com/expedia/www/haystack/commons/health/UpdateHealthStatusFile.scala @@ -0,0 +1,41 @@ +/* + * + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.commons.health + +import java.nio.charset.StandardCharsets +import java.nio.file.{Files, Paths} + +import com.expedia.www.haystack.commons.health.HealthStatus.HealthStatus + +/** + * writes the current health status to a status file. This can be used to provide the health to external system + * like container orchestration frameworks + * @param statusFilePath: file path where health status will be recorded. + */ +class UpdateHealthStatusFile(statusFilePath: String) extends HealthStatusChangeListener { + + /** + * call on the any change in health status of app + * @param status: current health status + */ + override def onChange(status: HealthStatus): Unit = { + val isHealthy = if(status == HealthStatus.HEALTHY) "true" else "false" + Files.write(Paths.get(statusFilePath), isHealthy.getBytes(StandardCharsets.UTF_8)) + } +} \ No newline at end of file diff --git a/commons/commons/src/main/scala/com/expedia/www/haystack/commons/kstreams/GraphEdgeTimestampExtractor.scala b/commons/commons/src/main/scala/com/expedia/www/haystack/commons/kstreams/GraphEdgeTimestampExtractor.scala new file mode 100644 index 000000000..ce6d9f43a --- /dev/null +++ b/commons/commons/src/main/scala/com/expedia/www/haystack/commons/kstreams/GraphEdgeTimestampExtractor.scala @@ -0,0 +1,34 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.commons.kstreams + +import com.expedia.www.haystack.commons.entities.GraphEdge +import org.apache.kafka.clients.consumer.ConsumerRecord +import org.apache.kafka.streams.processor.TimestampExtractor + + +class GraphEdgeTimestampExtractor extends TimestampExtractor with IteratorAgeMetricSupport { + override def extract(consumerRecord: ConsumerRecord[AnyRef, AnyRef], previousTimestamp: Long): Long = { + + // sourceTimestamp of GraphEdge in millis + val sourceTimestampMs = consumerRecord.value().asInstanceOf[GraphEdge].sourceTimestamp + updateIteratorAge(sourceTimestampMs) + sourceTimestampMs + } +} diff --git a/commons/commons/src/main/scala/com/expedia/www/haystack/commons/kstreams/IteratorAgeMetricSupport.scala b/commons/commons/src/main/scala/com/expedia/www/haystack/commons/kstreams/IteratorAgeMetricSupport.scala new file mode 100644 index 000000000..ba2d59e7a --- /dev/null +++ b/commons/commons/src/main/scala/com/expedia/www/haystack/commons/kstreams/IteratorAgeMetricSupport.scala @@ -0,0 +1,13 @@ +package com.expedia.www.haystack.commons.kstreams + +import com.codahale.metrics.Histogram +import com.expedia.www.haystack.commons.metrics.MetricsSupport + +trait IteratorAgeMetricSupport extends MetricsSupport { + + val iteratorAge: Histogram = metricRegistry.histogram("kafka.iterator.age.ms") + + def updateIteratorAge(timeInMs: Long): Unit = { + iteratorAge.update(System.currentTimeMillis() - timeInMs) + } +} diff --git a/commons/commons/src/main/scala/com/expedia/www/haystack/commons/kstreams/MetricDataTimestampExtractor.scala b/commons/commons/src/main/scala/com/expedia/www/haystack/commons/kstreams/MetricDataTimestampExtractor.scala new file mode 100644 index 000000000..81715988f --- /dev/null +++ b/commons/commons/src/main/scala/com/expedia/www/haystack/commons/kstreams/MetricDataTimestampExtractor.scala @@ -0,0 +1,36 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.commons.kstreams + +import com.expedia.metrics.MetricData +import org.apache.kafka.clients.consumer.ConsumerRecord +import org.apache.kafka.streams.processor.TimestampExtractor + +class MetricDataTimestampExtractor extends TimestampExtractor with IteratorAgeMetricSupport { + + override def extract(record: ConsumerRecord[AnyRef, AnyRef], previousTimestamp: Long): Long = { + + //The startTime for metricData in computed in seconds and hence multiplying by 1000 to create the epochTimeInMs + val metricDataTimestampMs = record.value().asInstanceOf[MetricData].getTimestamp * 1000 + updateIteratorAge(metricDataTimestampMs) + metricDataTimestampMs + + } + +} diff --git a/commons/commons/src/main/scala/com/expedia/www/haystack/commons/kstreams/SpanTimestampExtractor.scala b/commons/commons/src/main/scala/com/expedia/www/haystack/commons/kstreams/SpanTimestampExtractor.scala new file mode 100644 index 000000000..8337ca4eb --- /dev/null +++ b/commons/commons/src/main/scala/com/expedia/www/haystack/commons/kstreams/SpanTimestampExtractor.scala @@ -0,0 +1,34 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.commons.kstreams + +import com.expedia.open.tracing.Span +import org.apache.kafka.clients.consumer.ConsumerRecord +import org.apache.kafka.streams.processor.TimestampExtractor + +class SpanTimestampExtractor extends TimestampExtractor with IteratorAgeMetricSupport { + + override def extract(record: ConsumerRecord[AnyRef, AnyRef], previousTimestamp: Long): Long = { + + //The startTime for span in computed in microseconds and hence dividing by 1000 to create the epochTimeInMs + val spanStartTimeMs = record.value().asInstanceOf[Span].getStartTime / 1000 + updateIteratorAge(spanStartTimeMs) + spanStartTimeMs + } +} diff --git a/commons/commons/src/main/scala/com/expedia/www/haystack/commons/kstreams/app/Main.scala b/commons/commons/src/main/scala/com/expedia/www/haystack/commons/kstreams/app/Main.scala new file mode 100644 index 000000000..fc71402f5 --- /dev/null +++ b/commons/commons/src/main/scala/com/expedia/www/haystack/commons/kstreams/app/Main.scala @@ -0,0 +1,103 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.commons.kstreams.app + +import java.util.concurrent.atomic.AtomicBoolean + +import com.codahale.metrics.JmxReporter +import com.expedia.www.haystack.commons.logger.LoggerUtils +import com.expedia.www.haystack.commons.metrics.MetricsSupport +import org.slf4j.LoggerFactory + +/** + * Starting point of a Kafka Streams application. One should extend this + * trait and provide a valid instance of `StreamsRunner` by overriding + * createStreamsRunner method to create and start a Kafka Streams application + */ +trait Main extends MetricsSupport { + + def main(args: Array[String]): Unit = { + //create an instance of the application + val jmxReporter: JmxReporter = JmxReporter.forRegistry(metricRegistry).build() + val app = new Application(createStreamsRunner(), jmxReporter) + + //start the application + app.start() + + //add a shutdown hook + Runtime.getRuntime.addShutdownHook(new Thread() { + override def run(): Unit = app.stop() + }) + } + + /** + * This method should create and return a new instance of the `StreamsRunner` class + *

That instance will be started and stopped as part of the application lifecycle + * @return Instance of `StreamsRunner` to be managed + */ + def createStreamsRunner(): StreamsRunner +} + +/** + * This is the main application class. This controls the application + * start and shutdown actions + * + * @param streamsRunner instance of StreamsRunner to start and stop the + * streams application + */ +class Application(streamsRunner: StreamsRunner, jmxReporter: JmxReporter) extends MetricsSupport { + + private val LOGGER = LoggerFactory.getLogger(classOf[Application]) + private val running = new AtomicBoolean(false) + + require(streamsRunner != null) + require(jmxReporter != null) + + /** + * Starts the given `StreamsRunner` and `JmxReporter` instances + */ + def start(): Unit = { + //start JMX reporter for metricRegistry + jmxReporter.start() + + //start the topology + streamsRunner.start() + + //initialized + running.set(true) + } + + /** + * This method stops the given `StreamsRunner` and `JmxReporter` is they have been + * previously started. If not, this method does nothing + */ + def stop(): Unit = { + if (running.getAndSet(false)) { + LOGGER.info("Shutting down topology") + streamsRunner.close() + + LOGGER.info("Shutting down jmxReporter") + jmxReporter.close() + + LOGGER.info("Shutting down logger. Bye!") + LoggerUtils.shutdownLogger() + } + } +} + + diff --git a/commons/commons/src/main/scala/com/expedia/www/haystack/commons/kstreams/app/ManagedKafkaStreams.scala b/commons/commons/src/main/scala/com/expedia/www/haystack/commons/kstreams/app/ManagedKafkaStreams.scala new file mode 100644 index 000000000..ef8a38c4e --- /dev/null +++ b/commons/commons/src/main/scala/com/expedia/www/haystack/commons/kstreams/app/ManagedKafkaStreams.scala @@ -0,0 +1,68 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.commons.kstreams.app + +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean + +import org.apache.kafka.streams.KafkaStreams + +/** + * Simple service wrapper over `KafkaStreams` to manage the life cycle of the + * instance. + * + * @param kafkaStreams underlying KafkaStreams instance that needs to be + * managed + * @param closeWaitInSeconds time to wait in seconds while stopping KafkaStreams + */ +class ManagedKafkaStreams(kafkaStreams: KafkaStreams, closeWaitInSeconds: Int) extends ManagedService { + require(kafkaStreams != null) + private val isRunning: AtomicBoolean = new AtomicBoolean(false) + + /** + * This creates a managed KafkaStreams that waits for ever at + * stop. To provide a specific timeout use the other constructor + * + * @param kafkaStreams underlying KafkaStreams instance that needs to be + * managed + */ + def this(kafkaStreams: KafkaStreams) = this(kafkaStreams, 0) + + /** + * @see ManagedService.start + */ + override def start(): Unit = { + kafkaStreams.start() + isRunning.set(true) + } + + /** + * @see ManagedService.stop + */ + override def stop(): Unit = { + if (isRunning.getAndSet(false)) { + kafkaStreams.close(closeWaitInSeconds, TimeUnit.SECONDS) + } + } + + /** + * @see ManagedService.hasStarted + * @return + */ + override def hasStarted: Boolean = isRunning.get() +} diff --git a/commons/commons/src/main/scala/com/expedia/www/haystack/commons/kstreams/app/ManagedService.scala b/commons/commons/src/main/scala/com/expedia/www/haystack/commons/kstreams/app/ManagedService.scala new file mode 100644 index 000000000..8fd176fe2 --- /dev/null +++ b/commons/commons/src/main/scala/com/expedia/www/haystack/commons/kstreams/app/ManagedService.scala @@ -0,0 +1,44 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.commons.kstreams.app + +/** + * A simple trait for managing a service + */ +trait ManagedService { + /** + * This method is called when the service needs to be started + *

Any exception thrown by this method is propagated up the calling chain + *

After a successful start, `hasStarted` should return true + */ + def start() + + /** + * This method is called when the service needs to be stopped. + *

If the service has not been started, this method should do nothing or + * should have no side effect + *

After successfully stopping, `hasStarted` should return false + */ + def stop() + + /** + * Indicates the state of the service + * @return + */ + def hasStarted : Boolean +} diff --git a/commons/commons/src/main/scala/com/expedia/www/haystack/commons/kstreams/app/StateChangeListener.scala b/commons/commons/src/main/scala/com/expedia/www/haystack/commons/kstreams/app/StateChangeListener.scala new file mode 100644 index 000000000..b0edde1ae --- /dev/null +++ b/commons/commons/src/main/scala/com/expedia/www/haystack/commons/kstreams/app/StateChangeListener.scala @@ -0,0 +1,53 @@ +package com.expedia.www.haystack.commons.kstreams.app + +import com.expedia.www.haystack.commons.health.HealthStatusController +import org.apache.kafka.streams.KafkaStreams +import org.apache.kafka.streams.KafkaStreams.StateListener +import org.slf4j.LoggerFactory + +/** + * Watches the state of a KafkaStreams application and sets the health of the process + * using the provided `HealthStatusController` instance + * @param healthStatusController required instance of `HealthStatusController` that manages + * the state of the current process + */ +class StateChangeListener(healthStatusController: HealthStatusController) extends StateListener + with Thread.UncaughtExceptionHandler { + + require(healthStatusController != null) + + private val LOGGER = LoggerFactory.getLogger(classOf[StateChangeListener]) + + /** + * Method to set the status of the application + * @param healthy sets the state as healthy if this is true and unhealthy if the state is false + */ + def state(healthy : Boolean) : Unit = + if (healthy) { + healthStatusController.setHealthy() + } + else { + healthStatusController.setUnhealthy() + } + + /** + * This method is called when state of the KafkaStreams application changes. + * + * @param newState new state + * @param oldState previous state + */ + override def onChange(newState: KafkaStreams.State, oldState: KafkaStreams.State): Unit = { + LOGGER.info(s"State change event called with newState=$newState and oldState=$oldState") + } + + /** + * This method is invoked when the given thread terminates due to the + * given uncaught exception. + * @param t the thread that had an unhandled exception + * @param e the exception that caused the thread to terminate + */ + override def uncaughtException(t: Thread, e: Throwable): Unit = { + LOGGER.error(s"uncaught exception occurred running kafka streams for thread=${t.getName}", e) + state(false) + } +} diff --git a/commons/commons/src/main/scala/com/expedia/www/haystack/commons/kstreams/app/StreamsFactory.scala b/commons/commons/src/main/scala/com/expedia/www/haystack/commons/kstreams/app/StreamsFactory.scala new file mode 100644 index 000000000..4615e00f2 --- /dev/null +++ b/commons/commons/src/main/scala/com/expedia/www/haystack/commons/kstreams/app/StreamsFactory.scala @@ -0,0 +1,107 @@ +package com.expedia.www.haystack.commons.kstreams.app + +import java.util.Properties +import java.util.concurrent.TimeUnit +import java.util.function.Supplier + +import org.apache.kafka.clients.admin.AdminClient +import org.apache.kafka.streams.{KafkaStreams, StreamsConfig, Topology} + +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import org.slf4j.LoggerFactory + +import scala.util.Try + +/** + * Factory class to create a KafkaStreams instance and wrap it as a simple service {@see ManagedKafkaStreams} + * + * Optionally this class can check the presence of consuming topic + * + * @param topologySupplier A supplier that creates and returns a Kafka Stream Topology + * @param streamsConfig Configuration instance for KafkaStreams + * @param consumerTopic Optional consuming topic name + */ +class StreamsFactory(topologySupplier: Supplier[Topology], streamsConfig: StreamsConfig, consumerTopic: String) { + + require(topologySupplier != null, "streamsBuilder is required") + require(streamsConfig != null, "streamsConfig is required") + + val consumerTopicName = Option(consumerTopic) + + def this(streamsSupplier: Supplier[Topology], streamsConfig: StreamsConfig) = this(streamsSupplier, streamsConfig, null) + + private val LOGGER = LoggerFactory.getLogger(classOf[StreamsFactory]) + + /** + * creates a new instance of KafkaStreams application wrapped as a {@link ManagedService} instance + * @param listener instance of StateChangeListener that observes KafkaStreams state changes + * @return instance of ManagedService + */ + def create(listener: StateChangeListener): ManagedService = { + checkConsumerTopic() + + val streams = new KafkaStreams(topologySupplier.get(), streamsConfig) + streams.setStateListener(listener) + streams.setUncaughtExceptionHandler(listener) + streams.cleanUp() + + val timeOut = Option(streamsConfig.getInt(StreamsConfig.REQUEST_TIMEOUT_MS_CONFIG)) match { + case Some(v) if v > 0 => v / 1000 + case _ => 5 + } + + new ManagedKafkaStreams(streams, timeOut) + } + + private def checkConsumerTopic(): Unit = { + if (consumerTopicName.nonEmpty) { + val topicName = consumerTopicName.get + LOGGER.info(s"checking for the consumer topic $topicName") + val adminClient = AdminClient.create(getBootstrapProperties) + try { + val present = adminClient.listTopics().names().get().contains(topicName) + if (!present) { + throw new TopicNotPresentException(topicName, + s"Topic '$topicName' is configured as a consumer and it is not present") + } + } + finally { + Try(adminClient.close(5, TimeUnit.SECONDS)) + } + } + } + + private def getBootstrapProperties: Properties = { + val properties = new Properties() + properties.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, + streamsConfig.getList(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG)) + properties + } + + /** + * Custom RuntimeException that represents a required Kafka topic not being present + * @param topic Name of the topic that is missing + * @param message Message + */ + class TopicNotPresentException(topic: String, message: String) extends RuntimeException(message) { + def getTopic : String = topic + } +} + diff --git a/commons/commons/src/main/scala/com/expedia/www/haystack/commons/kstreams/app/StreamsRunner.scala b/commons/commons/src/main/scala/com/expedia/www/haystack/commons/kstreams/app/StreamsRunner.scala new file mode 100644 index 000000000..7c5c5d303 --- /dev/null +++ b/commons/commons/src/main/scala/com/expedia/www/haystack/commons/kstreams/app/StreamsRunner.scala @@ -0,0 +1,54 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.commons.kstreams.app + +import org.slf4j.LoggerFactory + +import scala.util.{Failure, Success, Try} + +class StreamsRunner(streamsFactory: StreamsFactory, stateChangeListener: StateChangeListener) extends AutoCloseable { + + private val LOGGER = LoggerFactory.getLogger(classOf[StreamsRunner]) + private var managedStreams : ManagedService = _ + + require(streamsFactory != null, "valid streamsFactory is required") + require(stateChangeListener != null, "valid stateChangeListener is required") + + def start(): Unit = { + LOGGER.info("Starting the given topology.") + + Try(streamsFactory.create(stateChangeListener)) match { + case Success(streams) => + managedStreams = streams + managedStreams.start() + stateChangeListener.state(true) + LOGGER.info("KafkaStreams started successfully") + case Failure(e) => + LOGGER.error(s"KafkaStreams failed to start : ${e.getMessage}", e) + stateChangeListener.state(false) + } + } + + def close(): Unit = { + if (managedStreams != null) { + managedStreams.stop() + } + } +} + + diff --git a/commons/commons/src/main/scala/com/expedia/www/haystack/commons/kstreams/serde/SpanSerde.scala b/commons/commons/src/main/scala/com/expedia/www/haystack/commons/kstreams/serde/SpanSerde.scala new file mode 100644 index 000000000..c0fd3a859 --- /dev/null +++ b/commons/commons/src/main/scala/com/expedia/www/haystack/commons/kstreams/serde/SpanSerde.scala @@ -0,0 +1,75 @@ +package com.expedia.www.haystack.commons.kstreams.serde + +import java.util + +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import com.expedia.open.tracing.Span +import com.expedia.www.haystack.commons.metrics.MetricsSupport +import org.apache.kafka.common.serialization.{Deserializer, Serde, Serializer} + +class SpanSerde extends Serde[Span] with MetricsSupport { + + override def configure(configs: util.Map[String, _], b: Boolean): Unit = () + + override def close(): Unit = () + + def serializer: Serializer[Span] = { + new SpanSerializer + } + + def deserializer: Deserializer[Span] = { + new SpanDeserializer + } +} + +class SpanSerializer extends Serializer[Span] { + override def configure(configs: util.Map[String, _], b: Boolean): Unit = () + + override def close(): Unit = () + + override def serialize(topic: String, obj: Span): Array[Byte] = if (obj != null) obj.toByteArray else null +} + +class SpanDeserializer extends Deserializer[Span] with MetricsSupport { + private val spanSerdeMeter = metricRegistry.meter("span.serde.failure") + + override def configure(configs: util.Map[String, _], b: Boolean): Unit = () + + override def close(): Unit = () + + override def deserialize(topic: String, data: Array[Byte]): Span = performDeserialize(data) + + /** + * converts the binary protobuf bytes into Span object + * + * @param data serialized bytes of Span + * @return + */ + private def performDeserialize(data: Array[Byte]): Span = { + try { + if (data == null || data.length == 0) null else Span.parseFrom(data) + } catch { + case _: Exception => + /* may be log and add metric */ + spanSerdeMeter.mark() + null + } + } +} diff --git a/commons/commons/src/main/scala/com/expedia/www/haystack/commons/kstreams/serde/graph/GraphEdgeKeySerde.scala b/commons/commons/src/main/scala/com/expedia/www/haystack/commons/kstreams/serde/graph/GraphEdgeKeySerde.scala new file mode 100644 index 000000000..3754cb035 --- /dev/null +++ b/commons/commons/src/main/scala/com/expedia/www/haystack/commons/kstreams/serde/graph/GraphEdgeKeySerde.scala @@ -0,0 +1,62 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.commons.kstreams.serde.graph + +import java.util + +import com.expedia.www.haystack.commons.entities.{GraphEdge, GraphVertex} +import org.apache.kafka.common.serialization.{Deserializer, Serde, Serializer} +import org.json4s.DefaultFormats +import org.json4s.jackson.Serialization + +class GraphEdgeKeySerde extends Serde[GraphEdge] { + implicit val formats = DefaultFormats + override def deserializer(): Deserializer[GraphEdge] = new GraphEdgeKeyDeserializer() + + override def serializer(): Serializer[GraphEdge] = new GraphEdgeKeySerializer() + + override def configure(map: util.Map[String, _], b: Boolean): Unit = () + + override def close(): Unit = () + + + class GraphEdgeKeyDeserializer extends Deserializer[GraphEdge] { + override def configure(map: util.Map[String, _], b: Boolean): Unit = () + + override def close(): Unit = () + + override def deserialize(topic: String, data: Array[Byte]): GraphEdge = { + Serialization.read[GraphEdge](new String(data)) + } + } + + class GraphEdgeKeySerializer extends Serializer[GraphEdge] { + override def configure(map: util.Map[String, _], b: Boolean): Unit = () + + override def serialize(topic: String, edge: GraphEdge): Array[Byte] = { + Serialization.write(normalizeKey(edge)).getBytes("utf-8") + } + + override def close(): Unit = () + } + + private def normalizeKey(edge: GraphEdge): GraphEdge = { + GraphEdge(source = GraphVertex(edge.source.name), destination = GraphVertex(edge.destination.name), edge.operation, 0l) + } +} diff --git a/commons/commons/src/main/scala/com/expedia/www/haystack/commons/kstreams/serde/graph/GraphEdgeValueSerde.scala b/commons/commons/src/main/scala/com/expedia/www/haystack/commons/kstreams/serde/graph/GraphEdgeValueSerde.scala new file mode 100644 index 000000000..264f191e8 --- /dev/null +++ b/commons/commons/src/main/scala/com/expedia/www/haystack/commons/kstreams/serde/graph/GraphEdgeValueSerde.scala @@ -0,0 +1,58 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.commons.kstreams.serde.graph + +import java.util + +import com.expedia.www.haystack.commons.entities.GraphEdge +import org.apache.kafka.common.serialization.{Deserializer, Serde, Serializer} +import org.json4s.DefaultFormats +import org.json4s.jackson.Serialization + +class GraphEdgeValueSerde extends Serde[GraphEdge] { + implicit val formats = DefaultFormats + + override def deserializer(): Deserializer[GraphEdge] = new GraphEdgeDeserializer + + override def serializer(): Serializer[GraphEdge] = new GraphEdgeSerializer + + override def configure(map: util.Map[String, _], b: Boolean): Unit = () + + override def close(): Unit = () + + class GraphEdgeSerializer extends Serializer[GraphEdge] { + override def serialize(topic: String, graphEdge: GraphEdge): Array[Byte] = { + Serialization.write(graphEdge).getBytes("utf-8") + } + + override def configure(map: util.Map[String, _], b: Boolean): Unit = () + + override def close(): Unit = () + } + + class GraphEdgeDeserializer extends Deserializer[GraphEdge] { + override def deserialize(topic: String, data: Array[Byte]): GraphEdge = { + Serialization.read[GraphEdge](new String(data)) + } + + override def configure(map: util.Map[String, _], b: Boolean): Unit = () + + override def close(): Unit = () + } +} + diff --git a/commons/commons/src/main/scala/com/expedia/www/haystack/commons/kstreams/serde/metricdata/MetricDataSerde.scala b/commons/commons/src/main/scala/com/expedia/www/haystack/commons/kstreams/serde/metricdata/MetricDataSerde.scala new file mode 100644 index 000000000..963a0c7f8 --- /dev/null +++ b/commons/commons/src/main/scala/com/expedia/www/haystack/commons/kstreams/serde/metricdata/MetricDataSerde.scala @@ -0,0 +1,161 @@ +/* + * Copyright 2019 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.commons.kstreams.serde.metricdata + +import java.nio.ByteBuffer +import java.util + +import com.expedia.metrics.{MetricData, MetricDefinition, TagCollection} +import com.expedia.www.haystack.commons.metrics.MetricsSupport +import org.apache.kafka.common.serialization.{Deserializer, Serde, Serializer} +import org.msgpack.core.MessagePack.Code +import org.msgpack.core.{MessagePack, MessagePacker} +import org.msgpack.value.impl.ImmutableLongValueImpl +import org.msgpack.value.{Value, ValueFactory} + +import scala.collection.JavaConverters._ +import scala.collection.immutable.ListMap + +/** + * This class takes a metric data object and serializes it into a messagepack encoded bytestream + * which is metrics 2.0 format. The serialized data is finally streamed to kafka + */ +class MetricDataSerde() extends Serde[MetricData] with MetricsSupport { + + override def deserializer(): MetricDeserializer = { + new MetricDeserializer() + } + + override def serializer(): MetricSerializer = { + new MetricSerializer() + } + + override def configure(configs: java.util.Map[String, _], isKey: Boolean): Unit = () + + override def close(): Unit = () +} + +class MetricDeserializer() extends Deserializer[MetricData] with MetricsSupport { + + private val metricDataDeserFailureMeter = metricRegistry.meter("metricdata.deser.failure") + private val TAG_DELIMETER = "=" + private val metricKey = "Metric" + private val valueKey = "Value" + private val timeKey = "Time" + private val tagsKey = "Tags" + + + override def configure(map: java.util.Map[String, _], b: Boolean): Unit = () + + /** + * converts the messagepack bytes into MetricData object + * + * @param data serialized bytes of MetricData + * @return + */ + override def deserialize(topic: String, data: Array[Byte]): MetricData = { + try { + val unpacker = MessagePack.newDefaultUnpacker(data) + val metricData = unpacker.unpackValue().asMapValue().map() + val key = metricData.get(ValueFactory.newString(metricKey)).asStringValue().toString + val tags = createTags(metricData) + val metricDefinition = new MetricDefinition(key, new TagCollection(tags.asJava), TagCollection.EMPTY) + new MetricData(metricDefinition, metricData.get(ValueFactory.newString(valueKey)).asFloatValue().toDouble, + metricData.get(ValueFactory.newString(timeKey)).asIntegerValue().toLong) + } catch { + case ex: Exception => + /* may be log and add metric */ + metricDataDeserFailureMeter.mark() + null + } + } + + + private def createTags(metricData: util.Map[Value, Value]): Map[String, String] = { + ListMap(metricData.get(ValueFactory.newString(tagsKey)).asArrayValue().list().asScala.map(tag => { + val kvPairs = tag.toString.split(TAG_DELIMETER) + (kvPairs(0), kvPairs(1)) + }): _*) + } + + + override def close(): Unit = () +} + +class MetricSerializer() extends Serializer[MetricData] with MetricsSupport { + private val metricDataSerFailureMeter = metricRegistry.meter("metricdata.ser.failure") + private val metricDataSerSuccessMeter = metricRegistry.meter("metricdata.ser.success") + private val metricKey = "Metric" + private val valueKey = "Value" + private val timeKey = "Time" + private val tagsKey = "Tags" + + override def configure(map: java.util.Map[String, _], b: Boolean): Unit = () + + override def serialize(topic: String, metricData: MetricData): Array[Byte] = { + try { + val packer = MessagePack.newDefaultBufferPacker() + + val metricDataMap = Map[Value, Value]( + ValueFactory.newString(metricKey) -> ValueFactory.newString(metricData.getMetricDefinition.getKey), + ValueFactory.newString(valueKey) -> ValueFactory.newFloat(metricData.getValue), + ValueFactory.newString(timeKey) -> new ImmutableSignedLongValueImpl(metricData.getTimestamp), + ValueFactory.newString(tagsKey) -> ValueFactory.newArray(retrieveTags(metricData).asJava) + ) + packer.packValue(ValueFactory.newMap(metricDataMap.asJava)) + val data = packer.toByteArray + metricDataSerSuccessMeter.mark() + data + } catch { + case ex: Exception => + /* may be log and add metric */ + metricDataSerFailureMeter.mark() + null + } + } + + private def retrieveTags(metricData: MetricData): List[Value] = { + getMetricTags(metricData).asScala.map(tuple => { + ValueFactory.newString(s"${tuple._1}=${tuple._2}") + }).toList + } + + private def getMetricTags(metricData: MetricData): util.Map[String, String] = { + metricData.getMetricDefinition.getTags.getKv + } + + override def close(): Unit = () + + /** + * This is a value extention class for signed long type. The java client for messagepack packs positive longs as unsigned + * and there is no way to force a signed long who's numberal value is positive. + * Metric Tank schema requres a signed long type for the timestamp key. + * + * @param long + */ + class ImmutableSignedLongValueImpl(long: Long) extends ImmutableLongValueImpl(long) { + + override def writeTo(pk: MessagePacker) { + val buffer = ByteBuffer.allocate(java.lang.Long.BYTES + 1) + buffer.put(Code.INT64) + buffer.putLong(long) + pk.addPayload(buffer.array()) + } + } + +} diff --git a/commons/commons/src/main/scala/com/expedia/www/haystack/commons/kstreams/serde/metricdata/MetricTankSerde.scala b/commons/commons/src/main/scala/com/expedia/www/haystack/commons/kstreams/serde/metricdata/MetricTankSerde.scala new file mode 100644 index 000000000..459b24238 --- /dev/null +++ b/commons/commons/src/main/scala/com/expedia/www/haystack/commons/kstreams/serde/metricdata/MetricTankSerde.scala @@ -0,0 +1,205 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.commons.kstreams.serde.metricdata + +import java.nio.ByteBuffer +import java.util + +import com.expedia.metrics.{MetricData, MetricDefinition, TagCollection} +import com.expedia.www.haystack.commons.entities.TagKeys._ +import com.expedia.www.haystack.commons.entities.{Interval, TagKeys} +import com.expedia.www.haystack.commons.metrics.MetricsSupport +import org.apache.commons.codec.digest.DigestUtils +import org.apache.kafka.common.serialization.{Deserializer, Serde, Serializer} +import org.msgpack.core.MessagePack.Code +import org.msgpack.core.{MessagePack, MessagePacker} +import org.msgpack.value.impl.ImmutableLongValueImpl +import org.msgpack.value.{Value, ValueFactory} + +import scala.collection.JavaConverters._ +import scala.collection.immutable.ListMap + +/** + * This class takes a metric data object and serializes it into a messagepack encoded bytestream + * which can be directly consumed by metrictank. The serialized data is finally streamed to kafka + */ +class MetricTankSerde() extends Serde[MetricData] with MetricsSupport { + + override def deserializer(): MetricDataDeserializer = { + new MetricDataDeserializer() + } + + override def serializer(): MetricDataSerializer = { + new MetricDataSerializer() + } + + override def configure(configs: java.util.Map[String, _], isKey: Boolean): Unit = () + + override def close(): Unit = () +} + +class MetricDataDeserializer() extends Deserializer[MetricData] with MetricsSupport { + + private val metricPointDeserFailureMeter = metricRegistry.meter("metricpoint.deser.failure") + private val TAG_DELIMETER = "=" + private val metricKey = "Metric" + private val valueKey = "Value" + private val timeKey = "Time" + private val typeKey = "Mtype" + private val tagsKey = "Tags" + private val idKey = "Id" + + override def configure(map: java.util.Map[String, _], b: Boolean): Unit = () + + /** + * converts the messagepack bytes into MetricPoint object + * + * @param data serialized bytes of MetricPoint + * @return + */ + override def deserialize(topic: String, data: Array[Byte]): MetricData = { + try { + val unpacker = MessagePack.newDefaultUnpacker(data) + val metricData = unpacker.unpackValue().asMapValue().map() + val key = metricData.get(ValueFactory.newString(metricKey)).asStringValue().toString + val tags = createTags(metricData) + val metricDefinition = new MetricDefinition(key, new TagCollection(tags.asJava), TagCollection.EMPTY) + new MetricData(metricDefinition, metricData.get(ValueFactory.newString(valueKey)).asFloatValue().toDouble, + metricData.get(ValueFactory.newString(timeKey)).asIntegerValue().toLong) + } catch { + case ex: Exception => + /* may be log and add metric */ + metricPointDeserFailureMeter.mark() + null + } + } + + private def createMetricNameFromMetricKey(metricKey: String): String = { + metricKey.split("\\.").last + } + + + private def createTags(metricData: util.Map[Value, Value]): Map[String, String] = { + ListMap(metricData.get(ValueFactory.newString(tagsKey)).asArrayValue().list().asScala.map(tag => { + val kvPairs = tag.toString.split("=") + (kvPairs(0), kvPairs(1)) + }): _*) + } + + + override def close(): Unit = () +} + +class MetricDataSerializer() extends Serializer[MetricData] with MetricsSupport { + private val metricPointSerFailureMeter = metricRegistry.meter("metricpoint.ser.failure") + private val metricPointSerSuccessMeter = metricRegistry.meter("metricpoint.ser.success") + private val DEFAULT_ORG_ID = 1 + private[commons] val DEFAULT_INTERVAL_IN_SEC = 60 + private val idKey = "Id" + private val orgIdKey = "OrgId" + private val nameKey = "Name" + private val metricKey = "Metric" + private val valueKey = "Value" + private val timeKey = "Time" + private val typeKey = "Mtype" + private val tagsKey = "Tags" + private[commons] val intervalKey = "Interval" + + override def configure(map: java.util.Map[String, _], b: Boolean): Unit = () + + override def serialize(topic: String, metricData: MetricData): Array[Byte] = { + try { + val packer = MessagePack.newDefaultBufferPacker() + + val metricDataMap = Map[Value, Value]( + ValueFactory.newString(idKey) -> ValueFactory.newString(s"${getId(metricData)}"), + ValueFactory.newString(nameKey) -> ValueFactory.newString(metricData.getMetricDefinition.getKey), + ValueFactory.newString(orgIdKey) -> ValueFactory.newInteger(getOrgId(metricData)), + ValueFactory.newString(intervalKey) -> new ImmutableSignedLongValueImpl(retrieveInterval(metricData)), + ValueFactory.newString(metricKey) -> ValueFactory.newString(metricData.getMetricDefinition.getKey), + ValueFactory.newString(valueKey) -> ValueFactory.newFloat(metricData.getValue), + ValueFactory.newString(timeKey) -> new ImmutableSignedLongValueImpl(metricData.getTimestamp), + ValueFactory.newString(typeKey) -> ValueFactory.newString(retrieveType(metricData)), + ValueFactory.newString(tagsKey) -> ValueFactory.newArray(retrieveTags(metricData).asJava) + ) + packer.packValue(ValueFactory.newMap(metricDataMap.asJava)) + val data = packer.toByteArray + metricPointSerSuccessMeter.mark() + data + } catch { + case ex: Exception => + /* may be log and add metric */ + metricPointSerFailureMeter.mark() + null + } + } + + //Retrieves the interval in case its present in the tags else uses the default interval + private def retrieveInterval(metricData: MetricData): Int = { + getMetricTags(metricData).asScala.get(TagKeys.INTERVAL_KEY).map(stringInterval => Interval.fromName(stringInterval).timeInSeconds).getOrElse(DEFAULT_INTERVAL_IN_SEC) + } + + private def retrieveType(metricData: MetricData): String = { + getMetricTags(metricData).get(MetricDefinition.MTYPE) + } + + private def retrieveTags(metricData: MetricData): List[Value] = { + getMetricTags(metricData).asScala.map(tuple => { + ValueFactory.newString(s"${tuple._1}=${tuple._2}") + }).toList + } + + private def getId(metricData: MetricData): String = { + s"${getOrgId(metricData)}.${DigestUtils.md5Hex(getKey(metricData))}" + } + + private def getKey(metricData: MetricData): String = { + val metricTags = getMetricTags(metricData).asScala.foldLeft("")((tag, tuple) => { + tag + s"${tuple._1}.${tuple._2}." + }) + s"$metricTags${metricData.getMetricDefinition.getKey}" + } + + private def getOrgId(metricData: MetricData): Int = { + getMetricTags(metricData).getOrDefault(ORG_ID_KEY, DEFAULT_ORG_ID.toString).toInt + } + + private def getMetricTags(metricData: MetricData) : util.Map[String, String] = { + metricData.getMetricDefinition.getTags.getKv + } + + override def close(): Unit = () + + /** + * This is a value extention class for signed long type. The java client for messagepack packs positive longs as unsigned + * and there is no way to force a signed long who's numberal value is positive. + * Metric Tank schema requres a signed long type for the timestamp key. + * + * @param long + */ + class ImmutableSignedLongValueImpl(long: Long) extends ImmutableLongValueImpl(long) { + + override def writeTo(pk: MessagePacker) { + val buffer = ByteBuffer.allocate(java.lang.Long.BYTES + 1) + buffer.put(Code.INT64) + buffer.putLong(long) + pk.addPayload(buffer.array()) + } + } + +} diff --git a/commons/commons/src/main/scala/com/expedia/www/haystack/commons/logger/LoggerUtils.scala b/commons/commons/src/main/scala/com/expedia/www/haystack/commons/logger/LoggerUtils.scala new file mode 100644 index 000000000..017b22a41 --- /dev/null +++ b/commons/commons/src/main/scala/com/expedia/www/haystack/commons/logger/LoggerUtils.scala @@ -0,0 +1,49 @@ +/* + * + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.commons.logger + +import org.slf4j.{ILoggerFactory, LoggerFactory} + +object LoggerUtils { + + /** + * shutdown the logger using reflection. + * for logback, it calls stop() method on loggerContext + * for log4j, it calls close() method on log4j context + */ + def shutdownLogger(): Unit = { + val factory = LoggerFactory.getILoggerFactory + shutdownLoggerWithFactory(factory) + } + + def shutdownLoggerWithFactory(factory: ILoggerFactory): Unit = { + val clazz = factory.getClass + try { + clazz.getMethod("stop").invoke(factory) // logback + } catch { + case _: ReflectiveOperationException => + try { + clazz.getMethod("close").invoke(factory) // log4j + } catch { + case _: Exception => + } + case _: Exception => + } + } +} diff --git a/commons/commons/src/main/scala/com/expedia/www/haystack/commons/metrics/MetricsSupport.scala b/commons/commons/src/main/scala/com/expedia/www/haystack/commons/metrics/MetricsSupport.scala new file mode 100644 index 000000000..756e39a56 --- /dev/null +++ b/commons/commons/src/main/scala/com/expedia/www/haystack/commons/metrics/MetricsSupport.scala @@ -0,0 +1,44 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.commons.metrics + +import com.codahale.metrics.{Metric, MetricRegistry} + +trait MetricsSupport { + val metricRegistry: MetricRegistry = MetricsRegistries.metricRegistry +} + +object MetricsRegistries { + + val metricRegistry = new MetricRegistry() + + implicit class MetricRegistryExtension(val metricRegistry: MetricRegistry) extends AnyVal { + + def getOrAddGauge[T](expectedName: String, gauge: com.codahale.metrics.Gauge[T]): Boolean = { + val existingGauges = metricRegistry.getGauges((existingName: String, _: Metric) => { + existingName.equalsIgnoreCase(expectedName) + }) + + if (existingGauges == null || existingGauges.size() == 0) { + metricRegistry.register(expectedName, gauge) + true + } else { + false + } + } + } +} diff --git a/commons/commons/src/main/scala/com/expedia/www/haystack/commons/retries/MaxRetriesAttemptedException.scala b/commons/commons/src/main/scala/com/expedia/www/haystack/commons/retries/MaxRetriesAttemptedException.scala new file mode 100644 index 000000000..8ca915a6d --- /dev/null +++ b/commons/commons/src/main/scala/com/expedia/www/haystack/commons/retries/MaxRetriesAttemptedException.scala @@ -0,0 +1,21 @@ +/* + * + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.commons.retries + +class MaxRetriesAttemptedException(message: String, reason: Throwable ) extends RuntimeException(message, reason) diff --git a/commons/commons/src/main/scala/com/expedia/www/haystack/commons/retries/RetryOperation.scala b/commons/commons/src/main/scala/com/expedia/www/haystack/commons/retries/RetryOperation.scala new file mode 100644 index 000000000..7ad7bdb23 --- /dev/null +++ b/commons/commons/src/main/scala/com/expedia/www/haystack/commons/retries/RetryOperation.scala @@ -0,0 +1,112 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.commons.retries + +import scala.annotation.tailrec +import scala.util.{Failure, Try} + +object RetryOperation { + + /** + * retry configuration + * @param maxRetries maximum number of retry attempts + * @param backOffInMillis initial backkoff in millis + * @param backoffFactor exponential backoff that gets applied on the previousBackoff value + */ + case class Config(maxRetries: Int, backOffInMillis: Long, backoffFactor: Double) { + /** + * @return next back off config after applying the exponential factor to initialBackOffInMillis + */ + def nextBackOffConfig: Config = this.copy(backOffInMillis = Math.ceil(backOffInMillis * backoffFactor).toLong) + } + + trait Callback { + def onResult[T](result: T): Unit + + def onError(ex: Throwable, retry: Boolean): Unit + + def lastError(): Throwable + } + + /** + * executes the given function with a retry on failures + * + * @param f main function to execute and retry if fail + * @param retryConfig retry configuration with max retry count, backoff values + * @tparam T result object from the main 'f' function + */ + def executeWithRetryBackoff[T](f: () => T, retryConfig: Config): Try[T] = { + executeWithRetryBackoff(f, 0, retryConfig) + } + + @tailrec + private def executeWithRetryBackoff[T](f: () => T, currentRetryCount: Int, retryConfig: Config): Try[T] = { + Try { + f() + } match { + case Failure(reason) if currentRetryCount < retryConfig.maxRetries && !reason.isInstanceOf[InterruptedException] => + Thread.sleep(retryConfig.backOffInMillis) + executeWithRetryBackoff(f, currentRetryCount + 1, retryConfig.nextBackOffConfig) + case result@_ => result + } + } + + /** + * executes the given async function with a retry on failures + * + * @param f main function to execute and retry if fail + * @param retryConfig retry configuration with max retry count, backoff values + * @param onSuccess this callback is called if the main 'f' function executes with success + * @param onFailure this callback is called if the main 'f' function fails after all reattempts + * @tparam T result object from the main 'f' function + */ + def withRetryBackoff[T](f: (Callback) => Unit, + retryConfig: Config, + onSuccess: (T) => Unit, + onFailure: (Exception) => Unit): Unit = { + withRetryBackoff(f, 0, retryConfig, onSuccess, onFailure) + } + + private def withRetryBackoff[T](f: (Callback) => Unit, + currentRetry: Int, + retryConfig: Config, + onSuccess: (T) => Unit, + onFailure: (Exception) => Unit, + lastSeenError: Throwable = null): Unit = { + try { + val retryResult = new Callback { + override def onResult[Any](result: Any): Unit = { + onSuccess(result.asInstanceOf[T]) + } + + override def onError(ex: Throwable, retry: Boolean): Unit = { + if (retry && currentRetry < retryConfig.maxRetries) { + Thread.sleep(retryConfig.backOffInMillis) + withRetryBackoff(f, currentRetry + 1, retryConfig.nextBackOffConfig, onSuccess, onFailure, ex) + } else { + onFailure(new MaxRetriesAttemptedException(s"max retries=${retryConfig.maxRetries} have reached and all attempts have failed!", ex)) + } + } + + override def lastError(): Throwable = lastSeenError + } + f(retryResult) + } catch { + case ex: Exception => onFailure(ex) + } + } +} diff --git a/commons/commons/src/main/scala/com/expedia/www/haystack/commons/util/MetricDefinitionKeyGenerator.scala b/commons/commons/src/main/scala/com/expedia/www/haystack/commons/util/MetricDefinitionKeyGenerator.scala new file mode 100644 index 000000000..e5ba4c09f --- /dev/null +++ b/commons/commons/src/main/scala/com/expedia/www/haystack/commons/util/MetricDefinitionKeyGenerator.scala @@ -0,0 +1,19 @@ +package com.expedia.www.haystack.commons.util + +import com.expedia.metrics.{MetricDefinition, TagCollection} + +import scala.collection.JavaConverters._ +import scala.collection.immutable.ListMap + +object MetricDefinitionKeyGenerator { + + def generateKey(metricDefinition: MetricDefinition): String = { + List(s"key=${metricDefinition.getKey}", getTagsAsString(metricDefinition.getTags), + getTagsAsString(metricDefinition.getMeta)).filter(!_.isEmpty).mkString(",") + } + + def getTagsAsString(tags: TagCollection): String = { + ListMap(tags.getKv.asScala.toSeq.sortBy(_._1): _*).map(tag => s"${tag._1}=${tag._2}").mkString(",") + } + +} diff --git a/commons/commons/src/test/resources/logback-test.xml b/commons/commons/src/test/resources/logback-test.xml new file mode 100644 index 000000000..adfa02c68 --- /dev/null +++ b/commons/commons/src/test/resources/logback-test.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/commons/commons/src/test/resources/sample.conf b/commons/commons/src/test/resources/sample.conf new file mode 100644 index 000000000..e7980d7a0 --- /dev/null +++ b/commons/commons/src/test/resources/sample.conf @@ -0,0 +1,7 @@ +haystack { + graphite { + host = "influxdb.kube-system.svc" + port = 2003 + } +} + diff --git a/commons/commons/src/test/scala/com/expedia/www/haystack/commons/config/ConfigurationLoaderSpec.scala b/commons/commons/src/test/scala/com/expedia/www/haystack/commons/config/ConfigurationLoaderSpec.scala new file mode 100644 index 000000000..49406056c --- /dev/null +++ b/commons/commons/src/test/scala/com/expedia/www/haystack/commons/config/ConfigurationLoaderSpec.scala @@ -0,0 +1,97 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.commons.config + +import com.expedia.www.haystack.commons.unit.UnitTestSpec +import com.typesafe.config.ConfigFactory +import scala.collection.JavaConverters._ + +class ConfigurationLoaderSpec extends UnitTestSpec { + private val keyName = "traces.key.sequence" + + "ConfigurationLoader.loadConfigFileWithEnvOverrides" should { + + "load a given config file as expected when no environment overrides are present" in { + Given("a sample HOCON conf file") + val file = "sample.conf" + When("loadConfigFileWithEnvOverrides is invoked with no environment variables") + val config = ConfigurationLoader.loadConfigFileWithEnvOverrides(resourceName = file) + Then("it should load the configuration entries as expected") + "influxdb.kube-system.svc" should equal(config.getString("haystack.graphite.host")) + 2003 should equal(config.getInt("haystack.graphite.port")) + } + } + + "ConfigurationLoader.parsePropertiesFromMap" should { + "parses a given map and returns transformed key-value that matches a given prefix" in { + Given("a sample map with a key-value") + val data = Map("FOO_HAYSTACK_GRAPHITE_HOST" -> "influxdb.kube-system.svc", "foo.bar" -> "baz") + When("parsePropertiesFromMap is invoked with matching prefix") + val config = ConfigurationLoader.parsePropertiesFromMap(data, Set(), "FOO_") + Then("it should transform the entries that match the prefix as expected") + Some("influxdb.kube-system.svc") should equal(config.get("haystack.graphite.host")) + None should be(config.get("foo.bar")) + } + + "parses a given map with empty array of values and return transformed key-value that matches a given prefix" in { + Given("a sample map with a key and empty array of values") + val envVars = Map[String, String](ConfigurationLoader.ENV_NAME_PREFIX + "TRACES_KEY_SEQUENCE" -> "[]") + When("parsePropertiesFromMap is invoked") + val config = ConfigFactory.parseMap(ConfigurationLoader.parsePropertiesFromMap(envVars, Set(keyName), ConfigurationLoader.ENV_NAME_PREFIX).asJava) + Then("it should return an empty list with given key") + config.getList(keyName).size() shouldBe 0 + } + + "parses a given map with non-empty array of values and return transformed key-value that matches a given prefix" in { + Given("a sample map with a key and empty array of values") + val envVars = Map[String, String](ConfigurationLoader.ENV_NAME_PREFIX + "TRACES_KEY_SEQUENCE" -> "[v1]") + When("parsePropertiesFromMap is invoked") + val config = ConfigFactory.parseMap(ConfigurationLoader.parsePropertiesFromMap(envVars, Set(keyName), ConfigurationLoader.ENV_NAME_PREFIX).asJava) + Then("it should return an empty list with given key") + config.getStringList(keyName).size() shouldBe 1 + config.getStringList(keyName).get(0) shouldBe "v1" + } + + "should throw runtime exception if env variable doesn't comply array value signature - [..]" in { + Given("a sample map with a key and non compliant array of values") + val envVars = Map[String, String](ConfigurationLoader.ENV_NAME_PREFIX + "TRACES_KEY_SEQUENCE" -> "v1") + When("parsePropertiesFromMap is invoked") + val exception = intercept[RuntimeException] { + ConfigurationLoader.parsePropertiesFromMap(envVars, Set(keyName), ConfigurationLoader.ENV_NAME_PREFIX) + } + Then("it should throw exception with excepted message") + exception.getMessage shouldEqual "config key is of array type, so it should start and end with '[', ']' respectively" + } + + "should load config from env variable with non-empty value" in { + Given("a sample map with a key and empty array of values") + val envVars = Map[String, String]( + ConfigurationLoader.ENV_NAME_PREFIX + "TRACES_KEY_SEQUENCE" -> "[v1]", + ConfigurationLoader.ENV_NAME_PREFIX + "TRACES_KEY2" -> "v2", + "NON_HAYSTACK_KEY" -> "not_interested") + + When("parsePropertiesFromMap is invoked") + val config = ConfigFactory.parseMap(ConfigurationLoader.parsePropertiesFromMap(envVars, Set(keyName), ConfigurationLoader.ENV_NAME_PREFIX).asJava) + Then("it should return an empty list with given key") + config.getStringList(keyName).size() shouldBe 1 + config.getStringList(keyName).get(0) shouldBe "v1" + config.getString("traces.key2") shouldBe "v2" + config.hasPath("non.haystack.key") shouldBe false + } + } +} diff --git a/commons/commons/src/test/scala/com/expedia/www/haystack/commons/entities/encoders/EncoderFactorySpec.scala b/commons/commons/src/test/scala/com/expedia/www/haystack/commons/entities/encoders/EncoderFactorySpec.scala new file mode 100644 index 000000000..d1dcec832 --- /dev/null +++ b/commons/commons/src/test/scala/com/expedia/www/haystack/commons/entities/encoders/EncoderFactorySpec.scala @@ -0,0 +1,56 @@ +package com.expedia.www.haystack.commons.entities.encoders + +import com.expedia.www.haystack.commons.unit.UnitTestSpec + +class EncoderFactorySpec extends UnitTestSpec { + "EncoderFactory" should { + + "return a NoopEncoder by default for null" in { + When("encoder is null") + val encoder = EncoderFactory.newInstance(null) + + Then("should be a NoopEncoder") + encoder shouldBe an[NoopEncoder] + } + + "return a NoopEncoder by default for empty string" in { + When("encoder is empty string") + val encoder = EncoderFactory.newInstance("") + + Then("should be a NoopEncoder") + encoder shouldBe an[NoopEncoder] + } + + "return a Base64Encoder when value = base64" in { + When("encoder is empty string") + val encoder = EncoderFactory.newInstance(EncoderFactory.BASE_64) + + Then("should be a Base64Encoder") + encoder shouldBe an[Base64Encoder] + } + + "return a Base64Encoder when value = baSe64" in { + When("encoder is empty string") + val encoder = EncoderFactory.newInstance("baSe64") + + Then("should be a Base64Encoder") + encoder shouldBe an[Base64Encoder] + } + + "return a PeriodReplacementEncoder when value = periodreplacement" in { + When("encoder is empty string") + val encoder = EncoderFactory.newInstance("periodreplacement") + + Then("should be a PeriodReplacementEncoder") + encoder shouldBe an[PeriodReplacementEncoder] + } + + "return a PeriodReplacementEncoder when value = periodReplacement" in { + When("encoder is empty string") + val encoder = EncoderFactory.newInstance(EncoderFactory.PERIOD_REPLACEMENT) + + Then("should be a PeriodReplacementEncoder") + encoder shouldBe an[PeriodReplacementEncoder] + } + } +} diff --git a/commons/commons/src/test/scala/com/expedia/www/haystack/commons/graph/GraphEdgeCollectorSpec.scala b/commons/commons/src/test/scala/com/expedia/www/haystack/commons/graph/GraphEdgeCollectorSpec.scala new file mode 100644 index 000000000..ade664059 --- /dev/null +++ b/commons/commons/src/test/scala/com/expedia/www/haystack/commons/graph/GraphEdgeCollectorSpec.scala @@ -0,0 +1,77 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.commons.graph + +import com.expedia.open.tracing.Tag.TagType +import com.expedia.open.tracing.{Span, Tag} +import com.expedia.www.haystack.commons.entities.TagKeys +import com.expedia.www.haystack.commons.unit.UnitTestSpec + + +class GraphEdgeCollectorSpec extends UnitTestSpec { + + + "graph edge collector" should { + + "should collect predefined collection tags" in { + Given("a graph edge collector with a list of tags to be collected") + val tags = Set("tag1", "tag2") + And("a span containing tags") + val span = Span.newBuilder().addTags(Tag.newBuilder().setKey("tag1").setVStr("val1")).build() + + When("collecting the tags for a given span") + val edgeTagCollector = new GraphEdgeTagCollector(tags) + val collectedTags = edgeTagCollector.collectTags(span) + + Then("only the predefined tags that are also part of the span should be collected") + collectedTags.get("tag1") should be (Some("val1")) + collectedTags should not contain ("tag2") + } + + "should always collect default tags" in { + Given("a graph edge collector and an empty tag list") + val tags = Set[String]() + And("a span containing only the default tag") + val span = Span.newBuilder().addTags(Tag.newBuilder().setKey(TagKeys.ERROR_KEY).setVBool(true) + .setType(TagType.BOOL)).build() + + When("collecting the tags for a given span") + val edgeTagCollector = new GraphEdgeTagCollector(tags) + val collectedTags = edgeTagCollector.collectTags(span) + + Then("only the predefined tags that are also part of the span should be collected") + collectedTags.get(TagKeys.ERROR_KEY) should be (Some("true")) + } + + "should throw an exception if tag type cannot be converted to string" in { + Given("a graph edge collector and an empty tag list") + val tags = Set("test") + And("a span containing a tag whose type is not supported") + val span = Span.newBuilder().addTags(Tag.newBuilder().setKey("test").setType(TagType.BINARY)).build() + + When("collecting the tags for a given span") + Then("only the predefined tags that are also part of the span should be collected") + val edgeTagCollector = new GraphEdgeTagCollector(tags) + intercept[IllegalArgumentException] { + edgeTagCollector.collectTags(span) + } + } + + } + +} diff --git a/commons/commons/src/test/scala/com/expedia/www/haystack/commons/health/HealthControllerSpec.scala b/commons/commons/src/test/scala/com/expedia/www/haystack/commons/health/HealthControllerSpec.scala new file mode 100644 index 000000000..147a0b9ed --- /dev/null +++ b/commons/commons/src/test/scala/com/expedia/www/haystack/commons/health/HealthControllerSpec.scala @@ -0,0 +1,56 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.commons.health + +import com.expedia.www.haystack.commons.unit.UnitTestSpec + +class HealthControllerSpec extends UnitTestSpec { + val statusFile = "/tmp/app-health.status" + + + "file based health checker" should { + + "set the value with the correct boolean value for the app's health status" in { + Given("a file path") + + When("checked with default state") + val healthChecker = HealthController + healthChecker.addListener(new UpdateHealthStatusFile(statusFile)) + val status = healthChecker.isHealthy + + Then("default state should be unhealthy") + status shouldBe false + + When("explicitly set as healthy") + healthChecker.setHealthy() + + Then("The state should be updated to healthy") + healthChecker.isHealthy shouldBe true + readStatusLine shouldEqual "true" + + When("explicitly set as unhealthy") + healthChecker.setUnhealthy() + + Then("The state should be updated to unhealthy") + healthChecker.isHealthy shouldBe false + readStatusLine shouldBe "false" + } + } + + private def readStatusLine = scala.io.Source.fromFile(statusFile).getLines().toList.head +} diff --git a/commons/commons/src/test/scala/com/expedia/www/haystack/commons/kstreams/GraphEdgeTimestampExtractorSpec.scala b/commons/commons/src/test/scala/com/expedia/www/haystack/commons/kstreams/GraphEdgeTimestampExtractorSpec.scala new file mode 100644 index 000000000..c8c532cbc --- /dev/null +++ b/commons/commons/src/test/scala/com/expedia/www/haystack/commons/kstreams/GraphEdgeTimestampExtractorSpec.scala @@ -0,0 +1,44 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.commons.kstreams + +import com.expedia.www.haystack.commons.entities.{GraphEdge, GraphVertex} +import com.expedia.www.haystack.commons.unit.UnitTestSpec +import org.apache.kafka.clients.consumer.ConsumerRecord + +class GraphEdgeTimestampExtractorSpec extends UnitTestSpec { + + "GraphEdgeTimestampExtractor" should { + + "extract timestamp from GraphEdge" in { + + Given("a GraphEdge with some timestamp") + val time = System.currentTimeMillis() + val graphEdge = GraphEdge(GraphVertex("svc1"), GraphVertex("svc2"), "oper1", time) + val extractor = new GraphEdgeTimestampExtractor + val record: ConsumerRecord[AnyRef, AnyRef] = new ConsumerRecord("dummy-topic", 1, 1, "dummy-key", graphEdge) + + When("extract timestamp") + val epochTime = extractor.extract(record, System.currentTimeMillis()) + + Then("extracted time should equal GraphEdge time in milliseconds") + epochTime shouldEqual time + } + } +} diff --git a/commons/commons/src/test/scala/com/expedia/www/haystack/commons/kstreams/MetricDataTimestampExtractorSpec.scala b/commons/commons/src/test/scala/com/expedia/www/haystack/commons/kstreams/MetricDataTimestampExtractorSpec.scala new file mode 100644 index 000000000..510ab0450 --- /dev/null +++ b/commons/commons/src/test/scala/com/expedia/www/haystack/commons/kstreams/MetricDataTimestampExtractorSpec.scala @@ -0,0 +1,52 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.commons.kstreams + +import com.expedia.metrics.{MetricData, MetricDefinition, TagCollection} +import com.expedia.www.haystack.commons.unit.UnitTestSpec +import org.apache.kafka.clients.consumer.ConsumerRecord + +import scala.util.Random + +class MetricDataTimestampExtractorSpec extends UnitTestSpec { + + "MetricDataTimestampExtractor" should { + + "extract timestamp from MetricData" in { + + Given("a metric data with some timestamp") + val currentTimeInSecs = computeCurrentTimeInSecs + val metricData = getMetricData(currentTimeInSecs) + val metricDataTimestampExtractor = new MetricDataTimestampExtractor + val record: ConsumerRecord[AnyRef, AnyRef] = new ConsumerRecord("dummy-topic", 1, 1, "dummy-key", metricData) + + When("extract timestamp") + val epochTime = metricDataTimestampExtractor.extract(record, System.currentTimeMillis()) + + Then("extracted time should equal metric point time in milliseconds") + epochTime shouldEqual currentTimeInSecs * 1000 + } + } + + private def getMetricData(timeStamp : Long): MetricData = { + val metricDefinition = new MetricDefinition("duration") + new MetricData(metricDefinition, Random.nextDouble(), timeStamp) + } + +} diff --git a/commons/commons/src/test/scala/com/expedia/www/haystack/commons/kstreams/SpanTimestampExtractorSpec.scala b/commons/commons/src/test/scala/com/expedia/www/haystack/commons/kstreams/SpanTimestampExtractorSpec.scala new file mode 100644 index 000000000..af992507a --- /dev/null +++ b/commons/commons/src/test/scala/com/expedia/www/haystack/commons/kstreams/SpanTimestampExtractorSpec.scala @@ -0,0 +1,46 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.commons.kstreams + +import java.util.UUID + +import com.expedia.www.haystack.commons.unit.UnitTestSpec +import org.apache.kafka.clients.consumer.ConsumerRecord + +class SpanTimestampExtractorSpec extends UnitTestSpec { + + "SpanTimestampExtractor" should { + + " should extract timestamp from Span" in { + + Given("a span with some timestamp") + val currentTimeInMicroSeconds = System.currentTimeMillis() * 1000 + + val span = generateTestSpan(UUID.randomUUID().toString, currentTimeInMicroSeconds, "foo", "bar", 20, client = false, server = true) + val spanTimestampExtractor = new SpanTimestampExtractor + val record: ConsumerRecord[AnyRef, AnyRef] = new ConsumerRecord("dummy-topic", 1, 1, "dummy-key", span) + + When("extract timestamp") + val epochTime = spanTimestampExtractor.extract(record, System.currentTimeMillis()) + + Then("extracted time should equal span startTime time") + epochTime shouldEqual currentTimeInMicroSeconds / 1000 + } + } +} diff --git a/commons/commons/src/test/scala/com/expedia/www/haystack/commons/kstreams/app/ApplicationSpec.scala b/commons/commons/src/test/scala/com/expedia/www/haystack/commons/kstreams/app/ApplicationSpec.scala new file mode 100644 index 000000000..c4fec1548 --- /dev/null +++ b/commons/commons/src/test/scala/com/expedia/www/haystack/commons/kstreams/app/ApplicationSpec.scala @@ -0,0 +1,66 @@ +package com.expedia.www.haystack.commons.kstreams.app + +import com.codahale.metrics.JmxReporter +import com.expedia.www.haystack.commons.unit.UnitTestSpec +import org.easymock.EasyMock._ + +class ApplicationSpec extends UnitTestSpec { + + "Application" should { + + "require an instance of StreamsRunner" in { + Given("only a valid instance of jmxReporter") + val streamsRunner : StreamsRunner = null + val jmxReporter = mock[JmxReporter] + When("an instance of Application is created") + Then("it should throw an exception") + intercept[IllegalArgumentException] { + new Application(streamsRunner, jmxReporter) + } + } + "require an instance of JmxReporter" in { + Given("only a valid instance of StreamsRunner") + val streamsRunner : StreamsRunner = mock[StreamsRunner] + val jmxReporter = null + When("an instance of Application is created") + Then("it should throw an exception") + intercept[IllegalArgumentException] { + new Application(streamsRunner, jmxReporter) + } + } + "start both JmxReporter and StreamsRunner at start" in { + Given("a fully configured application") + val streamsRunner : StreamsRunner = mock[StreamsRunner] + val jmxReporter = mock[JmxReporter] + val application = new Application(streamsRunner, jmxReporter) + When("application is started") + expecting { + streamsRunner.start().once() + jmxReporter.start().once() + } + replay(streamsRunner, jmxReporter) + application.start() + Then("it should call start on both streamsRunner and jmxReporter") + verify(streamsRunner, jmxReporter) + } + "close both JmxReporter and StreamsRunner at stop" in { + Given("a fully configured and running application") + val streamsRunner : StreamsRunner = mock[StreamsRunner] + val jmxReporter = mock[JmxReporter] + val application = new Application(streamsRunner, jmxReporter) + When("application is stopped") + expecting { + streamsRunner.start().once() + jmxReporter.start().once() + streamsRunner.close().once() + jmxReporter.close().once() + } + replay(streamsRunner, jmxReporter) + application.start() + application.stop() + Then("it should call close on both streamsRunner and jmxReporter") + verify(streamsRunner, jmxReporter) + } + + } +} diff --git a/commons/commons/src/test/scala/com/expedia/www/haystack/commons/kstreams/app/ManagedKafkaStreamsSpec.scala b/commons/commons/src/test/scala/com/expedia/www/haystack/commons/kstreams/app/ManagedKafkaStreamsSpec.scala new file mode 100644 index 000000000..80749eb98 --- /dev/null +++ b/commons/commons/src/test/scala/com/expedia/www/haystack/commons/kstreams/app/ManagedKafkaStreamsSpec.scala @@ -0,0 +1,66 @@ +package com.expedia.www.haystack.commons.kstreams.app + +import java.util.concurrent.TimeUnit + +import com.expedia.www.haystack.commons.unit.UnitTestSpec +import org.apache.kafka.streams.KafkaStreams +import org.easymock.EasyMock._ + +class ManagedKafkaStreamsSpec extends UnitTestSpec { + "ManagedKafkaStreams" should { + "start the underlying kafkaStreams when started" in { + Given("a fully configured ManagedKafkaStreams instance") + val kafkaStreams = mock[KafkaStreams] + val managedKafkaStreams = new ManagedKafkaStreams(kafkaStreams) + When("start is invoked") + expecting { + kafkaStreams.start().once() + } + replay(kafkaStreams) + managedKafkaStreams.start() + Then("it should start the KafkaStreams application") + verify(kafkaStreams) + } + "close the KafkaStreams when stopped" in { + Given("a fully configured ManagedKafkaStreams instance") + val kafkaStreams = mock[KafkaStreams] + val managedKafkaStreams = new ManagedKafkaStreams(kafkaStreams) + When("stop is invoked") + expecting { + kafkaStreams.start().once() + kafkaStreams.close(0, TimeUnit.SECONDS).andReturn(true).once() + } + replay(kafkaStreams) + managedKafkaStreams.start() + Then("it should close the KafkaStreams application") + managedKafkaStreams.stop() + verify(kafkaStreams) + } + "not do anything when stop is called without starting" in { + Given("a fully configured ManagedKafkaStreams instance") + val kafkaStreams = mock[KafkaStreams] + val managedKafkaStreams = new ManagedKafkaStreams(kafkaStreams) + When("stop is invoked without starting") + replay(kafkaStreams) + Then("it should do nothing") + managedKafkaStreams.stop() + verify(kafkaStreams) + } + "close the KafkaStreams with the given timeout when stopped" in { + Given("a fully configured ManagedKafkaStreams instance with a timeout") + val kafkaStreams = mock[KafkaStreams] + val managedKafkaStreams = new ManagedKafkaStreams(kafkaStreams, 5) + When("stop is invoked") + expecting { + kafkaStreams.start().once() + kafkaStreams.close(5, TimeUnit.SECONDS).andReturn(true).once() + } + replay(kafkaStreams) + managedKafkaStreams.start() + Then("it should close the KafkaStreams application with the given timeout") + managedKafkaStreams.stop() + verify(kafkaStreams) + } + } + +} diff --git a/commons/commons/src/test/scala/com/expedia/www/haystack/commons/kstreams/app/StateChangeListenerSpec.scala b/commons/commons/src/test/scala/com/expedia/www/haystack/commons/kstreams/app/StateChangeListenerSpec.scala new file mode 100644 index 000000000..f4b070c52 --- /dev/null +++ b/commons/commons/src/test/scala/com/expedia/www/haystack/commons/kstreams/app/StateChangeListenerSpec.scala @@ -0,0 +1,52 @@ +package com.expedia.www.haystack.commons.kstreams.app + +import com.expedia.www.haystack.commons.health.HealthStatusController +import com.expedia.www.haystack.commons.unit.UnitTestSpec +import org.easymock.EasyMock._ + +class StateChangeListenerSpec extends UnitTestSpec { + "StateChangeListener" should { + "set the health status to healthy when requested" in { + Given("a valid instance of StateChangeListener") + val healthStatusController = mock[HealthStatusController] + val stateChangeListener = new StateChangeListener(healthStatusController) + When("set healthy is invoked") + expecting { + healthStatusController.setHealthy().once() + } + replay(healthStatusController) + stateChangeListener.state(true) + Then("it should set health status to healthy") + verify(healthStatusController) + } + "set the health status to unhealthy when requested" in { + Given("a valid instance of StateChangeListener") + val healthStatusController = mock[HealthStatusController] + val stateChangeListener = new StateChangeListener(healthStatusController) + When("set unhealthy is invoked") + expecting { + healthStatusController.setUnhealthy().once() + } + replay(healthStatusController) + stateChangeListener.state(false) + Then("it should set health status to healthy") + verify(healthStatusController) + } + "set application status to unhealthy when an un caught exception is raised" in { + Given("a valid instance of StateChangeListener") + val healthStatusController = mock[HealthStatusController] + val stateChangeListener = new StateChangeListener(healthStatusController) + val exception = new IllegalArgumentException + val thread = new Thread("Thread-1") + When("an uncaught exception is raised") + expecting { + healthStatusController.setUnhealthy().once() + } + replay(healthStatusController) + stateChangeListener.uncaughtException(thread, exception) + Then("it should set the status to unhealthy") + verify(healthStatusController) + } + } + +} diff --git a/commons/commons/src/test/scala/com/expedia/www/haystack/commons/kstreams/app/StreamsRunnerSpec.scala b/commons/commons/src/test/scala/com/expedia/www/haystack/commons/kstreams/app/StreamsRunnerSpec.scala new file mode 100644 index 000000000..6a61c157b --- /dev/null +++ b/commons/commons/src/test/scala/com/expedia/www/haystack/commons/kstreams/app/StreamsRunnerSpec.scala @@ -0,0 +1,46 @@ +package com.expedia.www.haystack.commons.kstreams.app + +import com.expedia.www.haystack.commons.unit.UnitTestSpec +import org.easymock.EasyMock._ + +class StreamsRunnerSpec extends UnitTestSpec { + "StreamsRunner" should { + "start managed KStreams when the factory successfully creates one" in { + Given("a StreamsFactory") + val factory = mock[StreamsFactory] + And("a StateChangeListener") + val stateChangeListener = mock[StateChangeListener] + val managedService = mock[ManagedService] + val streamsRunner = new StreamsRunner(factory, stateChangeListener) + When("streamsRunner is asked to start the application") + expecting { + factory.create(stateChangeListener).andReturn(managedService).once() + managedService.start().once() + stateChangeListener.state(true).once() + } + replay(factory, managedService, stateChangeListener) + streamsRunner.start() + Then("it should create an instance of managed streams from the given factory and start it. " + + "It should also set the state to healthy") + verify(factory, managedService, stateChangeListener) + } + "set the state to unhealthy when the factory fails to create one" in { + Given("a StreamsFactory") + val factory = mock[StreamsFactory] + And("a StateChangeListener") + val stateChangeListener = mock[StateChangeListener] + val managedService = mock[ManagedService] + val streamsRunner = new StreamsRunner(factory, stateChangeListener) + When("streamsRunner is asked to start the application and factory fails") + expecting { + factory.create(stateChangeListener).andThrow(new RuntimeException).once() + stateChangeListener.state(false).once() + } + replay(factory, managedService, stateChangeListener) + streamsRunner.start() + Then("it should attempt tp create an instance of managed streams from the given factory. " + + "It should also set the state to unhealthy") + verify(factory, managedService, stateChangeListener) + } + } +} diff --git a/commons/commons/src/test/scala/com/expedia/www/haystack/commons/kstreams/serde/SpanSerdeSpec.scala b/commons/commons/src/test/scala/com/expedia/www/haystack/commons/kstreams/serde/SpanSerdeSpec.scala new file mode 100644 index 000000000..bf1d13ca9 --- /dev/null +++ b/commons/commons/src/test/scala/com/expedia/www/haystack/commons/kstreams/serde/SpanSerdeSpec.scala @@ -0,0 +1,37 @@ +package com.expedia.www.haystack.commons.kstreams.serde + +import com.expedia.www.haystack.commons.unit.UnitTestSpec + +class SpanSerdeSpec extends UnitTestSpec { + + + "span serializer" should { + "should serialize a span" in { + Given("a span serializer") + val serializer = (new SpanSerde).serializer + And("a valid span is provided") + val span = generateTestSpan("foo", "bar", 100, client = true, server = false) + When("span serializer is used to serialize the span") + val bytes = serializer.serialize("proto-spans", span) + Then("it should serialize the object") + bytes.nonEmpty should be(true) + } + } + "span deserializer" should { + "should deserialize a span" in { + Given("a span deserializer") + val serializer = (new SpanSerde).serializer + val deserializer = (new SpanSerde).deserializer + And("a valid span is provided") + val span = generateTestSpan("foo", "bar", 100, client = true, server = false) + When("span deserializer is used on valid array of bytes") + val bytes = serializer.serialize("proto-spans", span) + val span2 = deserializer.deserialize("proto-spans", bytes) + Then("it should deserialize correctly") + span should be(span2) + } + } + + + +} diff --git a/commons/commons/src/test/scala/com/expedia/www/haystack/commons/kstreams/serde/graph/GraphEdgeKeySerdeSpec.scala b/commons/commons/src/test/scala/com/expedia/www/haystack/commons/kstreams/serde/graph/GraphEdgeKeySerdeSpec.scala new file mode 100644 index 000000000..e60517262 --- /dev/null +++ b/commons/commons/src/test/scala/com/expedia/www/haystack/commons/kstreams/serde/graph/GraphEdgeKeySerdeSpec.scala @@ -0,0 +1,68 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.commons.kstreams.serde.graph + +import com.expedia.www.haystack.commons.entities.{GraphEdge, GraphVertex} +import com.expedia.www.haystack.commons.unit.UnitTestSpec + +class GraphEdgeKeySerdeSpec extends UnitTestSpec { + "GraphEdge Key serializer" should { + "should serialize a GraphEdge" in { + Given("a GraphEdge serializer") + val serializer = (new GraphEdgeKeySerde).serializer() + + And("a valid GraphEdge is provided") + val edge = GraphEdge(GraphVertex("sourceSvc"), GraphVertex("destinationSvc"), + "operation", 1) + + When("GraphEdge serializer is used to serialize the GraphEdge") + val bytes = serializer.serialize("graph-nodes", edge) + + Then("it should serialize the object") + new String(bytes) shouldEqual "{\"source\":{\"name\":\"sourceSvc\",\"tags\":{}},\"destination\":{\"name\":\"destinationSvc\",\"tags\":{}},\"operation\":\"operation\",\"sourceTimestamp\":0}" + } + } + + "GraphEdge Key deserializer" should { + "should deserialize a GraphEdge" in { + Given("a GraphEdge deserializer") + val serializer = (new GraphEdgeKeySerde).serializer() + val deserializer = (new GraphEdgeKeySerde).deserializer() + + And("a valid GraphEdge is provided") + val edge = GraphEdge(GraphVertex("sourceSvc"), GraphVertex("destinationSvc"), + "operation", System.currentTimeMillis()) + + When("GraphEdge deserializer is used on valid array of bytes") + val bytes = serializer.serialize("graph-nodes", edge) + val dataWithoutSourceTimestamp = "{\"source\":{\"name\":\"sourceSvc\",\"tags\":{}},\"destination\":{\"name\":\"destinationSvc\",\"tags\":{}},\"operation\":\"operation\"}" + + val serializedEdge_1 = deserializer.deserialize("graph-nodes", bytes) + val serializedEdge_2 = deserializer.deserialize("graph-nodes", dataWithoutSourceTimestamp.getBytes("utf-8")) + + Then("it should deserialize correctly") + serializedEdge_1.source.name should be("sourceSvc") + serializedEdge_1.destination.name should be("destinationSvc") + serializedEdge_1.operation shouldEqual "operation" + serializedEdge_1.source.tags.size shouldBe 0 + serializedEdge_1.destination.tags.size shouldBe 0 + serializedEdge_2.sourceTimestamp should not be 0l + } + } +} diff --git a/commons/commons/src/test/scala/com/expedia/www/haystack/commons/kstreams/serde/graph/GraphEdgeValueSerdeSpec.scala b/commons/commons/src/test/scala/com/expedia/www/haystack/commons/kstreams/serde/graph/GraphEdgeValueSerdeSpec.scala new file mode 100644 index 000000000..34e0b8809 --- /dev/null +++ b/commons/commons/src/test/scala/com/expedia/www/haystack/commons/kstreams/serde/graph/GraphEdgeValueSerdeSpec.scala @@ -0,0 +1,62 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.commons.kstreams.serde.graph + +import com.expedia.www.haystack.commons.entities.{GraphEdge, GraphVertex} +import com.expedia.www.haystack.commons.unit.UnitTestSpec + +class GraphEdgeValueSerdeSpec extends UnitTestSpec { + "GraphEdge Value serializer" should { + "should serialize a GraphEdge" in { + Given("a GraphEdge serializer") + val serializer = (new GraphEdgeValueSerde).serializer() + + And("a valid GraphEdge is provided") + val edge = GraphEdge(GraphVertex("sourceSvc"), GraphVertex("destinationSvc"), + "operation", System.currentTimeMillis()) + + When("GraphEdge serializer is used to serialize the GraphEdge") + val bytes = serializer.serialize("graph-nodes", edge) + + Then("it should serialize the object") + bytes.nonEmpty should be(true) + } + } + + "GraphEdge Value deserializer" should { + "should deserialize a GraphEdge" in { + Given("a GraphEdge deserializer") + val serializer = (new GraphEdgeValueSerde).serializer() + val deserializer = (new GraphEdgeValueSerde).deserializer() + + And("a valid GraphEdge is provided") + val edge = GraphEdge(GraphVertex("sourceSvc", Map("testtag" -> "true")), GraphVertex("destinationSvc"), + "operation", System.currentTimeMillis()) + + When("GraphEdge deserializer is used on valid array of bytes") + val bytes = serializer.serialize("graph-nodes", edge) + val serializedEdge = deserializer.deserialize("graph-nodes", bytes) + + Then("it should deserialize correctly") + edge should be(serializedEdge) + edge.source.name should be("sourceSvc") + edge.source.tags.get("testtag") shouldBe Some("true") + } + } +} diff --git a/commons/commons/src/test/scala/com/expedia/www/haystack/commons/kstreams/serde/metricdata/MetricTankSerdeSpec.scala b/commons/commons/src/test/scala/com/expedia/www/haystack/commons/kstreams/serde/metricdata/MetricTankSerdeSpec.scala new file mode 100644 index 000000000..3d7546094 --- /dev/null +++ b/commons/commons/src/test/scala/com/expedia/www/haystack/commons/kstreams/serde/metricdata/MetricTankSerdeSpec.scala @@ -0,0 +1,165 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.commons.kstreams.serde.metricdata + +import com.expedia.metrics.{MetricData, MetricDefinition, TagCollection} +import com.expedia.www.haystack.commons.entities.{Interval, TagKeys} +import com.expedia.www.haystack.commons.unit.UnitTestSpec +import org.msgpack.core.MessagePack +import org.msgpack.value.ValueFactory + +import scala.util.Random + +class MetricTankSerdeSpec extends UnitTestSpec { + val statusFile = "/tmp/app-health.status" + val DURATION_METRIC_NAME = "duration" + val SERVICE_NAME = "dummy_service" + val OPERATION_NAME = "dummy_operation" + val TOPIC_NAME = "dummy" + + + val metricTags = Map(TagKeys.OPERATION_NAME_KEY -> OPERATION_NAME, + TagKeys.SERVICE_NAME_KEY -> SERVICE_NAME) + + "MetricTank serde for metric data" should { + + "serialize and deserialize metric data using messagepack" in { + + Given("metric data") + val tags = new java.util.LinkedHashMap[String, String] { + put("serviceName", SERVICE_NAME) + put("operationName", OPERATION_NAME) + put(MetricDefinition.MTYPE, "gauge") + put(MetricDefinition.UNIT, "short") + } + val metricData = getMetricData(tags) + val metricTankSerde = new MetricTankSerde() + + When("its serialized using the metricTank Serde") + val serializedBytes = metricTankSerde.serializer().serialize(TOPIC_NAME, metricData) + + Then("it should be encoded as message pack") + val unpacker = MessagePack.newDefaultUnpacker(serializedBytes) + unpacker should not be null + + metricTankSerde.close() + } + + "serialize metric data with the right metric interval if present" in { + + Given("metric data with a 5 minute interval") + val metricTankSerde = new MetricTankSerde() + + val tags = new java.util.LinkedHashMap[String, String] { + put("serviceName", SERVICE_NAME) + put("operationName", OPERATION_NAME) + put(MetricDefinition.MTYPE, "gauge") + put(MetricDefinition.UNIT, "short") + put("interval", Interval.FIVE_MINUTE.name.toString) + } + val metricData = getMetricData(tags) + + When("its serialized using the metricTank Serde") + val serializedBytes = metricTankSerde.serializer().serialize(TOPIC_NAME, metricData) + val unpacker = MessagePack.newDefaultUnpacker(serializedBytes) + Then("it should be able to unpack the content") + unpacker should not be null + + Then("it unpacked content should be a valid map") + val deserializedMetricData = unpacker.unpackValue().asMapValue().map() + deserializedMetricData should not be null + + Then("interval key should be set as 300 seconds") + deserializedMetricData.get(ValueFactory.newString(metricTankSerde.serializer().intervalKey)).asIntegerValue().asInt() shouldBe 300 + + metricTankSerde.close() + } + + "serialize metricpoint with the default interval if not present" in { + + Given("metric point without the interval tag") + val metricTankSerde = new MetricTankSerde() + val tags = new java.util.LinkedHashMap[String, String] { + put("serviceName", SERVICE_NAME) + put("operationName", OPERATION_NAME) + put(MetricDefinition.MTYPE, "gauge") + put(MetricDefinition.UNIT, "short") + } + val metricData = getMetricData(tags) + + When("its serialized using the metricTank Serde") + val serializedBytes = metricTankSerde.serializer().serialize(TOPIC_NAME, metricData) + val unpacker = MessagePack.newDefaultUnpacker(serializedBytes) + Then("it should be able to unpack the content") + unpacker should not be null + + Then("it unpacked content should be a valid map") + val deserializedMetricData = unpacker.unpackValue().asMapValue().map() + deserializedMetricData should not be null + + Then("interval key should be set as default metric interval in seconds") + deserializedMetricData.get(ValueFactory.newString(metricTankSerde.serializer().intervalKey)).asIntegerValue().asInt() shouldBe metricTankSerde.serializer().DEFAULT_INTERVAL_IN_SEC + + metricTankSerde.close() + } + + + "serialize and deserialize simple metric points without loosing data" in { + + Given("metric point") + val metricTankSerde = new MetricTankSerde() + val tags = new java.util.LinkedHashMap[String, String] { + put("serviceName", SERVICE_NAME) + put("operationName", OPERATION_NAME) + put(MetricDefinition.MTYPE, "gauge") + put(MetricDefinition.UNIT, "short") + } + val metricData = getMetricData(tags) + + When("its serialized in the metricTank Format") + val serializedBytes = metricTankSerde.serializer().serialize(TOPIC_NAME, metricData) + val deserializedMetricPoint = metricTankSerde.deserializer().deserialize(TOPIC_NAME, serializedBytes) + + Then("it should be encoded as message pack") + metricData shouldEqual deserializedMetricPoint + + metricTankSerde.close() + } + } + + "serializer returns null for any exception" in { + + Given("MetricTankSerde and a null metric data") + val metricTankSerde = new MetricTankSerde() + val metricData = null + + When("its serialized using the metricTank Serde") + val serializedBytes = metricTankSerde.serializer().serialize(TOPIC_NAME, metricData) + + Then("serializer should return null") + serializedBytes shouldBe null + metricTankSerde.close() + } + + + private def getMetricData(tags: java.util.LinkedHashMap[String, String]): MetricData = { + val metricDefinition = new MetricDefinition(DURATION_METRIC_NAME, new TagCollection(tags), TagCollection.EMPTY) + new MetricData(metricDefinition, Random.nextDouble(), System.currentTimeMillis() / 1000) + } + +} diff --git a/commons/commons/src/test/scala/com/expedia/www/haystack/commons/logger/LoggerUtilSpec.scala b/commons/commons/src/test/scala/com/expedia/www/haystack/commons/logger/LoggerUtilSpec.scala new file mode 100644 index 000000000..b0cbbb4dd --- /dev/null +++ b/commons/commons/src/test/scala/com/expedia/www/haystack/commons/logger/LoggerUtilSpec.scala @@ -0,0 +1,94 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.commons.logger + +import org.scalatest.easymock.EasyMockSugar +import org.scalatest.{FunSpec, Matchers} +import org.slf4j.{ILoggerFactory, Logger} + +class LoggerUtilSpec extends FunSpec with Matchers with EasyMockSugar { + + describe("Logger Utils") { + it("should close the logger if it has stop method for e.g. logback") { + val logger = mock[Logger] + var isStopped = false + + val loggerFactory = new ILoggerFactory { + override def getLogger(s: String): Logger = logger + + def stop(): Unit = isStopped = true + } + + whenExecuting(logger) { + LoggerUtils.shutdownLoggerWithFactory(loggerFactory) + isStopped shouldBe true + } + } + + it("should close the logger if it has close method for e.g. log4j") { + val logger = mock[Logger] + var isStopped = false + + val loggerFactory = new ILoggerFactory { + override def getLogger(s: String): Logger = logger + + def close(): Unit = isStopped = true + } + + whenExecuting(logger) { + LoggerUtils.shutdownLoggerWithFactory(loggerFactory) + isStopped shouldBe true + } + } + + it("should not able to close the logger if it has neither stop/close method") { + val logger = mock[Logger] + var isStopped = false + + val loggerFactory = new ILoggerFactory { + override def getLogger(s: String): Logger = logger + + def shutdown(): Unit = isStopped = true + } + + whenExecuting(logger) { + LoggerUtils.shutdownLoggerWithFactory(loggerFactory) + isStopped shouldBe false + } + } + + it("should do nothing when stop method throws exception") { + val logger = mock[Logger] + var isStopped = true + + val loggerFactory = new ILoggerFactory { + override def getLogger(s: String): Logger = logger + + def stop(): Unit = { + isStopped = false + throw new Exception + } + } + + whenExecuting(logger) { + LoggerUtils.shutdownLoggerWithFactory(loggerFactory) + isStopped shouldBe false + } + } + } +} diff --git a/commons/commons/src/test/scala/com/expedia/www/haystack/commons/metrics/MetricRegistySpec.scala b/commons/commons/src/test/scala/com/expedia/www/haystack/commons/metrics/MetricRegistySpec.scala new file mode 100644 index 000000000..74f217ab5 --- /dev/null +++ b/commons/commons/src/test/scala/com/expedia/www/haystack/commons/metrics/MetricRegistySpec.scala @@ -0,0 +1,47 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.commons.metrics + +import com.codahale.metrics.Gauge +import com.expedia.www.haystack.commons.unit.UnitTestSpec +import com.expedia.www.haystack.commons.metrics.MetricsRegistries._ + +class MetricRegistySpec extends UnitTestSpec with MetricsSupport { + + "MetricRegisty extension" should { + + "return the same gauge metric if its created more than once" in { + Given("gauge metric") + val metricName = "testMetric" + val gaugeMetric = new Gauge[Long] { + override def getValue: Long = this.hashCode() + } + + When("its registered more than once") + val firstAttempt = metricRegistry.getOrAddGauge(metricName, gaugeMetric) + val secondAttempt = metricRegistry.getOrAddGauge(metricName, gaugeMetric) + + + Then("the first time it should create a new metric and register in the metrics registry") + firstAttempt shouldBe true + + Then("the second time it shouldn't create the same metric ") + secondAttempt shouldBe false + } + } +} diff --git a/commons/commons/src/test/scala/com/expedia/www/haystack/commons/retries/RetryOperationSpec.scala b/commons/commons/src/test/scala/com/expedia/www/haystack/commons/retries/RetryOperationSpec.scala new file mode 100644 index 000000000..df0a23b3e --- /dev/null +++ b/commons/commons/src/test/scala/com/expedia/www/haystack/commons/retries/RetryOperationSpec.scala @@ -0,0 +1,167 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.commons.retries + +import java.util.concurrent.atomic.AtomicInteger + +import org.scalatest.{FunSpec, Matchers} + +import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.Future + +class RetryOperationSpec extends FunSpec with Matchers { + describe("Retry Operation handler") { + it("should not retry if main async function runs successfully") { + @volatile var onSuccessCalled = 0 + val mainFuncCalled = new AtomicInteger(0) + + RetryOperation.withRetryBackoff((callback) => { + mainFuncCalled.incrementAndGet() + Future { + Thread.sleep(500) + callback.onResult("xxxx") + } + }, + RetryOperation.Config(maxRetries = 3, backOffInMillis = 100, backoffFactor = 1.5), + onSuccess = (result: String) => { + result.toString shouldEqual "xxxx" + onSuccessCalled = onSuccessCalled + 1 + }, onFailure = (_) => { + fail("onFailure callback should not be called") + }) + + Thread.sleep(3000) + mainFuncCalled.get() shouldBe 1 + onSuccessCalled shouldBe 1 + } + } + + it("should retry for async function if callback says retry but should not fail as last attempt succeeds") { + @volatile var onSuccessCalled = 0 + val retryConfig = RetryOperation.Config(maxRetries = 3, backOffInMillis = 100, backoffFactor = 1.5) + val mainFuncCalled = new AtomicInteger(0) + + RetryOperation.withRetryBackoff((callback) => { + val count = mainFuncCalled.incrementAndGet() + if (count > 1) { + callback.lastError() should not be null + } else { + callback.lastError() shouldBe null + } + if (count <= retryConfig.maxRetries) { + Future { + Thread.sleep(200) + callback.onError(new RuntimeException("error"), retry = true) + } + } else { + Future { + Thread.sleep(200) + callback.onResult("xxxxx") + } + } + }, + retryConfig, + onSuccess = (result: String) => { + result shouldEqual "xxxxx" + onSuccessCalled = onSuccessCalled + 1 + }, onFailure = (_) => { + fail("onFailure should not be called") + }) + + Thread.sleep(4000) + mainFuncCalled.get() shouldBe retryConfig.maxRetries + 1 + onSuccessCalled shouldBe 1 + } + + it("should retry for async function if callback asks for a retry and fail finally as all attempts fail") { + @volatile var onFailureCalled = 0 + val retryConfig = RetryOperation.Config(maxRetries = 2, backOffInMillis = 100, backoffFactor = 1.5) + val mainFuncCalled = new AtomicInteger(0) + + val error = new RuntimeException("error") + RetryOperation.withRetryBackoff((callback) => { + mainFuncCalled.incrementAndGet() + Future { + Thread.sleep(500) + callback.onError(error, retry = true) + } + }, + retryConfig, + onSuccess = (_: Any) => { + fail("onSuccess should not be called") + }, onFailure = (ex) => { + assert(ex.isInstanceOf[MaxRetriesAttemptedException]) + ex.getCause shouldBe error + onFailureCalled = onFailureCalled + 1 + }) + + Thread.sleep(4000) + mainFuncCalled.get() shouldBe (retryConfig.maxRetries + 1) + onFailureCalled shouldBe 1 + } + + it("should not retry if main async function runs successfully") { + var mainFuncCalled = 0 + val resp = RetryOperation.executeWithRetryBackoff(() => { + mainFuncCalled = mainFuncCalled + 1 + "success" + }, RetryOperation.Config(3, 100, 2)) + + mainFuncCalled shouldBe 1 + resp.get shouldEqual "success" + } + + it("should retry for function if callback says retry but should not fail as last attempt succeeds") { + var mainFuncCalled = 0 + val retryConfig = RetryOperation.Config(3, 100, 2) + val resp = RetryOperation.executeWithRetryBackoff(() => { + mainFuncCalled = mainFuncCalled + 1 + if(mainFuncCalled - 1 < retryConfig.maxRetries) throw new RuntimeException else "success" + }, retryConfig) + + mainFuncCalled shouldBe retryConfig.maxRetries + 1 + resp.get shouldEqual "success" + } + + it("should retry for function if callback asks for a retry and fail finally as all attempts fail") { + var mainFuncCalled = 0 + val retryConfig = RetryOperation.Config(3, 100, 2) + val error = new RuntimeException("error") + val resp = RetryOperation.executeWithRetryBackoff(() => { + mainFuncCalled = mainFuncCalled + 1 + throw error + }, retryConfig) + + mainFuncCalled shouldBe retryConfig.maxRetries + 1 + resp.isFailure shouldBe true + } + + it("retry operation backoff config should return the next backoff config") { + val retry = RetryOperation.Config(3, 1000, 1.5) + + var nextBackoffConfig = retry.nextBackOffConfig + nextBackoffConfig.maxRetries shouldBe 3 + nextBackoffConfig.nextBackOffConfig.backoffFactor shouldBe 1.5 + nextBackoffConfig.backOffInMillis shouldBe 1500 + + nextBackoffConfig = nextBackoffConfig.nextBackOffConfig + nextBackoffConfig.maxRetries shouldBe 3 + nextBackoffConfig.nextBackOffConfig.backoffFactor shouldBe 1.5 + nextBackoffConfig.backOffInMillis shouldBe 2250 + } +} diff --git a/commons/commons/src/test/scala/com/expedia/www/haystack/commons/unit/UnitTestSpec.scala b/commons/commons/src/test/scala/com/expedia/www/haystack/commons/unit/UnitTestSpec.scala new file mode 100644 index 000000000..4b0059124 --- /dev/null +++ b/commons/commons/src/test/scala/com/expedia/www/haystack/commons/unit/UnitTestSpec.scala @@ -0,0 +1,80 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.commons.unit + +import java.util.UUID + +import com.expedia.open.tracing.{Log, Span, Tag} +import org.scalatest._ +import org.scalatest.easymock.EasyMockSugar + +trait UnitTestSpec extends WordSpec with GivenWhenThen with Matchers with BeforeAndAfterAll with BeforeAndAfterEach with EasyMockSugar { + + val SERVER_SEND_EVENT = "ss" + val SERVER_RECV_EVENT = "sr" + val CLIENT_SEND_EVENT = "cs" + val CLIENT_RECV_EVENT = "cr" + protected def computeCurrentTimeInSecs: Long = { + System.currentTimeMillis() / 1000L + } + + private[commons] def generateTestSpan(serviceName: String, operation: String, duration: Long, client: Boolean, server: Boolean): Span = { + generateTestSpan(UUID.randomUUID().toString, serviceName, operation, duration, client, server) + } + + private[commons] def generateTestSpan(spanId: String, serviceName: String, operation: String, duration: Long, client: Boolean, server: Boolean): Span = { + val ts = System.currentTimeMillis() - (10 * 1000) + generateTestSpan(spanId, ts, serviceName, operation, duration, client, server) + } + + private[commons] def generateTestSpan(spanId: String, ts: Long, serviceName: String, operation: String, duration: Long, client: Boolean, server: Boolean): Span = { + + + val spanBuilder = Span.newBuilder() + spanBuilder.setTraceId(UUID.randomUUID().toString) + spanBuilder.setSpanId(spanId) + spanBuilder.setServiceName(serviceName) + spanBuilder.setOperationName(operation) + spanBuilder.setStartTime(ts) + spanBuilder.setDuration(duration) + + val logBuilder = Log.newBuilder() + if (client) { + logBuilder.setTimestamp(ts) + logBuilder.addFields(Tag.newBuilder().setKey("event").setVStr(CLIENT_SEND_EVENT).build()) + spanBuilder.addLogs(logBuilder.build()) + logBuilder.clear() + logBuilder.setTimestamp(ts + duration) + logBuilder.addFields(Tag.newBuilder().setKey("event").setVStr(CLIENT_RECV_EVENT).build()) + spanBuilder.addLogs(logBuilder.build()) + } + + if (server) { + logBuilder.setTimestamp(ts) + logBuilder.addFields(Tag.newBuilder().setKey("event").setVStr(SERVER_RECV_EVENT).build()) + spanBuilder.addLogs(logBuilder.build()) + logBuilder.clear() + logBuilder.setTimestamp(ts + duration) + logBuilder.addFields(Tag.newBuilder().setKey("event").setVStr(SERVER_SEND_EVENT).build()) + spanBuilder.addLogs(logBuilder.build()) + } + + spanBuilder.build() + } + +} diff --git a/commons/commons/src/test/scala/com/expedia/www/haystack/commons/util/MetricDefinitionKeyGeneratorSpec.scala b/commons/commons/src/test/scala/com/expedia/www/haystack/commons/util/MetricDefinitionKeyGeneratorSpec.scala new file mode 100644 index 000000000..b97ab1e39 --- /dev/null +++ b/commons/commons/src/test/scala/com/expedia/www/haystack/commons/util/MetricDefinitionKeyGeneratorSpec.scala @@ -0,0 +1,37 @@ +package com.expedia.www.haystack.commons.util + +import java.util + +import com.expedia.metrics.{MetricDefinition, TagCollection} +import com.expedia.www.haystack.commons.entities.TagKeys.PRODUCT_KEY +import com.expedia.www.haystack.commons.unit.UnitTestSpec + +class MetricDefinitionKeyGeneratorSpec extends UnitTestSpec { + "Metric Definition Key Generator" should { + "generate a unique key based on key and tags in MetricDefinition" in { + Given("a Metric Definition") + val metricDefinition = getMetricDefinition + + When("MetricDefinitionKeyGenerator is called") + val key = MetricDefinitionKeyGenerator.generateKey(metricDefinition) + + Then("a unique key is generated") + key should equal("key=duration,mtype=gauge,op=some-op,product=haystack,svc=some-svc,unit=short") + } + } + + private def getMetricDefinition: MetricDefinition = { + val metricTags = new util.LinkedHashMap[String, String] { + put("svc", "some-svc") + put("op", "some-op") + } + val tags = new util.LinkedHashMap[String, String] { + putAll(metricTags) + put(MetricDefinition.MTYPE, "gauge") + put(MetricDefinition.UNIT, "short") + put(PRODUCT_KEY, "haystack") + } + val tc = new TagCollection(tags) + new MetricDefinition("duration", tc, TagCollection.EMPTY) + } +} diff --git a/commons/idl/pom.xml b/commons/idl/pom.xml new file mode 100644 index 000000000..e0481496e --- /dev/null +++ b/commons/idl/pom.xml @@ -0,0 +1,138 @@ + + + + + + 4.0.0 + + + com.expedia.www + haystack-commons-parent + 1.0.66-SNAPSHOT + + + haystack-idl-java + jar + + + + com.google.protobuf + protobuf-java + + + + io.grpc + grpc-protobuf + provided + + + + io.grpc + grpc-stub + provided + + + + + ${basedir}/src/main/scala + + + + ${basedir}/src/main/resources + true + + + + + + org.codehaus.mojo + build-helper-maven-plugin + + + org.apache.maven.plugins + maven-compiler-plugin + + + com.github.os72 + protoc-jar-maven-plugin + + + generate-sources + + run + + + com.google.protobuf:protoc:3.0.0 + + ${project.basedir}/../haystack-idl/proto + ${project.basedir}/../haystack-idl/proto/api + ${project.basedir}/../haystack-idl/proto/backend + + + ${project.basedir}/../haystack-idl/proto + ${project.basedir}/../haystack-idl/proto/api + ${project.basedir}/../haystack-idl/proto/backend + + ${project.basedir}/target/generated-sources + + + java + + + grpc-java + io.grpc:protoc-gen-grpc-java:1.0.1 + + + + + + + + + org.apache.maven.plugins + maven-source-plugin + + + org.apache.maven.plugins + maven-javadoc-plugin + + + org.apache.maven.plugins + maven-jar-plugin + + + org.apache.maven.plugins + maven-gpg-plugin + + + org.sonatype.plugins + nexus-staging-maven-plugin + true + + ossrh + https://oss.sonatype.org/ + true + + + + + diff --git a/commons/mvnw b/commons/mvnw new file mode 100755 index 000000000..5551fde8e --- /dev/null +++ b/commons/mvnw @@ -0,0 +1,286 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven2 Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + export JAVA_HOME="`/usr/libexec/java_home`" + else + export JAVA_HOME="/Library/Java/Home" + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" + # TODO classpath? +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="`which java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +BASE_DIR=`find_maven_basedir "$(pwd)"` +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found .mvn/wrapper/maven-wrapper.jar" + fi +else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." + fi + jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" + while IFS="=" read key value; do + case "$key" in (wrapperUrl) jarUrl="$value"; break ;; + esac + done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Downloading from: $jarUrl" + fi + wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + + if command -v wget > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + fi + wget "$jarUrl" -O "$wrapperJarPath" + elif command -v curl > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + fi + curl -o "$wrapperJarPath" "$jarUrl" + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + if [ -e "$javaClass" ]; then + if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Compiling MavenWrapperDownloader.java ..." + fi + # Compiling the Java class + ("$JAVA_HOME/bin/javac" "$javaClass") + fi + if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + # Running the downloader + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Running MavenWrapperDownloader.java ..." + fi + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +if [ "$MVNW_VERBOSE" = true ]; then + echo $MAVEN_PROJECTBASEDIR +fi +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +fi + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/commons/mvnw.cmd b/commons/mvnw.cmd new file mode 100755 index 000000000..e5cfb0ae9 --- /dev/null +++ b/commons/mvnw.cmd @@ -0,0 +1,161 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven2 Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" +if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" +FOR /F "tokens=1,2 delims==" %%A IN (%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties) DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + echo Found %WRAPPER_JAR% +) else ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + powershell -Command "(New-Object Net.WebClient).DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')" + echo Finished downloading %WRAPPER_JAR% +) +@REM End of extension + +%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" +if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%" == "on" pause + +if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% + +exit /B %ERROR_CODE% diff --git a/commons/pom.xml b/commons/pom.xml new file mode 100644 index 000000000..c77ec5c44 --- /dev/null +++ b/commons/pom.xml @@ -0,0 +1,522 @@ + + + + + + 4.0.0 + + com.expedia.www + haystack-commons-parent + 1.0.66-SNAPSHOT + pom + + + scm:git:git://github.com/ExpediaDotCom/haystack-commons.git + scm:git:ssh://github.com/ExpediaDotCom/haystack-commons.git + http://github.com/ExpediaDotCom/haystack-commons + + + haystack-commons + This module contains some of the common code for haystack modules + https://github.com/ExpediaDotCom/haystack-commons/tree/master + + + + + Apache License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + + haystack + Haystack Team + haystack@expedia.com + https://github.com/ExpediaDotCom/haystack + + + + + idl + commons + + + + + 1.3.1 + 3.0.2 + 3.3.1 + 1.7.25 + 1.4 + 19.0 + + 0.8.13 + 1.1.1 + 1.7.0 + + 3.6.0 + 0.4.0 + + + 1.8 + 2 + 12 + + 7 + ${scala.major.version}.${scala.minor.version} + ${scala.major.minor.version}.${scala.maintenance.version} + + + 1.6.0 + 3.0.3 + 3.4 + + + 3.0.0 + 3.3.0.1 + 3.4.2 + 1.0 + 3.8.0 + 3.1.0 + ${project.basedir}/../scalastyle/scalastyle_config.xml + 0.9.0 + 1.3.0 + + 1.6 + 3.0.1 + 3.1.0 + 3.0.1 + 1.6.8 + + + + + + + com.google.guava + guava + ${guava.version} + provided + + + + + javax.annotation + javax.annotation-api + 1.3.2 + provided + + + + com.typesafe + config + ${typesafe-config.version} + provided + + + + + org.json4s + json4s-jackson_${scala.major.minor.version} + ${json4s.version} + provided + + + + org.codehaus.mojo + build-helper-maven-plugin + ${build-helper-maven-plugin.version} + provided + + + org.slf4j + slf4j-api + + + org.slf4j + slf4j-nop + + + org.slf4j + slf4j-jdk14 + + + org.slf4j + jcl-over-slf4j + + + + + + org.scala-lang + scala-library + ${scala-library.version} + + + + org.slf4j + slf4j-api + ${slf4j.version} + + + + com.google.protobuf + protobuf-java + ${protobuf.version} + + + + com.expedia + metrics-java + ${metrics-java.version} + + + + io.grpc + grpc-protobuf + ${grpc.version} + provided + + + io.grpc + grpc-stub + ${grpc.version} + provided + + + + com.codahale.metrics + metrics-core + ${metrics-core.version} + provided + + + + commons-codec + commons-codec + ${commons-codec.version} + + + + + org.apache.kafka + kafka_${scala.major.minor.version} + ${kafka.version} + provided + + + + org.apache.kafka + kafka-streams + ${kafka.version} + provided + + + + org.msgpack + msgpack-core + ${msgpack.version} + provided + + + + + + org.scalatest + scalatest_${scala.major.minor.version} + ${scalatest.version} + test + + + + org.easymock + easymock + ${easymock.version} + test + + + + + + org.pegdown + pegdown + ${pegdown.version} + test + + + + + + + ${basedir}/src/main/scala + + + + ${basedir}/src/main/resources + true + + + + + + + + + io.takari + maven + 0.6.1 + + + org.codehaus.mojo + build-helper-maven-plugin + ${build-helper-maven-plugin.version} + + + generate-sources + + add-source + + + + ${basedir}/src/main/java + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + ${project.jdk.version} + ${project.jdk.version} + UTF-8 + + ${maven-compiler-plugin.version} + + + + net.alchim31.maven + scala-maven-plugin + ${scala-maven-plugin.version} + + + scala-compile-first + process-resources + + add-source + compile + + + + scala-test-compile + process-test-resources + + testCompile + + + + attach-javadocs + + doc-jar + + + + + + + com.github.os72 + protoc-jar-maven-plugin + ${maven-protobuf-plugin.version} + + + + org.scalatest + scalatest-maven-plugin + ${maven-scalatest-plugin.version} + + + test + + test + + + + + + + org.scalastyle + scalastyle-maven-plugin + ${maven-scalastyle-plugin.version} + + false + true + true + false + ${project.basedir}/src/main/scala + ${project.basedir}/src/test/scala + ${scalastyle.config.location} + ${project.build.directory}/scalastyle-output.xml + UTF-8 + + + + compile-scalastyle + + check + + compile + + + + + + org.scoverage + scoverage-maven-plugin + ${scoverage.plugin.version} + + + 80 + true + true + ${scala-library.version} + true + + + + + org.apache.maven.plugins + maven-source-plugin + ${maven-source-plugin.version} + + + attach-sources + + jar-no-fork + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + ${maven-javadoc-plugin.version} + + + attach-javadocs + + jar + + + + + + org.apache.maven.plugins + maven-jar-plugin + ${maven-jar-plugin-version} + + + org.apache.maven.plugins + maven-gpg-plugin + ${maven-gpg-plugin.version} + + + sign-artifacts + verify + + sign + + + + + + org.sonatype.plugins + nexus-staging-maven-plugin + ${nexus-staging-maven-plugin.version} + true + + ossrh + https://oss.sonatype.org/ + true + + + + + + + + org.apache.maven.plugins + maven-source-plugin + + + org.apache.maven.plugins + maven-javadoc-plugin + + + org.apache.maven.plugins + maven-jar-plugin + ${maven-jar-plugin-version} + + + org.apache.maven.plugins + maven-gpg-plugin + + + org.sonatype.plugins + nexus-staging-maven-plugin + + + + + + + ossrh + https://oss.sonatype.org/content/repositories/snapshots + + + ossrh + http://oss.sonatype.org/service/local/staging/deploy/maven2/ + + + + diff --git a/commons/scalastyle/scalastyle_config.xml b/commons/scalastyle/scalastyle_config.xml new file mode 100644 index 000000000..e0cd28086 --- /dev/null +++ b/commons/scalastyle/scalastyle_config.xml @@ -0,0 +1,136 @@ + + Scalastyle standard configuration + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docker/.gitignore b/docker/.gitignore new file mode 100644 index 000000000..1062418c4 --- /dev/null +++ b/docker/.gitignore @@ -0,0 +1,2 @@ +.idea/ +*.iml diff --git a/docker/LICENSE b/docker/LICENSE new file mode 100644 index 000000000..9f133f5cd --- /dev/null +++ b/docker/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2017 Expedia, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/docker/README.md b/docker/README.md new file mode 100644 index 000000000..4717ea79d --- /dev/null +++ b/docker/README.md @@ -0,0 +1,103 @@ +- [Running Haystack using docker-compose](#running-haystack-using-docker-compose) + * [Allocate memory to docker](#allocate-memory-to-docker) + * [To start Haystack's traces, blobs, trends, service graph and adaptive-alerting](#to-start-haystacks-traces-blobs-trends-service-graph-and-adaptive-alerting) + * [To start Zipkin (tracing) with Haystack's trends, service graph and adaptive-alerting](#to-start-zipkin-tracing-with-haystacks-trends-service-graph-and-adaptive-alerting) + * [Note on composing components](#note-on-composing-components) + +## Running Haystack using docker-compose + +### Allocate memory to docker + +Please check this [Stackoverflow answer](https://stackoverflow.com/questions/44533319/how-to-assign-more-memory-to-docker-container) + +To run all of haystack and its components, __it is suggested to change the default in docker settings from `2GiB` to `6GiB`__ + +### To start Haystack's traces, trends, service graph and adaptive-alerting + +```bash +docker-compose -f docker-compose.yml \ + -f traces/docker-compose.yml \ + -f trends/docker-compose.yml \ + -f service-graph/docker-compose.yml \ + -f adaptive-alerting/docker-compose.yml \ + -f agent/docker-compose.yml \ + -f example/traces/docker-compose.yml up +``` + +The command above starts haystack components, and two sample web applications with the haystack-agent. It may take a minute or two for the containers to come up and connect with each other. + +Haystack's UI will be available at http://localhost:8080 + +Haystack's agent will be available on host port 35000 (i.e., localhost: 35000). + +[Sample application](https://github.com/ExpediaDotCom/opentracing-spring-haystack-example) has a 'frontend' and 'backend'. The 'frontend' app will be available at http://localhost:9090/hello. Sending a request to frontend will cause a call to the backend before fulfilling this request. + +Send some sample requests to the 'frontend' application by running + +```bash +run.sh +``` + +One can then see the traces, trends and a service-graph showing the relationship between the two applications in the UI. + +### To start Haystack's traces, blobs, trends, service graph and adaptive-alerting + +```bash +docker-compose -f docker-compose.yml \ + -f traces/docker-compose.yml \ + -f trends/docker-compose.yml \ + -f service-graph/docker-compose.yml \ + -f adaptive-alerting/docker-compose.yml \ + -f agent/docker-compose.yml \ + -f example/blobs/docker-compose.yml up +``` + +The command above starts haystack components, and two sample web applications with the haystack-agent. It may take a minute or two for the containers to come up and connect with each other. + +Haystack's UI will be available at http://localhost:8080 + +Haystack's agent will be available in port 35000 in the host (i.e., localhost: 35000). + +[Sample Application](https://github.com/ExpediaDotCom/haystack-blob-example) has a 'client' and a 'server'. The client interacts with the server listening on port `9090`. The client app will be available at `http://localhost:9091/displayMessage`. Sending a request to client will cause a call to the server before fulfilling this request. + +Call the client using the link given above and then you will be able to see the traces, trends and a service-graph showing the relationship between the two applications in the UI. + +Alternatively, you can also send some sample requests to the 'server' application by running + +```bash +run.sh +``` + +### To start Zipkin (tracing) with Haystack's trends, service graph and adaptive-alerting + +```bash +docker-compose -f docker-compose.yml \ + -f zipkin/docker-compose.yml \ + -f trends/docker-compose.yml \ + -f adaptive-alerting/docker-compose.yml \ + -f service-graph/docker-compose.yml up +``` + +The command above starts [Pitchfork](https://github.com/HotelsDotCom/pitchfork) to proxy data to [Zipkin](https://github.com/openzipkin/) and Haystack. + +Give a minute or two for the containers to come up and connect with each other. Once the stack is up, one can use the sample application @ https://github.com/openzipkin/brave-webmvc-example and send some sample data to see traces (from Zipkin), trends and service-graph in haystack-ui @ http://localhost:8080 + +### Note on composing components + +Note the two commands above combine a series of `docker-compose.yml` files. + +- Haystack needs at least one trace provider ( `traces/docker-compose.yml` or `zipkin/docker-compose.yml` ) and one trends provider ( `trends/docker-compose.yml` ) +- One can remove `adaptive-alerting/docker-compose.yml` and `service-graph/docker-compose.yml` if those components are not required +- One can remove `examples/traces/docker-compose.yml` or `examples/blobs/docker-compose.yml` and just have `agent/docker-compose.yml` to start your application integrated with haystack to send data +- If one is using Zipkin instrument app, use `zipkin/docker-compose.yml` to send data to the stack and use trends, service-graph and adaptive-alerting as needed +- Starting the stack with only the base docker-compose.yml will start core services like kafka, cassandra and elastic-search along with haystack-ui with mock backend + +```bash +docker-compose -f docker-compose.yml up +``` + +### Note on Adaptive Alerting + +- Model Service that fetches anomaly detection model for a specific metric has been replaced with a mocked (using wiremock) to allow the stack to use a default model. Default detection model us [EWMA](https://en.wikipedia.org/wiki/EWMA_chart) +- Model Service is being refactored to allow better model selection and we will be releasing it in the next month or two +- Alert-Notification service that dispatches alerts to either email or slack is [commented in docker-compose](adaptive-alerting/docker-compose.yml#L100) file for local testing. You can uncomment it and provide slack_token or smtp credentials via environment. diff --git a/docker/adaptive-alerting/configs/aa-manager/docker.conf b/docker/adaptive-alerting/configs/aa-manager/docker.conf new file mode 100644 index 000000000..150ea4d20 --- /dev/null +++ b/docker/adaptive-alerting/configs/aa-manager/docker.conf @@ -0,0 +1,9 @@ +ad-manager { + streams { + bootstrap.servers = "kafkasvc:9092" + application.id = "ad-manager" + timestamp.extractor = "com.expedia.adaptivealerting.kafka.serde.MappedMetricDataTimestampExtractor" + JsonPojoClass = "com.expedia.adaptivealerting.core.data.MappedMetricData" + } + model-service-uri-template = "http://modelservice:8080/api/models/search/findByMetricHash?hash=%s" +} diff --git a/docker/adaptive-alerting/configs/aa-mapper/docker.conf b/docker/adaptive-alerting/configs/aa-mapper/docker.conf new file mode 100644 index 000000000..6c04a393b --- /dev/null +++ b/docker/adaptive-alerting/configs/aa-mapper/docker.conf @@ -0,0 +1,11 @@ +ad-mapper { + streams { + bootstrap.servers = "kafkasvc:9092" + application.id = "ad-mapper" + default.value.serde = "com.expedia.adaptivealerting.kafka.serde.MetricDataSerde" + timestamp.extractor = "com.expedia.adaptivealerting.kafka.serde.MetricDataTimestampExtractor" + JsonPojoClass = "com.expedia.metrics.MetricData" + } + inbound-topic = "mdm" + model-service-uri-template = "http://modelservice:8080/api/detectors/search/findByMetricHash?hash=%s" +} diff --git a/docker/adaptive-alerting/db_init/init_db.sh b/docker/adaptive-alerting/db_init/init_db.sh new file mode 100644 index 000000000..7ef8e8203 --- /dev/null +++ b/docker/adaptive-alerting/db_init/init_db.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env sh + +mysql -u root -proot < /home/build-db.sql +mysql -u root -proot < /home/stored-procs.sql +mysql -u root -proot < /home/sample-data.sql diff --git a/docker/adaptive-alerting/docker-compose.yml b/docker/adaptive-alerting/docker-compose.yml new file mode 100644 index 000000000..f154d19c5 --- /dev/null +++ b/docker/adaptive-alerting/docker-compose.yml @@ -0,0 +1,163 @@ +version: "3" +services: + +# database: +# image: mysql:5.7 +# ports: +# - "3306:3306" +# restart: always +# volumes: +# - ./adaptive-alerting/sql:/home/ +# - ./adaptive-alerting/db_init:/docker-entrypoint-initdb.d +# command: --default-authentication-plugin=mysql_native_password +# environment: +# MYSQL_ROOT_PASSWORD: root +# MYSQL_DATABASE: aa_model_service +# +# +# modelservice: +# image: expediadotcom/adaptive-alerting-modelservice:e90821e5ca0e0d895e01d9cb87612c463dcf0dc6 +# ports: +# - "8008:8008" +# environment: +# AA_GRAPHITE_ENABLED : "false" +# SPRING_CONFIG_LOCATION : "classpath:/application.yml" +# PASSWORD : "root" +# JAVA_XMS: 128m +# depends_on: +# - database +# links: +# - database +# restart: always + + modelservice: + image: rodolpheche/wiremock +# ports: +# - "8500:8080" + volumes: + - ./adaptive-alerting/stubs:/home/wiremock/ + + + aa-mapper: + image: expediadotcom/adaptive-alerting-ad-mapper:e90821e5ca0e0d895e01d9cb87612c463dcf0dc6 + environment: + AA_GRAPHITE_ENABLED : "false" + AA_OVERRIDES_CONFIG_PATH : "/home/docker.conf" + JAVA_XMS: 128m + volumes: + - ./adaptive-alerting/configs/aa-mapper:/home + depends_on: + - kafkasvc + - zookeeper + - modelservice + links: + - kafkasvc + - modelservice + restart: always + + aa-manager: + image: expediadotcom/adaptive-alerting-ad-manager:e90821e5ca0e0d895e01d9cb87612c463dcf0dc6 + environment: + AA_GRAPHITE_ENABLED : "false" + AA_OVERRIDES_CONFIG_PATH : "/home/docker.conf" + JAVA_XMS: 128m + volumes: + - ./adaptive-alerting/configs/aa-manager:/home + depends_on: + - kafkasvc + - zookeeper + - modelservice + links: + - kafkasvc + - modelservice + restart: always + + anomaly-to-alert: + image: expediadotcom/adaptive-alerting-a2a-mapper:3482d04bcee0c62b1d64e5cf9b289f243d03a77b + environment: + AA_GRAPHITE_ENABLED : "false" + JAVA_XMS: 128m + depends_on: + - kafkasvc + - zookeeper + links: + - kafkasvc + restart: always + + subscription: + image: expediadotcom/alert-manager-service:67a10b9e28dfc51e806b9ee629ad91a7dfc1d505 + environment: + AM_GRAPHITE_ENABLED : "false" + SPRING_CONFIG_LOCATION : "classpath:/application.yml" + JAVA_XMS: 128m + depends_on: + - elasticsearch + links: + - elasticsearch + restart: always + + +# tell about kafka client to read alerts from kafka topic +# notifier: +# image: expediadotcom/alert-manager-notifier:3117ece498d619d560939b5acc6bc948b23899da +# environment: +# AM_GRAPHITE_ENABLED : "false" +# SPRING_CONFIG_LOCATION : "classpath:/application.yml" +## SMTP_PASSWORD: "password" +## SMTP_USERNAME: "user" +## SMTP_HOST: "localhost" +## SLACK_TOKEN: "token" +# JAVA_XMS: 128m +# depends_on: +# - elasticsearch +# - subscription +# - zookeeper +# - kafkasvc +# links: +# - elasticsearch +# - kafkasvc +# - subscription +# restart: always + + + alert-api: + image: expediadotcom/haystack-alert-api:f63141bbc1c1e671766b5e83b0e8efb605ab0402 + ports: + - "4500:8088" + environment: + HAYSTACK_GRAPHITE_ENABLED: "false" + HAYSTACK_LOG_LEVEL: "INFO" + JAVA_XMS: 128m + depends_on: + - elasticsearch + - subscription + links: + - elasticsearch + - subscription + restart: always + + + anomaly-store: + image: expediadotcom/haystack-anomaly-store:f63141bbc1c1e671766b5e83b0e8efb605ab0402 + environment: + HAYSTACK_GRAPHITE_ENABLED: "false" + HAYSTACK_LOG_LEVEL: "INFO" + JAVA_XMS: 128m + depends_on: + - elasticsearch + - kafkasvc + links: + - kafkasvc + - elasticsearch + restart: always + + + ui: + environment: + HAYSTACK_OVERRIDES_CONFIG_PATH: /data/connectors.json + HAYSTACK_PROP_CONNECTORS_ALERTS_CONNECTOR__NAME: "haystack" + HAYSTACK_PROP_CONNECTORS_ALERTS_SUBSCRIPTIONS_CONNECTOR__NAME: "haystack" + HAYSTACK_PROP_CONNECTORS_ALERTS_HAYSTACK__HOST: "alert-api" + HAYSTACK_PROP_CONNECTORS_ALERTS_HAYSTACK__PORT: "8088" + + diff --git a/docker/adaptive-alerting/sql/build-db.sql b/docker/adaptive-alerting/sql/build-db.sql new file mode 100644 index 000000000..53bc7ba7a --- /dev/null +++ b/docker/adaptive-alerting/sql/build-db.sql @@ -0,0 +1,75 @@ +DROP DATABASE IF EXISTS aa_model_service; + +CREATE DATABASE aa_model_service; + +USE aa_model_service; + +create table metric ( + id int unsigned primary key not null auto_increment, + ukey varchar(255) unique not null, + hash char(36) unique not null, + description varchar(255), + tags json, + date_created timestamp default CURRENT_TIMESTAMP +); + +create table model_type ( + id smallint unsigned primary key not null auto_increment, + ukey varchar(100) unique not null, + date_created timestamp default CURRENT_TIMESTAMP +); + +CREATE TABLE detector ( + id int unsigned primary key NOT NULL AUTO_INCREMENT, + uuid char(36) unique not null, + model_type_id smallint unsigned not null, + hyperparams json, + training_meta json, + seyren_flag boolean default false, + date_created timestamp NULL DEFAULT CURRENT_TIMESTAMP, + created_by varchar(100), + constraint model_type_id_fk foreign key (model_type_id) references model_type (id) +); + +create table model ( + id int unsigned primary key not null auto_increment, + params json, + detector_id int unsigned not null, + weak_sigmas decimal(3, 3), + strong_sigmas decimal(3, 3), + other_stuff json, + date_created timestamp default CURRENT_TIMESTAMP, + constraint detector_id_fk foreign key (detector_id) references detector (id) +); + +create table metric_detector_mapping ( + id int unsigned primary key not null auto_increment, + metric_id int unsigned not null, + detector_id int unsigned not null, + date_created timestamp default CURRENT_TIMESTAMP, + constraint metric_id_fk foreign key (metric_id) references metric (id), + constraint detector_id_mapping_fk foreign key (detector_id) references detector (id), + unique index (metric_id, detector_id) +); + +create table user ( + id int unsigned primary key not null auto_increment, + username varchar(100) unique not null, + password varchar(100) not null, + role varchar(100), + enabled boolean +); + +create table oauth_client_details ( + client_id VARCHAR(256) PRIMARY KEY, + resource_ids VARCHAR(256), + client_secret VARCHAR(256), + scope VARCHAR(256), + authorized_grant_types VARCHAR(256), + web_server_redirect_uri VARCHAR(256), + authorities VARCHAR(256), + access_token_validity INTEGER, + refresh_token_validity INTEGER, + additional_information VARCHAR(4096), + autoapprove VARCHAR(256) +); diff --git a/docker/adaptive-alerting/sql/sample-data.sql b/docker/adaptive-alerting/sql/sample-data.sql new file mode 100644 index 000000000..41b8c2c79 --- /dev/null +++ b/docker/adaptive-alerting/sql/sample-data.sql @@ -0,0 +1,29 @@ +USE `aa_model_service`; + +INSERT INTO `metric` (`ukey`, `hash`, `tags`) VALUES + ('karmalab.stats.gauges.AirBoss.chelappabo003_karmalab_net.java.lang.Threading.ThreadCount', '1.71828d68a2938ff1ef96c340f12e2dd6', '{"unit": "unknown", "mtype": "gauge", "org_id": "1", "interval": "30"}') +; +INSERT INTO `metric` (`ukey`, `hash`, `tags`) VALUES + ('dummy.metric', '1.25345234523452352253452f12e2dd6', '{"unit": "unknown", "mtype": "gauge", "org_id": "1", "interval": "30"}') +; + + +INSERT INTO `model_type` (`ukey`) VALUES + ('constant-detector'), + ('cusum-detector'), + ('ewma-detector'), + ('individuals-detector'), + ('pewma-detector'), + ('rcf-detector') +; + +CALL insert_detector('3ec81aa2-2cdc-415e-b4f3-c1beb223ae60','cusum-detector'); +CALL insert_detector('2cdc1aa2-2cdc-355e-b4f3-d2beb223ae60','constant-detector'); + +CALL insert_model('3ec81aa2-2cdc-415e-b4f3-c1beb223ae60','{"alpha":40,"beta":30}', '2018-10-10 10:02:04'); +CALL insert_model('3ec81aa2-2cdc-415e-b4f3-c1beb223ae60','{"alpha":100,"beta":455}','2018-10-12 17:01:04'); +CALL insert_model('2cdc1aa2-2cdc-355e-b4f3-d2beb223ae60','{"low":100,"high":455}','2018-10-10 10:01:04'); +CALL insert_model('2cdc1aa2-2cdc-355e-b4f3-d2beb223ae60','{"low":100,"high":455}', '2018-10-12 17:01:04'); + +CALL insert_mapping('1.71828d68a2938ff1ef96c340f12e2dd6', '3ec81aa2-2cdc-415e-b4f3-c1beb223ae60'); +CALL insert_mapping('1.25345234523452352253452f12e2dd6', '2cdc1aa2-2cdc-355e-b4f3-d2beb223ae60'); diff --git a/docker/adaptive-alerting/sql/stored-procs.sql b/docker/adaptive-alerting/sql/stored-procs.sql new file mode 100644 index 000000000..90e7cc4a2 --- /dev/null +++ b/docker/adaptive-alerting/sql/stored-procs.sql @@ -0,0 +1,73 @@ +USE `aa_model_service`; + +DROP PROCEDURE IF EXISTS insert_mapping; +DROP PROCEDURE IF EXISTS insert_model; +DROP PROCEDURE IF EXISTS insert_detector; + +DROP PROCEDURE IF EXISTS insert_mapping_wildcard_metric_targets_to_model; +DELIMITER // + +CREATE PROCEDURE insert_detector ( + IN uuid CHAR(36), + IN type_ukey VARCHAR(100) +) + BEGIN + DECLARE type_id INT(5) UNSIGNED; + + SELECT t.id INTO type_id FROM model_type t WHERE t.ukey = type_ukey; + INSERT INTO detector (uuid, model_type_id) VALUES (uuid, type_id); + END // + +CREATE PROCEDURE insert_model ( + IN uuid CHAR(36), + IN params json, + IN date_created timestamp +) + BEGIN + DECLARE detector_id INT(5) UNSIGNED; + + SELECT d.id INTO detector_id FROM detector d WHERE d.uuid = uuid; + INSERT INTO model (detector_id, params , date_created) VALUES (detector_id, params, date_created); + END // + +CREATE PROCEDURE insert_mapping ( + IN metric_hash CHAR(36), + IN detector_uuid CHAR(36) +) + BEGIN + DECLARE metric_id INT(10) UNSIGNED; + DECLARE detector_id INT(10) UNSIGNED; + + SELECT m.id INTO metric_id FROM metric m WHERE m.hash = metric_hash; + SELECT m.id INTO detector_id FROM detector m WHERE m.uuid = detector_uuid; + INSERT INTO metric_detector_mapping (metric_id, detector_id) VALUES (metric_id, detector_id); + END // + +CREATE PROCEDURE insert_mapping_wildcard_metric_targets_to_detector ( + IN detector_uuid CHAR(36), + IN metric_ukey CHAR(100) +) + BEGIN + DECLARE metric_id INT(10) UNSIGNED; + DECLARE detector_id INT(10) UNSIGNED; + DECLARE done INT DEFAULT 0; + DECLARE present INT DEFAULT 0; + DECLARE cur1 cursor for SELECT m.id FROM metric m WHERE m.ukey LIKE metric_ukey; + DECLARE continue handler for not found set done=1; + + open cur1; + + REPEAT + FETCH cur1 into metric_id; + if NOT done then + SELECT id INTO detector_id FROM detector WHERE uuid = detector_uuid; + IF NOT EXISTS (SELECT 1 FROM metric_detector_mapping m3 WHERE m3.metric_id = metric_id and m3.detector_id = detector_id) + THEN + INSERT INTO metric_detector_mapping (metric_id, detector_id) VALUES (metric_id, detector_id); + END IF; + END IF; + UNTIL done END REPEAT; + close cur1; + END // + +DELIMITER ; diff --git a/docker/adaptive-alerting/stubs/__files/get_detectors.json b/docker/adaptive-alerting/stubs/__files/get_detectors.json new file mode 100644 index 000000000..9a73fce0a --- /dev/null +++ b/docker/adaptive-alerting/stubs/__files/get_detectors.json @@ -0,0 +1,10 @@ +{ + "_embedded": { + "models": [] + }, + "_links": { + "self": { + "href": "http://modelservice:8080/api/detectors/search/findByMetricHash?hash=1234" + } + } +} diff --git a/docker/adaptive-alerting/stubs/__files/get_models.json b/docker/adaptive-alerting/stubs/__files/get_models.json new file mode 100644 index 000000000..bff3f9d94 --- /dev/null +++ b/docker/adaptive-alerting/stubs/__files/get_models.json @@ -0,0 +1,10 @@ +{ + "_embedded": { + "detectors": [] + }, + "_links": { + "self": { + "href": "http://modelservice:8080/api/models/search/findByMetricHash?hash=1234" + } + } +} diff --git a/docker/adaptive-alerting/stubs/mappings/get-detectors.json b/docker/adaptive-alerting/stubs/mappings/get-detectors.json new file mode 100644 index 000000000..7e7795a36 --- /dev/null +++ b/docker/adaptive-alerting/stubs/mappings/get-detectors.json @@ -0,0 +1,15 @@ +{ + "request": { + "method": "GET", + "urlPath": "/api/detectors/search/findByMetricHash", + "queryParameters": { + "hash" : { + "matches" : ".*" + } + } + }, + "response": { + "status": 200, + "bodyFileName": "get_detectors.json" + } +} diff --git a/docker/adaptive-alerting/stubs/mappings/get-models.json b/docker/adaptive-alerting/stubs/mappings/get-models.json new file mode 100644 index 000000000..b5bddf483 --- /dev/null +++ b/docker/adaptive-alerting/stubs/mappings/get-models.json @@ -0,0 +1,16 @@ +{ + "request": { + "method": "GET", + "urlPath": "/api/models/search/findByMetricHash", + "queryParameters": { + "hash" : { + "matches" : ".*" + } + } + + }, + "response": { + "status": 200, + "bodyFileName": "get_models.json" + } +} diff --git a/docker/agent/default.conf b/docker/agent/default.conf new file mode 100644 index 000000000..fe1125af4 --- /dev/null +++ b/docker/agent/default.conf @@ -0,0 +1,55 @@ +agents { + spans { + enabled = true + port = 35000 + dispatchers { + kafka { + bootstrap.servers = "kafkasvc:9092" + producer.topic = "proto-spans" + buffer.memory = 1048576 + retries = 2 + } + } + } + ossblobs { + enabled = false + port = 35001 + max.blob.size.in.kb = 512 + dispatchers { + s3 { + keep.alive = true + max.outstanding.requests = 150 + should.wait.for.upload = false + max.connections = 50 + retry.count = 1 + bucket.name = "haystack-blobs" + region = "us-east-1" + aws.access.key = "accessKey" + aws.secret.key = "secretKey" + } + } + } + + pitchfork { + enabled = false + port = 9411 + http.threads { + max = 16 + min = 2 + } + gzip.enabled = true + idle.timeout.ms = 60000 + stop.timeout.ms = 30000 + accept.null.timestamps = false + max.timestamp.drift.sec = -1 + + dispatchers { + kafka { + bootstrap.servers = "kafkasvc:9092" + producer.topic = "proto-spans" + buffer.memory = 1048576 + retries = 2 + } + } + } +} diff --git a/docker/agent/docker-compose.yml b/docker/agent/docker-compose.yml new file mode 100644 index 000000000..a123fbb7a --- /dev/null +++ b/docker/agent/docker-compose.yml @@ -0,0 +1,13 @@ +version: "3" +services: + haystack-agent: + image: expediadotcom/haystack-agent:latest + volumes: + # make sure you run docker-compose from the + # root path + - ./agent/default.conf:/app/bin/default.conf + environment: + JAVA_XMS: 128m + haystack_env_agents_spans_port: 35000 + ports: + - "35000:35000" diff --git a/docker/connectors.json b/docker/connectors.json new file mode 100644 index 000000000..0d0e9a22a --- /dev/null +++ b/docker/connectors.json @@ -0,0 +1,3 @@ +{ + "connectors": { } +} diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 000000000..761fa4f08 --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,64 @@ +# +# Copyright 2018 Expedia, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +version: "3" +services: + elasticsearch: + image: elastic/elasticsearch:6.0.1 + environment: + ES_JAVA_OPTS: "-Xms512m -Xmx512m" + xpack.security.enabled: "false" + ports: + - "9200:9200" + restart: always + + cassandra: + image: cassandra:3.11.0 + environment: + MAX_HEAP_SIZE: 256m + HEAP_NEWSIZE: 256m + # uncomment below port mapping to expose and connect to this application out of local docker container network +# ports: +# - "9042:9042" + + zookeeper: + image: wurstmeister/zookeeper + ports: + - "2181:2181" + + kafkasvc: + image: wurstmeister/kafka:2.11-1.1.1 + depends_on: + - zookeeper + environment: + KAFKA_BROKER_ID: 1 + KAFKA_ADVERTISED_LISTENERS: INSIDE://kafkasvc:9092,OUTSIDE://localhost:19092 + KAFKA_LISTENERS: INSIDE://:9092,OUTSIDE://:19092 + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: INSIDE:PLAINTEXT,OUTSIDE:PLAINTEXT + KAFKA_INTER_BROKER_LISTENER_NAME: INSIDE + KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 + KAFKA_CREATE_TOPICS: "proto-spans:1:1,metricpoints:1:1,metric-data-points:1:1,mdm:1:1,metrics:1:1,graph-nodes:1:1,service-graph:1:1,mapped-metrics:1:1,anomalies:1:1,alerts:1:1" + volumes: + - /var/run/docker.sock:/var/run/docker.sock + ports: + - "9092:9092" + - "19092:19092" + + ui: + image: expediadotcom/haystack-ui:1.1.7 + volumes: + - ./:/data + ports: + - "8080:8080" diff --git a/docker/example/blobs/docker-compose.yml b/docker/example/blobs/docker-compose.yml new file mode 100644 index 000000000..a70dc379f --- /dev/null +++ b/docker/example/blobs/docker-compose.yml @@ -0,0 +1,56 @@ +version: '3' +services: + s3rver: + image: vaibhavsawhney1511/s3rver + ports: + - "5000:5000" + + haystack-agent: + # it is expected that this example is run along with the + # agent/docker-compose.yml file + + depends_on: + - s3rver + environment: + JAVA_XMS: 128m + haystack_env_agents_spans_port: 35000 + HAYSTACK_PROP_AGENTS_OSSBLOBS_ENABLED: "true" + HAYSTACK_PROP_AGENTS_OSSBLOBS_PORT: 35001 + HAYSTACK_PROP_AGENTS_OSSBLOBS_DISPATCHERS_S3_SERVICE_ENDPOINT: http://s3rver:5000 + HAYSTACK_PROP_AGENTS_OSSBLOBS_DISPATCHERS_S3_PATH_STYLE_ACCESS_ENABLED: "true" + HAYSTACK_PROP_AGENTS_OSSBLOBS_DISPATCHERS_S3_DISABLE_CHUNKED_ENCODING: "true" + HAYSTACK_PROP_AGENTS_OSSBLOBS_DISPATCHERS_S3_aws_access_key: "S3RVER" + HAYSTACK_PROP_AGENTS_OSSBLOBS_DISPATCHERS_S3_aws_secret_key: "S3RVER" + HAYSTACK_PROP_AGENTS_OSSBLOBS_DISPATCHERS_S3_BUCKET_NAME: "s3rver" + HAYSTACK_PROP_AGENTS_OSSBLOBS_DISPATCHERS_S3_SHOULD_WAIT_FOR_UPLOAD: "false" + ports: + - "35000:35000" + - "35001:35001" + + reverse-proxy: + image: expediadotcom/blobs-http-reverse-proxy:latest + depends_on: + - haystack-agent + environment: + grpc-server-endpoint: haystack-agent:35001 + http-port: ":35002" + ports: + - "35002:35002" + + haystack-blob-example-client: + image: expediadotcom/haystack-blob-example-client:4b43b0858d8be7455a830df430e7f0a4a0a8afbf + depends_on: + - haystack-blob-example-server + expose: + - "9091" + ports: + - "9091:9091" + + haystack-blob-example-server: + image: expediadotcom/haystack-blob-example-server:4b43b0858d8be7455a830df430e7f0a4a0a8afbf + depends_on: + - reverse-proxy + expose: + - "9090" + ports: + - "9090:9090" \ No newline at end of file diff --git a/docker/example/opentelemetry/docker-compose.yml b/docker/example/opentelemetry/docker-compose.yml new file mode 100644 index 000000000..1ad64a7d9 --- /dev/null +++ b/docker/example/opentelemetry/docker-compose.yml @@ -0,0 +1,35 @@ +# +# Copyright 2018 Expedia, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +version: "3" + +services: + haystack-agent: + # it is expected that this example is run along with the + # agent/docker-compose.yml file + + environment: + HAYSTACK_PROP_AGENTS_PITCHFORK_ENABLED: "true" + ports: + - "9411:9411" + expose: + - "9411" + + haystack-opentelemetry-example: + image: expediadotcom/haystack-opentelemetry-example:latest + depends_on: + - haystack-agent + ports: + - "9090:9090" diff --git a/docker/example/traces/docker-compose.yml b/docker/example/traces/docker-compose.yml new file mode 100644 index 000000000..8aa875047 --- /dev/null +++ b/docker/example/traces/docker-compose.yml @@ -0,0 +1,28 @@ +version: "3" +services: + haystack-agent: + # it is expected that this example is run along with the + # agent/docker-compose.yml file + + frontend: + image: expediadotcom/opentracing-spring-haystack-example + environment: + APP_MODE: frontend + SPRING_PROFILE: remote + HAYSTACK_AGENT_HOST: haystack-agent + BACKEND_URL: http://backend:9091 + HAYSTACK_BLOBS_ENABLED: "false" + depends_on: + - haystack-agent + ports: + - "9090:9090" + + backend: + image: expediadotcom/opentracing-spring-haystack-example + environment: + APP_MODE: backend + SPRING_PROFILE: remote + HAYSTACK_AGENT_HOST: haystack-agent + HAYSTACK_BLOBS_ENABLED: "false" + depends_on: + - haystack-agent diff --git a/docker/run.sh b/docker/run.sh new file mode 100755 index 000000000..d697c6c93 --- /dev/null +++ b/docker/run.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash + +curl -XPOST -H "Content-Type: application/json" -d '{ + "fields": [{ + "name": "error", + "type": "string", + "enabled": true + }, { + "name": "http-status", + "type": "string", + "aliases": [ "http.status_code" ], + "enabled": true + }] +}' "http://localhost:9200/reload-configs/whitelist-index-fields/1" 2>1 1>/dev/null + +COUNT=0 +URL=$1 + +[[ -z ${URL} ]] && URL=http://localhost:9090/hello + +while true +do + COUNT=$((COUNT+1)) + curl ${URL} + echo " ${COUNT}" + sleep 1 +done diff --git a/docker/service-graph/docker-compose.yml b/docker/service-graph/docker-compose.yml new file mode 100644 index 000000000..ae6cb6c9a --- /dev/null +++ b/docker/service-graph/docker-compose.yml @@ -0,0 +1,49 @@ +# +# Copyright 2018 Expedia, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +version: "3" +services: + node-finder: + image: expediadotcom/haystack-service-graph-node-finder:1.0.17 + environment: + JAVA_XMS: 128m + HAYSTACK_GRAPHITE_ENABLED: "false" + HAYSTACK_PROP_KAFKA_NODE_METADATA_TOPIC_PARTITION_COUNT: "1" + HAYSTACK_PROP_KAFKA_NODE_METADATA_TOPIC_REPLICATION_FACTOR: "1" + depends_on: + - "kafkasvc" + entrypoint: ["/dockerize","-wait=tcp://kafkasvc:9092","-timeout=200s","-wait-retry-interval=40s","--","./start-app.sh"] + restart: always + + graph-builder: + image: expediadotcom/haystack-service-graph-graph-builder:1.0.17 + environment: + JAVA_XMS: 128m + HAYSTACK_GRAPHITE_ENABLED: "false" + HAYSTACK_PROP_SERVICE_HTTP_PORT: "8091" + HAYSTACK_PROP_KAFKA_STREAMS_REPLICATION_FACTOR: 1 + depends_on: + - "kafkasvc" + entrypoint: ["/dockerize","-wait=tcp://kafkasvc:9092","-timeout=200s","-wait-retry-interval=40s","--","./start-app.sh"] + restart: always + ports: + - "8091:8091" + + ui: + environment: + HAYSTACK_OVERRIDES_CONFIG_PATH: /data/connectors.json + HAYSTACK_PROP_CONNECTORS_SERVICE__GRAPH_CONNECTOR__NAME: "haystack" + HAYSTACK_PROP_CONNECTORS_SERVICE__GRAPH_WINDOW__SIZE__IN__SECS: 3600 + HAYSTACK_PROP_CONNECTORS_SERVICE__GRAPH_SERVICE__GRAPH__URL: "http://graph-builder:8091/servicegraph" diff --git a/docker/traces/docker-compose.yml b/docker/traces/docker-compose.yml new file mode 100644 index 000000000..ef12d16f3 --- /dev/null +++ b/docker/traces/docker-compose.yml @@ -0,0 +1,63 @@ +# +# Copyright 2018 Expedia, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +version: "3" +services: + storage-backend: + image: expediadotcom/haystack-trace-backend-cassandra + environment: + HAYSTACK_GRAPHITE_ENABLED: "false" + HAYSTACK_LOG_LEVEL: "DEBUG" + JAVA_XMS: 128m + depends_on: + - "cassandra" + entrypoint: ["/dockerize","-wait=tcp://cassandra:9042","-timeout=200s","-wait-retry-interval=20s","--","./start-app.sh"] + restart: always + # uncomment below port mapping to expose and connect to this application out of local docker container network +# ports: +# - "8090:8090" + + trace-reader: + image: expediadotcom/haystack-trace-reader:1.0.10 + environment: + HAYSTACK_GRAPHITE_ENABLED: "false" + HAYSTACK_PROP_BACKEND_CLIENT_HOST: "storage-backend" + JAVA_XMS: 128m + entrypoint: ["/dockerize","-wait=tcp://storage-backend:8090","-timeout=200s","-wait-retry-interval=40s","--","./start-app.sh"] + restart: always + + trace-indexer: + image: expediadotcom/haystack-trace-indexer:1.0.10 + environment: + HAYSTACK_GRAPHITE_ENABLED: "false" + HAYSTACK_PROP_BACKEND_CLIENT_HOST: "storage-backend" + HAYSTACK_PROP_SERVICE_METADATA_ENABLED: "true" + HAYSTACK_PROP_KAFKA_MAX_WAKEUPS: "100" + HAYSTACK_PROP_SERVICE_METADATA_FLUSH_INTERVAL_SEC: "0" + JAVA_XMS: 128m + entrypoint: ["/dockerize","-wait=tcp://kafkasvc:9092","-timeout=200s","-wait-retry-interval=40s","--","./start-app.sh"] + depends_on: + - "elasticsearch" + restart: always + + ui: + environment: + HAYSTACK_OVERRIDES_CONFIG_PATH: /data/connectors.json + HAYSTACK_PROP_CONNECTORS_TRACES_CONNECTOR__NAME: "haystack" + HAYSTACK_PROP_CONNECTORS_TRACES_SERVICE__REFRESH__INTERVAL__IN__SECS: "0" + HAYSTACK_PROP_CONNECTORS_TRACES_HAYSTACK__HOST: "trace-reader" + HAYSTACK_PROP_CONNECTORS_TRACES_HAYSTACK__PORT: "8088" + HAYSTACK_PROP_CONNECTORS_BLOBS_ENABLE__BLOBS: "true" + HAYSTACK_PROP_CONNECTORS_BLOBS_BLOBS__URL : "http://localhost:35002" diff --git a/docker/trends/docker-compose.yml b/docker/trends/docker-compose.yml new file mode 100644 index 000000000..bdf796fb3 --- /dev/null +++ b/docker/trends/docker-compose.yml @@ -0,0 +1,64 @@ +# +# Copyright 2018 Expedia, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +version: "3" +services: + metrictank: + image: grafana/metrictank:0.10.1 + environment: + MT_HTTP_MULTI_TENANT: "false" + MT_CARBON_IN_ENABLED: "false" + MT_KAFKA_MDM_IN_ENABLED: "true" + MT_CASSANDRA_ADDRS: "cassandra:9042" + MT_KAFKA_MDM_IN_BROKERS: "kafkasvc:9092" + MT_CASSANDRA_IDX_HOSTS: "cassandra:9042" + MT_STATS_ENABLED: "false" + MT_MEMORY_IDX_TAG_SUPPORT: "true" + depends_on: + - "kafkasvc" + restart: always + # uncomment below port mapping to expose and connect to this application out of local docker container network +# ports: +# - "6060:6060" + + trends-transformer: + image: expediadotcom/haystack-span-timeseries-transformer:1.1.3 + environment: + HAYSTACK_GRAPHITE_ENABLED: "false" + HAYSTACK_PROP_KAFKA_STREAMS_BOOTSTRAP_SERVERS: "kafkasvc:9092" + HAYSTACK_PROP_KAFKA_PRODUCER_TOPIC: "metric-data-points" + JAVA_XMS: 128m + entrypoint: ["/dockerize","-wait=tcp://kafkasvc:9092","-timeout=200s","-wait-retry-interval=40s","--","./start-app.sh"] + depends_on: + - "kafkasvc" + restart: always + + trends-aggregator: + image: expediadotcom/haystack-timeseries-aggregator:1.1.3 + environment: + HAYSTACK_GRAPHITE_ENABLED: "false" + HAYSTACK_PROP_KAFKA_STREAMS_BOOTSTRAP_SERVERS: "kafkasvc:9092" + HAYSTACK_PROP_KAFKA_CONSUMER_TOPIC: "metric-data-points" + JAVA_XMS: 128m + entrypoint: ["/dockerize","-wait=tcp://kafkasvc:9092","-timeout=200s","-wait-retry-interval=40s","--","./start-app.sh"] + depends_on: + - "kafkasvc" + restart: always + + ui: + environment: + HAYSTACK_OVERRIDES_CONFIG_PATH: /data/connectors.json + HAYSTACK_PROP_CONNECTORS_TRENDS_CONNECTOR__NAME: "haystack" + HAYSTACK_PROP_CONNECTORS_TRENDS_METRIC__TANK__URL: "http://metrictank:6060" diff --git a/docker/zipkin/docker-compose.yml b/docker/zipkin/docker-compose.yml new file mode 100644 index 000000000..bcda5a90a --- /dev/null +++ b/docker/zipkin/docker-compose.yml @@ -0,0 +1,48 @@ +# +# Copyright 2018 Expedia, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +version: "3" + +services: + pitchfork: + image: hotelsdotcom/pitchfork:latest + ports: + - "9411:9411" + environment: + PITCHFORK_FORWARDERS_LOGGING_ENABLED: "true" + PITCHFORK_FORWARDERS_LOGGING_LOG_FULL_SPAN: "true" + PITCHFORK_FORWARDERS_ZIPKIN_HTTP_ENABLED: "true" + PITCHFORK_FORWARDERS_ZIPKIN_HTTP_ENDPOINT: "http://zipkin:9411/api/v2/spans" + PITCHFORK_FORWARDERS_HAYSTACK_KAFKA_ENABLED: "true" + PITCHFORK_FORWARDERS_HAYSTACK_KAFKA_BOOTSTRAP_SERVERS: "kafkasvc:9092" + + zipkin: + image: openzipkin/zipkin + container_name: zipkin + environment: + STORAGE_TYPE: elasticsearch + ES_HOSTS: elasticsearch + KAFKA_BOOTSTRAP_SERVERS: kafkasvc:9092 + ports: + - "9412:9411" + depends_on: + - elasticsearch + - kafkasvc + + ui: + environment: + HAYSTACK_OVERRIDES_CONFIG_PATH: /data/connectors.json + HAYSTACK_PROP_CONNECTORS_TRACES_CONNECTOR__NAME: "zipkin" + HAYSTACK_PROP_CONNECTORS_TRACES_ZIPKIN__URL: 'http://zipkin:9411/api/v2' diff --git a/idl/.gitignore b/idl/.gitignore new file mode 100644 index 000000000..d82a0da01 --- /dev/null +++ b/idl/.gitignore @@ -0,0 +1,29 @@ +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.ear +*.zip +*.tar.gz +*.rar +.idea +*/.idea +*.iml +*/*.ipr +./build/* + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* +java/target/ +fakespans/fakespans diff --git a/idl/LICENSE b/idl/LICENSE new file mode 100644 index 000000000..9f133f5cd --- /dev/null +++ b/idl/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2017 Expedia, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/idl/README.md b/idl/README.md new file mode 100644 index 000000000..bcf5008d7 --- /dev/null +++ b/idl/README.md @@ -0,0 +1,12 @@ +# haystack-idl +Span and other data models used by Haystack are defined as [Protocol Buffer](https://developers.google.com/protocol-buffers/) files in [proto](./proto) folder + +## Generating Java source for Haystack Spans +A simple maven pom file is available in [java](./java) folder to compile Haystack proto files in to a jar + +## Creating test data in kafka +Simple utility in Go to generate and send sample Spans to Kakfa is in [fakespans](./fakespans) folder + +## Building fakespans +```docker run --rm -it -v "$PWD":/usr/src/app -w /usr/src/app golang:1.8 /usr/src/app/build.sh``` + diff --git a/idl/build.sh b/idl/build.sh new file mode 100755 index 000000000..99b2cdbee --- /dev/null +++ b/idl/build.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash + +mkdir -p build +cd fakespans +go get github.com/Shopify/sarama +go get github.com/codeskyblue/go-uuid +go get github.com/golang/protobuf/proto + + +for GOOS in darwin linux windows; do + for GOARCH in 386 amd64; do + echo "Building for ${GOOS} - ${GOARCH}" + export GOOS=${GOOS} + export GOARCH=${GOARCH} + go build -v -o ../build/fakespans-$GOOS-$GOARCH + done +done \ No newline at end of file diff --git a/idl/proto/agent/spanAgent.proto b/idl/proto/agent/spanAgent.proto new file mode 100644 index 000000000..edca7b03e --- /dev/null +++ b/idl/proto/agent/spanAgent.proto @@ -0,0 +1,39 @@ +/* + * + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +syntax = "proto3"; +import "span.proto"; + +option java_package = "com.expedia.open.tracing.agent.api"; +option java_multiple_files = true; +option go_package = "haystack"; + +message DispatchResult { + ResultCode code = 1; // result code is 0 for sucessful dipatch only + string error_message = 2; // error message if result code is non zero + + enum ResultCode { + SUCCESS = 0; + UNKNOWN_ERROR = 1; + RATE_LIMIT_ERROR = 2; + } +} + +// service interface to push spans to haystack agent +service SpanAgent { + rpc dispatch (Span) returns (DispatchResult); // dispatch span to haystack agent +} diff --git a/idl/proto/api/anomaly/anomalyReader.proto b/idl/proto/api/anomaly/anomalyReader.proto new file mode 100644 index 000000000..7255c3905 --- /dev/null +++ b/idl/proto/api/anomaly/anomalyReader.proto @@ -0,0 +1,52 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +syntax = "proto3"; + +option java_package = "com.expedia.open.tracing.api.anomaly"; +option java_multiple_files = true; +option go_package = "haystack"; + + +message SearchAnamoliesRequest { + map labels = 1; + int64 startTime = 2; + int64 endTime = 3; + int32 size = 4; +} + +message Anomaly { + double expectedValue = 1; + double observedValue = 2; + int64 timestamp = 3; +} + +message SearchAnamolyResponse { + string name = 1; + map labels = 2; + repeated Anomaly anomalies = 3; +} + +message SearchAnomaliesResponse { + repeated SearchAnamolyResponse searchAnomalyResponse = 1; +} + + +service AnomalyReader { + rpc getAnomalies(SearchAnamoliesRequest) returns (SearchAnomaliesResponse); // fetches the anomalies +} diff --git a/idl/proto/api/subscription/subscriptionManagement.proto b/idl/proto/api/subscription/subscriptionManagement.proto new file mode 100644 index 000000000..3a5a0a743 --- /dev/null +++ b/idl/proto/api/subscription/subscriptionManagement.proto @@ -0,0 +1,117 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +syntax = "proto3"; + +option java_package = "com.expedia.open.tracing.api.subscription"; +option java_multiple_files = true; +option go_package = "haystack"; + +message Empty { + +} + +enum DispatchType { + EMAIL = 0; + SLACK = 1; +} + +message Field { + string name = 1; // name of the field + string value = 2; // value of the field +} + +message Operand { + oneof operand { + Field field = 1; // leaf field value + ExpressionTree expression = 2; // a nested expression tree + } +} + +message ExpressionTree { + enum Operator { + AND = 0; + OR = 1; + } + + Operator operator = 1; // operator to use the subtree results + repeated Operand operands = 2; // list of operands +} + +message Dispatcher { + DispatchType type = 1; + string endpoint = 2; +} + +message SubscriptionRequest { + ExpressionTree expressionTree = 1; + repeated Dispatcher dispatchers = 2; +} + +message User { + string username = 1; +} + +message CreateSubscriptionRequest { + User user = 1; + SubscriptionRequest subscriptionRequest = 2; +} + +message CreateSubscriptionResponse { + string subscriptionId = 1; +} + +message SubscriptionResponse { + string subscriptionId = 1; + User user = 2; + repeated Dispatcher dispatchers = 3; + ExpressionTree expressionTree = 4; + int64 lastModifiedTime = 5; + int64 createdTime = 6; +} + +message SearchSubscriptionResponse { + repeated SubscriptionResponse subscriptionResponse = 1; +} + +message UpdateSubscriptionRequest { + string subscriptionId = 1; + SubscriptionRequest subscriptionRequest = 2; +} + +message DeleteSubscriptionRequest { + string subscriptionId = 1; +} + +message SearchSubscriptionRequest { + User user = 1; + map labels = 2; +} + +message GetSubscriptionRequest { + string subscriptionId = 1; +} + + +service SubscriptionManagement { + rpc createSubscription(CreateSubscriptionRequest) returns (CreateSubscriptionResponse); // create a new subscription. Returns a subscription Id + rpc updateSubscription(UpdateSubscriptionRequest) returns (Empty); // update a subscription. All updates would be idempotent. + rpc deleteSubscription(DeleteSubscriptionRequest) returns (Empty); // delete a subscription. + rpc getSubscription(GetSubscriptionRequest) returns (SubscriptionResponse); // Fetch a subscription given the id of the subscription. + rpc searchSubscription(SearchSubscriptionRequest) returns (SearchSubscriptionResponse); // search subscription given a set of labels. +} \ No newline at end of file diff --git a/idl/proto/api/traceReader.proto b/idl/proto/api/traceReader.proto new file mode 100644 index 000000000..9aba6af36 --- /dev/null +++ b/idl/proto/api/traceReader.proto @@ -0,0 +1,178 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +syntax = "proto3"; +import "span.proto"; + +option java_package = "com.expedia.open.tracing.api"; +option java_multiple_files = true; +option go_package = "haystack"; + +// collection of spans belonging to a single Trace +message Trace { + string traceId = 1; + repeated Span childSpans = 2; +} + +// request for fetching Trace for traceId +message TraceRequest { + string traceId = 1; +} + +// request for raw traces representing list of traceIds +message RawTracesRequest { + repeated string traceId = 1; +} + +// list of filtered traces +message RawTracesResult { + repeated Trace traces = 1; +} + +// request for fetching span for give traceId and spanId +message SpanRequest { + string traceId = 1; + string spanId = 2; +} + +message SpanResponse { + repeated Span spans = 1; // list of spans with a given traceId and spanId +} + +// a single operand in the expression tree +message Operand { + oneof operand { + Field field = 1; // leaf field value + ExpressionTree expression = 2; // a nested expression tree + } +} + +// nested n-ary expression tree for specifying expression to filter +// represents a binary operator which will be performed on a list of operands +message ExpressionTree { + enum Operator { + AND = 0; + OR = 1; + } + + Operator operator = 1; // operator to use the subtree results + repeated Operand operands = 2; // list of operands + bool isSpanLevelExpression = 3; // if this expression is a span level or trace level filter +} + +// criteria for searching traces +message TracesSearchRequest { + repeated Field fields = 1 [deprecated=true]; // fields to filter traces + int64 startTime = 2; // search window start time in microseconds time from epoch + int64 endTime = 3; // search window end time in microseconds time from epoch + int32 limit = 4; // limit on number of results to return + ExpressionTree filterExpression = 5; // expression tree for trace search filters +} + +// list of filtered traces +message TracesSearchResult { + repeated Trace traces = 1; +} + +// request for fetching trace count of search result per interval +message TraceCountsRequest { + repeated Field fields = 1 [deprecated=true]; // fields to filter traces + int64 startTime = 2; // search window start time in microseconds time from epoch + int64 endTime = 3; // search window end time in microseconds time from epoch + int64 interval = 4; // interval in microseconds + ExpressionTree filterExpression = 5; // expression tree for trace search filters +} + +// trace count list +message TraceCounts { + repeated TraceCount traceCount = 1; +} + +// count of traces for an interval +message TraceCount { + int64 timestamp = 1; // end time of trace search result in microseconds time from epoch + int64 count = 2; // count of traces +} + +// Field is a general abstraction on data associated with a span +// It can represent any indexed span attribute such as tag, log, spanName, or operationName +message Field { + enum Operator { // define the operator between name and its value + EQUAL = 0; + GREATER_THAN = 1; + LESS_THAN = 2; + NOT_EQUAL = 3; + } + + string name = 1; // name of the field + string value = 2; // value of the field + Operator operator = 3; // operation between name and value, default is EQUAL +} + +// An empty message type for rq/rs +message Empty {} + +// query for fetching values for given field +message FieldValuesRequest { + string fieldName = 1; // name of field to query for + repeated Field filters = 2; // provided fields to be used for filtering +} + +// whitelisted field metadata to accompany field name +message FieldMetadata { + bool isRangeQuery = 1; +} + +message FieldNames { + repeated string names = 1; + repeated FieldMetadata fieldMetadata = 2; +} + +message FieldValues { + repeated string values = 1; +} + +message CallNode { + string serviceName = 1; + string operationName = 2; + string infrastructureProvider = 3; // infrastructure provider hosting the service + string infrastructureLocation = 4; // infrastructure location hosting the service + string duration = 5; // duration of the call perceived by the service +} + +message Call { + CallNode from = 1; // service node from which call was started + CallNode to = 2; // service node to which call was terminated + int64 networkDelta = 3; // time delta in transit +} + +message TraceCallGraph { + repeated Call calls = 1; // list of service calls +} + +// service interface to search and get traces +service TraceReader { + rpc searchTraces (TracesSearchRequest) returns (TracesSearchResult); // search for traces based on filter fields and other criteria + rpc getTraceCounts (TraceCountsRequest) returns (TraceCounts); // fetch per interval count of traces search + rpc getTrace (TraceRequest) returns (Trace); // fetch a trace using traceId + rpc getRawTrace (TraceRequest) returns (Trace); // fetch a trace in raw un-transformed format using traceId + rpc getRawSpan (SpanRequest) returns (SpanResponse); // fetch a span of a trace in raw un-transformed format using traceId and spanId + rpc getFieldNames (Empty) returns (FieldNames); // get all searchable Fields available in haystack system + rpc getFieldValues (FieldValuesRequest) returns (FieldValues); // get values for a given Field + rpc getTraceCallGraph (TraceRequest) returns (TraceCallGraph); // get graph of service calls made in the given traceId + rpc getRawTraces (RawTracesRequest) returns (RawTracesResult); // get raw traces for given list of traceIds +} diff --git a/idl/proto/backend/storageBackend.proto b/idl/proto/backend/storageBackend.proto new file mode 100644 index 000000000..1bdd37a70 --- /dev/null +++ b/idl/proto/backend/storageBackend.proto @@ -0,0 +1,68 @@ +/* + * + * Copyright 2018 Expedia, Group. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +syntax = "proto3"; + +option java_package = "com.expedia.open.tracing.backend"; +option java_multiple_files = true; +option go_package = "haystack"; + + +message Field { + string name = 1; // name of the field + string value = 2; // value of the field +} +message Metadata { + repeated Field fields = 1; +} + +message TraceRecord { + string traceId = 1; + bytes spans = 2; // byte array value type + int64 timestamp = 4; + Metadata metadata = 3; +} + +// query for writing trace records to persistent store +message WriteSpansRequest { + repeated TraceRecord records = 1; +} + +// query for reading trace records from persistent store +message ReadSpansRequest { + repeated string traceIds = 1; // trace id of the request +} + +message ReadSpansResponse { + repeated TraceRecord records = 1; // collection of span buffers +} + +message WriteSpansResponse { + ResultCode code = 1; // result code is 0 for sucessful dipatch only + string error_message = 2; // error message if result code is non zero + + enum ResultCode { + SUCCESS = 0; + UNKNOWN_ERROR = 1; + } +} + +// service interface to write and read traces +service StorageBackend { + rpc writeSpans (WriteSpansRequest) returns (WriteSpansResponse); // write buffered spans to backend + rpc readSpans (ReadSpansRequest) returns (ReadSpansResponse); // read buffered spans from backend +} diff --git a/idl/proto/blobs/blob.proto b/idl/proto/blobs/blob.proto new file mode 100644 index 000000000..002d7490f --- /dev/null +++ b/idl/proto/blobs/blob.proto @@ -0,0 +1,30 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +syntax = "proto3"; + +option java_package = "com.expedia.blobs.model"; +option java_multiple_files = true; +option go_package = "blob"; + +// Blob represents the data thats needs to be saved for a specific service call. +message Blob { + + string key = 1; // unique key + map metadata = 2; + bytes content = 3; +} diff --git a/idl/proto/blobs/blobAgent.proto b/idl/proto/blobs/blobAgent.proto new file mode 100644 index 000000000..1469fd0f6 --- /dev/null +++ b/idl/proto/blobs/blobAgent.proto @@ -0,0 +1,61 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +syntax = "proto3"; +import "blob.proto"; + +option java_package = "com.expedia.haystack.agent.blobs.api"; +option java_multiple_files = true; +option go_package = "blob"; + +message DispatchResult { + ResultCode code = 1; // result code is 0 for sucessful dipatch only + string error_message = 2; // error message if result code is non zero + + enum ResultCode { + SUCCESS = 0; + UNKNOWN_ERROR = 1; + RATE_LIMIT_ERROR = 2; + MAX_SIZE_EXCEEDED_ERROR = 3; + } +} + +message BlobReadResponse { + Blob blob = 1; + ResultCode code = 2; + string error_message = 3; + + enum ResultCode { + SUCCESS = 0; + UNKNOWN_ERROR = 1; + } +} + +message FormattedBlobReadResponse { + string data = 1; +} + +message BlobSearch { + string key = 1; +} + +// service interface to push blobs to haystack agent +service BlobAgent { + rpc dispatch (Blob) returns (DispatchResult); // dispatch blob to haystack agent + rpc read (BlobSearch) returns (BlobReadResponse); + rpc readBlobAsString(BlobSearch) returns (FormattedBlobReadResponse); +} diff --git a/idl/proto/span.proto b/idl/proto/span.proto new file mode 100644 index 000000000..0f6b3941f --- /dev/null +++ b/idl/proto/span.proto @@ -0,0 +1,74 @@ +/* + * + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +syntax = "proto3"; + +option java_package = "com.expedia.open.tracing"; +option java_multiple_files = true; +option go_package = "haystack"; + +// Span represents a unit of work performed by a service. +message Span { + + string traceId = 1; // unique trace id + string spanId = 2; // unique span id + string parentSpanId = 3; // optional, a span can have its parent spanId + string serviceName = 4; // name of service + string operationName = 5; // name of operation + + int64 startTime = 6; // creation time of this span in microseconds since epoch + int64 duration = 7; // span duration in microseconds + + repeated Log logs = 8; // arbitrary set of timestamp-aware key-value pairs + repeated Tag tags = 9; // arbitrary set of key-value pairs +} + + +// Log is a timestamped event with a set of tags. +message Log { + int64 timestamp = 1; // timestamp in microseconds since epoch + repeated Tag fields = 2; +} + + +// Tag is a strongly typed key/value pair. We use 'oneof' protobuf attribute to represent the possible tagTypes +message Tag { + + // TagType denotes the type of a Tag's value. + enum TagType { + STRING = 0; + DOUBLE = 1; + BOOL = 2; + LONG = 3; + BINARY = 4; + } + string key = 1; // name of the tag key + TagType type = 2; // type of tag, namely string, double, bool, long and binary + oneof myvalue { + string vStr = 3; // string value type + int64 vLong = 4; // long value type + double vDouble = 5; // double value type + bool vBool = 6; // bool value type + bytes vBytes = 7; // byte array value type + } +} + + +// You can optionally use Batch to send a collection of spans. Spans may not necessarily belong to one traceId. +message Batch { + repeated Span spans = 1; // a collection of spans emitted from the process/service +} diff --git a/idl/proto/spanBuffer.proto b/idl/proto/spanBuffer.proto new file mode 100644 index 000000000..41d3632d6 --- /dev/null +++ b/idl/proto/spanBuffer.proto @@ -0,0 +1,29 @@ +/* + * + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +syntax = "proto3"; +import "span.proto"; + +option java_package = "com.expedia.open.tracing.buffer"; +option java_multiple_files = true; +option go_package = "haystack"; + +// This entity represents a collection of spans that belong to one traceId +message SpanBuffer { + string traceId = 1; // unique trace id + repeated Span childSpans = 2; // list of child spans +} diff --git a/service-graph/.gitignore b/service-graph/.gitignore new file mode 100644 index 000000000..137a11e38 --- /dev/null +++ b/service-graph/.gitignore @@ -0,0 +1,22 @@ +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next +release.properties +dependency-reduced-pom.xml +buildNumber.properties +.mvn/timing.properties + +# Avoid ignoring Maven wrapper jar file (.jar files are usually ignored) +!/.mvn/wrapper/maven-wrapper.jar + +#intellij +.idea/ +*.ipr +*.iws +*.iml + +#app +*/logs/ +*/local/ diff --git a/service-graph/CONTRIBUTING.md b/service-graph/CONTRIBUTING.md new file mode 100644 index 000000000..317757128 --- /dev/null +++ b/service-graph/CONTRIBUTING.md @@ -0,0 +1,14 @@ +##Bugs +We use Github Issues for our bug reporting. Please make sure the bug isn't already listed before opening a new issue. + +##Development +All work on Haystack happens directly on Github. Core Haystack team members will review opened pull requests. + +##Requests +If you see a feature that you would like to be added, please open an issue in the respective repository or in the general Haystack repo. + +##Contributing to Documentation +To contribute to documentation, you can directly modify the corresponding .md files in the docs directory under the base haystack repository, and submit a pull request. Once your PR is merged, the documentation is automatically built and deployed to https://expediadotcom.github.io/haystack. + +##License +By contributing to Haystack, you agree that your contributions will be licensed under its Apache License. \ No newline at end of file diff --git a/service-graph/LICENSE b/service-graph/LICENSE new file mode 100644 index 000000000..261eeb9e9 --- /dev/null +++ b/service-graph/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/service-graph/Makefile b/service-graph/Makefile new file mode 100644 index 000000000..afd0e07b8 --- /dev/null +++ b/service-graph/Makefile @@ -0,0 +1,32 @@ +.PHONY: all clean build report-coverage node-finder graph-builder snapshotter release + +PWD := $(shell pwd) + +clean: + mvn clean + +build: clean + mvn package + +node-finder: + mvn verify -DfinalName=haystack-service-graph-node-finder -pl node-finder -am + +graph-builder: + mvn verify -DfinalName=haystack-service-graph-graph-builder -pl graph-builder -am + +snapshotter: + mvn verify -DfinalName=haystack-service-graph-snapshotter -pl snapshotter -am + +all: clean node-finder graph-builder snapshotter + +# build all and release +release: clean node-finder graph-builder snapshotter + cd node-finder && $(MAKE) release + cd graph-builder && $(MAKE) release + cd snapshotter && $(MAKE) release + ./.travis/deploy.sh + +# run coverage tests +report-coverage: + mvn clean scoverage:test scoverage:report-only + open target/site/scoverage/index.html diff --git a/service-graph/README.md b/service-graph/README.md new file mode 100644 index 000000000..a36d03e95 --- /dev/null +++ b/service-graph/README.md @@ -0,0 +1,140 @@ +[![Build Status](https://travis-ci.org/ExpediaDotCom/haystack-service-graph.svg?branch=master)](https://travis-ci.org/ExpediaDotCom/haystack-service-graph) +[![License](https://img.shields.io/badge/license-Apache%20License%202.0-blue.svg)](https://github.com/ExpediaDotCom/haystack/blob/master/LICENSE) + +# Haystack-service-graph + +This repository has two components that focus on + +* Building a service dependency graph from incoming spans and +* Computing the network latency between the services that allows +[haystack-trends](https://github.com/ExpediaDotCom/haystack-traces) to produce latency trends between services. + +## Required Reading + +In order to understand Haystack, we recommend reading the details of the +[Haystack](https://expediadotcom.github.io/haystack) project. Haystack is written in +[Kafka streams](http://docs.confluent.io/current/streams/index.html) +and hence some prior knowledge of Iafka streams is helpful. + +## Component: node-finder + +This component discovers the relationships between services. Eventually those relationships will be expressed as a graph +in which the services are the nodes and the operations are the edges. Since client spans do not carry the name of the +service being called, and server spans do not carry the name of the service calling them, this component accumulates the +incoming spans and uses `span-id` to discover the dependent services and the operations between them. + +Discovered "span pairs" are then used to produce two different outputs + +1. A simple object that has + * the calling service name + * the called service name + * the operation name +2. A `MetricPoint` object with the `latency` between the service pair, discovered by examining timestamps in the spans. + +Like many other components of Haystack, this component is also a `Kafka streams` application. The picture below shows +the topology / architecture of this component. + + +---------------+ + | | + | proto-spans | + | | + +-------+-------+ + | + +---------V----------+ + | | + +----+ span-accumulator +----+ + | | | | + | +--------------------+ | + | | + +---------V---------+ +------------V------------+ + | | | | + | latency-producer | | nodes-n-edges-producer | + | | | | + +---------+---------+ +------------+------------+ + | | + +--------V--------+ +---------V---------+ + | | | | + | metric-sink | | graph-nodes-sink | + | | | | + +-----------------+ +-------------------+ + +The Starting point for the application is the +[Streams](node-finder/src/main/scala/com/expedia/www/haystack/service/graph/node/finder/app/Streams.scala) class, which +builds the topology shown in the picture above. This `node-finder` topology consists of one source, three processors +and two sinks. + +* Source: The topology contains a source called `proto-spans`. This source reads a Kafka topic with the same name. +It uses `SpanDeserializer` as the value deserializer to read incoming spans in the topic. + +* Processors: + * span-accumulator : This processor accumulates all the incoming spans in a PriorityQueue ordered by each Span's + timestamp to maintain the incoming order. Periodically, it traverses the priority queue to find spans with matching + span-ids and combines them to form a client-server span pair. These span pairs are then forwarded to the downstream + processors. Accumulation time is configurable with a configuration keyed by `accumulator.interval`. It has a minor + optimization built in during queue traversal to match recently arrived spans with spans in the next batch. + * latency-producer : The latency producer is one of the processors downstream of span-accumulator. This simple + processor produces a `MetricPoint` instance to record the network latency in the current span pair. + A sample JSON representation of the metric point will look like + ```json + { + "metric" : "latency", + "type" : "gauge", + "value" : 40.0, + "epochTimeInSeconds" : 1523637898, + "tags" : { + "serviceName" : "foo-service", + "operationName" : "bar-operation" + } + } + ``` + * nodes-n-edges-producer: This processor is another simple processor that is downstream of span-accumulator. + For every span pair received, this processor emits a simple JSON representation of a graph edge as shown below + + ```json + { + "source" : "foo-service", + "destination" : "baz-service", + "operation" : "bar-operation" + } + ``` +* Sinks: + * metric-sink: This sink is downstream of latency-producer. It serializes each MetricPoint instance with a + `Message Pack` serializer and writes the serialized output to a configured Kafka topic. + * graph-nodes-sink: This sink is downstream of nodes-n-edges-producer. It serializes the JSON as a string and writes + that string to a configured Kafka topic for the `graph-builder` component to consume and build a service dependency + graph. + +## Component: graph-builder + +This component takes graph edges emitted by `node-finder` and merges them together to form the full service-graph. +It also has an http endpoint to return the accumulated service-graph. + +#### Streaming +`graph-builder` accumulates incoming edges in +[ktable](https://kafka.apache.org/0102/javadoc/org/apache/kafka/streams/kstream/KTable.html), using the stream +[table duality concept](https://docs.confluent.io/current/streams/concepts.html#duality-of-streams-and-tables). +Each row in the ktable represets one graph edge. Each edge is supplemented with some stats such as running count and +last seen timestamp. + +Kafka does take care of persisting and replicating the graph ktable across brokers to have fault tolerance. + +#### HTTP API +`graph-builder` also acts as an http api to query the graph ktable, using servlets over embedded jetty for implementing +the endpoints. +[Kafka interactive query](https://kafka.apache.org/10/documentation/streams/developer-guide/interactive-queries.html) +is used for fetching service graphs from local. + +An interactive query to a single stream nodes return only the graph-edges sharded to that node, hence it is a partial +view of the world. The servlet take care of fetching partial graphs from all nodes having the ktable to form full +service graphs. + +######endpoints +1. `/servicegraph` : returns full service graph, includes edges from all know services. Edges include operations also. + +## Building + +To build the components in this repository at once, one can run +``` +make all +``` +To build the components separately, once can check the README in the individual component folders. \ No newline at end of file diff --git a/service-graph/Release.md b/service-graph/Release.md new file mode 100644 index 000000000..03675aa41 --- /dev/null +++ b/service-graph/Release.md @@ -0,0 +1,10 @@ +#Releasing +Currently we publish the repo to docker hub and nexus central repository. + +#How to release and publish + +* Git tagging: + +```git tag -a 1.x.x -m "Release description..."``` + +Or you can also tag using UI: https://github.com/ExpediaDotCom/haystack-service-graph/releases \ No newline at end of file diff --git a/service-graph/ReleaseNotes.md b/service-graph/ReleaseNotes.md new file mode 100644 index 000000000..66c1850a2 --- /dev/null +++ b/service-graph/ReleaseNotes.md @@ -0,0 +1,31 @@ +# Release Notes + +## 2019-01-29 1.0.15 + * Make S3 item name use / instead of _, to take advantage of S3 "folders" + +## 2019-01-29 1.0.14 + * Handle command line args properly in the S3 store + +## 2019-01-28 1.0.13 + * Fix Docker image name for snapshotter (was haystack-service-snapshotter, is now haystack-service-graph-snapshotter) + +## 2019-01-25 1.0.12 + * Fix typo in Docker image name for snapshotter + +## 2019-01-25 1.0.11 + * Publish snapshotter to Docker + +## 2019-01-23 1.0.10 + * Names of S3 service graph snapshot items should terminate in ".csv" + +## 2019-01-23 1.0.9 + * Make the parameter for listObjectsBatchSize in S3SnapshotStore optional, as it's only needed when calling write + +## 2019-01-23 1.0.8 + * Remove Main companion object (it wasn't really needed) + * Allow URL to be specified as a parameter instead of being hard coded + * More unit tests + +## 2019-01-23 1.0.7 + * Add Main companion class to Main object so that it can be instantiated by the Java JVM + * Add this ReleaseNotes.md file diff --git a/service-graph/checkstyles/scalastyle_config.xml b/service-graph/checkstyles/scalastyle_config.xml new file mode 100644 index 000000000..d364af665 --- /dev/null +++ b/service-graph/checkstyles/scalastyle_config.xml @@ -0,0 +1,134 @@ + + Scalastyle standard configuration + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/service-graph/deployment/scripts/publish-to-docker-hub.sh b/service-graph/deployment/scripts/publish-to-docker-hub.sh new file mode 100755 index 000000000..f075c6160 --- /dev/null +++ b/service-graph/deployment/scripts/publish-to-docker-hub.sh @@ -0,0 +1,43 @@ +#!/bin/bash + +set -e + +QUALIFIED_DOCKER_IMAGE_NAME=$DOCKER_ORG/$DOCKER_IMAGE_NAME +echo "DOCKER_ORG=$DOCKER_ORG, DOCKER_IMAGE_NAME=$DOCKER_IMAGE_NAME, QUALIFIED_DOCKER_IMAGE_NAME=$QUALIFIED_DOCKER_IMAGE_NAME" +echo "BRANCH=$BRANCH, TAG=$TAG, SHA=$SHA" + +# login +docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD + +# Add tags +if [[ $TAG =~ ([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then + echo "releasing semantic versions" + + unset MAJOR MINOR PATCH + MAJOR="${BASH_REMATCH[1]}" + MINOR="${BASH_REMATCH[2]}" + PATCH="${BASH_REMATCH[3]}" + + # for tag, add MAJOR, MAJOR.MINOR, MAJOR.MINOR.PATCH and latest as tag + # publish image with tags + docker tag $DOCKER_IMAGE_NAME $QUALIFIED_DOCKER_IMAGE_NAME:$MAJOR + docker push $QUALIFIED_DOCKER_IMAGE_NAME:$MAJOR + + docker tag $DOCKER_IMAGE_NAME $QUALIFIED_DOCKER_IMAGE_NAME:$MAJOR.$MINOR + docker push $QUALIFIED_DOCKER_IMAGE_NAME:$MAJOR.$MINOR + + docker tag $DOCKER_IMAGE_NAME $QUALIFIED_DOCKER_IMAGE_NAME:$MAJOR.$MINOR.$PATCH + docker push $QUALIFIED_DOCKER_IMAGE_NAME:$MAJOR.$MINOR.$PATCH + + docker tag $DOCKER_IMAGE_NAME $QUALIFIED_DOCKER_IMAGE_NAME:latest + docker push $QUALIFIED_DOCKER_IMAGE_NAME:latest + +elif [[ "$BRANCH" == "master" ]]; then + echo "releasing master branch" + + # for 'master' branch, add SHA as tags + docker tag $DOCKER_IMAGE_NAME $QUALIFIED_DOCKER_IMAGE_NAME:$SHA + + # publish image with tags + docker push $QUALIFIED_DOCKER_IMAGE_NAME +fi diff --git a/service-graph/deployment/terraform/graph-builder/main.tf b/service-graph/deployment/terraform/graph-builder/main.tf new file mode 100644 index 000000000..898c29c49 --- /dev/null +++ b/service-graph/deployment/terraform/graph-builder/main.tf @@ -0,0 +1,70 @@ +locals { + app_name = "graph-builder" + config_file_path = "${path.module}/templates/graph-builder_conf.tpl" + deployment_yaml_file_path = "${path.module}/templates/deployment_yaml.tpl" + count = "${var.enabled?1:0}" + checksum = "${sha1("${data.template_file.config_data.rendered}")}" + configmap_name = "graph-builder-${local.checksum}" +} + +resource "kubernetes_config_map" "haystack-config" { + metadata { + name = "${local.configmap_name}" + namespace = "${var.namespace}" + } + data { + "graph-builder.conf" = "${data.template_file.config_data.rendered}" + } + count = "${local.count}" +} + +data "template_file" "config_data" { + template = "${file("${local.config_file_path}")}" + + vars { + kafka_endpoint = "${var.kafka_endpoint}" + } +} + +data "template_file" "deployment_yaml" { + template = "${file("${local.deployment_yaml_file_path}")}" + vars { + app_name = "${local.app_name}" + namespace = "${var.namespace}" + graphite_port = "${var.graphite_port}" + graphite_host = "${var.graphite_hostname}" + graphite_enabled = "${var.graphite_enabled}" + node_selecter_label = "${var.node_selecter_label}" + image = "${var.image}" + replicas = "${var.replicas}" + memory_limit = "${var.memory_limit}" + memory_request = "${var.memory_request}" + jvm_memory_limit = "${var.jvm_memory_limit}" + cpu_limit = "${var.cpu_limit}" + cpu_request = "${var.cpu_request}" + configmap_name = "${local.configmap_name}" + env_vars= "${indent(9,"${var.env_vars}")}" + service_port = "${var.service_port}" + container_port = "${var.container_port}" + } +} + +resource "null_resource" "kubectl_apply" { + triggers { + template = "${data.template_file.deployment_yaml.rendered}" + } + provisioner "local-exec" { + command = "echo '${data.template_file.deployment_yaml.rendered}' | ${var.kubectl_executable_name} apply -f - --context ${var.kubectl_context_name}" + } + count = "${local.count}" +} + + +resource "null_resource" "kubectl_destroy" { + + provisioner "local-exec" { + command = "echo '${data.template_file.deployment_yaml.rendered}' | ${var.kubectl_executable_name} delete -f - --context ${var.kubectl_context_name}" + when = "destroy" + } + count = "${local.count}" +} diff --git a/service-graph/deployment/terraform/graph-builder/outputs.tf b/service-graph/deployment/terraform/graph-builder/outputs.tf new file mode 100644 index 000000000..562aba81f --- /dev/null +++ b/service-graph/deployment/terraform/graph-builder/outputs.tf @@ -0,0 +1,7 @@ +output "hostname" { + value = "${local.app_name}" +} + +output "service_port" { + value = "${var.service_port}" +} \ No newline at end of file diff --git a/service-graph/deployment/terraform/graph-builder/templates/deployment_yaml.tpl b/service-graph/deployment/terraform/graph-builder/templates/deployment_yaml.tpl new file mode 100644 index 000000000..38b5fb909 --- /dev/null +++ b/service-graph/deployment/terraform/graph-builder/templates/deployment_yaml.tpl @@ -0,0 +1,84 @@ +# ------------------- Deployment ------------------- # + +kind: Deployment +apiVersion: apps/v1beta2 +metadata: + labels: + k8s-app: ${app_name} + name: ${app_name} + namespace: ${namespace} +spec: + replicas: ${replicas} + revisionHistoryLimit: 10 + selector: + matchLabels: + k8s-app: ${app_name} + template: + metadata: + labels: + k8s-app: ${app_name} + spec: + containers: + - name: ${app_name} + image: ${image} + volumeMounts: + # Create on-disk volume to store exec logs + - mountPath: /config + name: config-volume + resources: + limits: + cpu: ${cpu_limit} + memory: ${memory_limit}Mi + requests: + cpu: ${cpu_request} + memory: ${memory_request}Mi + env: + - name: "HAYSTACK_OVERRIDES_CONFIG_PATH" + value: "/config/graph-builder.conf" + - name: "HAYSTACK_GRAPHITE_HOST" + value: "${graphite_host}" + - name: "HAYSTACK_GRAPHITE_PORT" + value: "${graphite_port}" + - name: "HAYSTACK_GRAPHITE_ENABLED" + value: "${graphite_enabled}" + - name: "HAYSTACK_PROP_SERVICE_HOST" + valueFrom: + fieldRef: + fieldPath: status.podIP + - name: "JAVA_XMS" + value: "${jvm_memory_limit}m" + - name: "JAVA_XMX" + value: "${jvm_memory_limit}m" + ${env_vars} + livenessProbe: + exec: + command: + - grep + - "true" + - /app/isHealthy + initialDelaySeconds: 30 + periodSeconds: 5 + failureThreshold: 2 + nodeSelector: + ${node_selecter_label} + volumes: + - name: config-volume + configMap: + name: ${configmap_name} + + +# ------------------- Service ------------------- # +--- +apiVersion: v1 +kind: Service +metadata: + labels: + k8s-app: ${app_name} + name: ${app_name} + namespace: ${namespace} +spec: + ports: + - port: ${service_port} + targetPort: ${container_port} + selector: + k8s-app: ${app_name} diff --git a/service-graph/deployment/terraform/graph-builder/templates/graph-builder_conf.tpl b/service-graph/deployment/terraform/graph-builder/templates/graph-builder_conf.tpl new file mode 100644 index 000000000..2e3ce7a01 --- /dev/null +++ b/service-graph/deployment/terraform/graph-builder/templates/graph-builder_conf.tpl @@ -0,0 +1,47 @@ +health.status.path = "/app/isHealthy" + +kafka { + close.timeout.ms = 30000 + + streams { + application.id = "haystack-service-graph-graph-builder" + bootstrap.servers = "${kafka_endpoint}" + num.stream.threads = 4 + request.timeout.ms = 60000 + commit.interval.ms = 3000 + auto.offset.reset = latest + timestamp.extractor = "org.apache.kafka.streams.processor.WallclockTimestampExtractor" + replication.factor = 1 + } + + consumer { + topic = "graph-nodes" + } + + producer { + topic = "service-graph" + } + + aggregate { + window.sec = 1800 + retention.days = 1 + } +} + +service { + threads { + min = 1 + max = 5 + idle.timeout = 12000 + } + + http { + port = 8080 + idle.timeout = 12000 + } + + client { + connection.timeout = 10000 + socket.timeout = 10000 + } +} diff --git a/service-graph/deployment/terraform/graph-builder/variables.tf b/service-graph/deployment/terraform/graph-builder/variables.tf new file mode 100644 index 000000000..1de153833 --- /dev/null +++ b/service-graph/deployment/terraform/graph-builder/variables.tf @@ -0,0 +1,27 @@ +variable "image" {} +variable "replicas" {} +variable "namespace" {} +variable "kafka_endpoint" {} +variable "graphite_hostname" {} +variable "graphite_port" {} +variable "graphite_enabled" {} +variable "enabled"{} +variable "kubectl_executable_name" {} +variable "kubectl_context_name" {} +variable "node_selecter_label"{} +variable "memory_request"{} +variable "memory_limit"{} +variable "jvm_memory_limit"{} +variable "cpu_request"{} +variable "cpu_limit"{} +variable "env_vars" {} +variable "termination_grace_period" { + default = 30 +} + +variable "service_port" { + default = 8080 +} +variable "container_port" { + default = 8080 +} diff --git a/service-graph/deployment/terraform/main.tf b/service-graph/deployment/terraform/main.tf new file mode 100644 index 000000000..63de8e9c2 --- /dev/null +++ b/service-graph/deployment/terraform/main.tf @@ -0,0 +1,64 @@ +module "node-finder" { + source = "node-finder" + image = "expediadotcom/haystack-service-graph-node-finder:${var.service-graph["version"]}" + replicas = "${var.service-graph["node_finder_instances"]}" + namespace = "${var.namespace}" + kafka_endpoint = "${var.kafka_hostname}:${var.kafka_port}" + + graphite_hostname = "${var.graphite_hostname}" + graphite_port = "${var.graphite_port}" + graphite_enabled = "${var.graphite_enabled}" + node_selecter_label = "${var.node_selector_label}" + enabled = "${var.service-graph["enabled"]}" + kubectl_executable_name = "${var.kubectl_executable_name}" + kubectl_context_name = "${var.kubectl_context_name}" + cpu_limit = "${var.service-graph["node_finder_cpu_limit"]}" + cpu_request = "${var.service-graph["node_finder_cpu_request"]}" + memory_limit = "${var.service-graph["node_finder_memory_limit"]}" + memory_request = "${var.service-graph["node_finder_memory_request"]}" + jvm_memory_limit = "${var.service-graph["node_finder_jvm_memory_limit"]}" + env_vars = "${var.service-graph["node_finder_environment_overrides"]}" + metricpoint_encoder_type = "${var.service-graph["metricpoint_encoder_type"]}" + collect_tags = "${var.service-graph["collect_tags"]}" +} + +module "graph-builder" { + source = "graph-builder" + image = "expediadotcom/haystack-service-graph-graph-builder:${var.service-graph["version"]}" + replicas = "${var.service-graph["graph_builder_instances"]}" + namespace = "${var.namespace}" + kafka_endpoint = "${var.kafka_hostname}:${var.kafka_port}" + graphite_hostname = "${var.graphite_hostname}" + graphite_port = "${var.graphite_port}" + graphite_enabled = "${var.graphite_enabled}" + node_selecter_label = "${var.node_selector_label}" + enabled = "${var.service-graph["enabled"]}" + kubectl_executable_name = "${var.kubectl_executable_name}" + kubectl_context_name = "${var.kubectl_context_name}" + cpu_limit = "${var.service-graph["graph_builder_cpu_limit"]}" + cpu_request = "${var.service-graph["graph_builder_cpu_request"]}" + memory_limit = "${var.service-graph["graph_builder_memory_limit"]}" + memory_request = "${var.service-graph["graph_builder_memory_request"]}" + jvm_memory_limit = "${var.service-graph["graph_builder_jvm_memory_limit"]}" + env_vars = "${var.service-graph["graph_builder_environment_overrides"]}" +} +/* +module "snapshotter" { + source = "snapshotter" + image = "expediadotcom/haystack-service-graph-snapshotter:${var.service-graph["version"]}" + namespace = "${var.namespace}" + graphite_hostname = "${var.graphite_hostname}" + graphite_port = "${var.graphite_port}" + enabled = "${var.service-graph["enabled"]}" + kubectl_executable_name = "${var.kubectl_executable_name}" + kubectl_context_name = "${var.kubectl_context_name}" + cpu_limit = "${var.service-graph["snapshotter_cpu_limit"]}" + snapshotter_purge_age_ms = "${var.service-graph["snapshotter_purge_age_ms"]}" + cpu_request = "${var.service-graph["snapshotter_cpu_request"]}" + memory_limit = "${var.service-graph["snapshotter_memory_limit"]}" + memory_request = "${var.service-graph["snapshotter_memory_request"]}" + jvm_memory_limit = "${var.service-graph["snapshotter_jvm_memory_limit"]}" + env_vars = "${var.service-graph["snapshotter_environment_overrides"]}" + main_args = "${var.service-graph["main_args"]}" +} +*/ diff --git a/service-graph/deployment/terraform/node-finder/main.tf b/service-graph/deployment/terraform/node-finder/main.tf new file mode 100644 index 000000000..c74e247f3 --- /dev/null +++ b/service-graph/deployment/terraform/node-finder/main.tf @@ -0,0 +1,70 @@ +locals { + app_name = "node-finder" + config_file_path = "${path.module}/templates/node-finder_conf.tpl" + deployment_yaml_file_path = "${path.module}/templates/deployment_yaml.tpl" + count = "${var.enabled?1:0}" + checksum = "${sha1("${data.template_file.config_data.rendered}")}" + configmap_name = "node-finder-${local.checksum}" +} + +resource "kubernetes_config_map" "haystack-config" { + metadata { + name = "${local.configmap_name}" + namespace = "${var.namespace}" + } + data { + "node-finder.conf" = "${data.template_file.config_data.rendered}" + } + count = "${local.count}" +} + +data "template_file" "config_data" { + template = "${file("${local.config_file_path}")}" + + vars { + kafka_endpoint = "${var.kafka_endpoint}" + metricpoint_encoder_type = "${var.metricpoint_encoder_type}" + collect_tags = "${var.collect_tags}" + } +} + +data "template_file" "deployment_yaml" { + template = "${file("${local.deployment_yaml_file_path}")}" + vars { + app_name = "${local.app_name}" + namespace = "${var.namespace}" + graphite_port = "${var.graphite_port}" + graphite_host = "${var.graphite_hostname}" + graphite_enabled = "${var.graphite_enabled}" + node_selecter_label = "${var.node_selecter_label}" + image = "${var.image}" + replicas = "${var.replicas}" + memory_limit = "${var.memory_limit}" + memory_request = "${var.memory_request}" + jvm_memory_limit = "${var.jvm_memory_limit}" + cpu_limit = "${var.cpu_limit}" + cpu_request = "${var.cpu_request}" + configmap_name = "${local.configmap_name}" + env_vars= "${indent(9,"${var.env_vars}")}" + } +} + +resource "null_resource" "kubectl_apply" { + triggers { + template = "${data.template_file.deployment_yaml.rendered}" + } + provisioner "local-exec" { + command = "echo '${data.template_file.deployment_yaml.rendered}' | ${var.kubectl_executable_name} apply -f - --context ${var.kubectl_context_name}" + } + count = "${local.count}" +} + + +resource "null_resource" "kubectl_destroy" { + + provisioner "local-exec" { + command = "echo '${data.template_file.deployment_yaml.rendered}' | ${var.kubectl_executable_name} delete -f - --context ${var.kubectl_context_name}" + when = "destroy" + } + count = "${local.count}" +} diff --git a/service-graph/deployment/terraform/node-finder/outputs.tf b/service-graph/deployment/terraform/node-finder/outputs.tf new file mode 100644 index 000000000..e69de29bb diff --git a/service-graph/deployment/terraform/node-finder/templates/deployment_yaml.tpl b/service-graph/deployment/terraform/node-finder/templates/deployment_yaml.tpl new file mode 100644 index 000000000..bf9d8ff30 --- /dev/null +++ b/service-graph/deployment/terraform/node-finder/templates/deployment_yaml.tpl @@ -0,0 +1,64 @@ +# ------------------- Deployment ------------------- # + +kind: Deployment +apiVersion: apps/v1beta2 +metadata: + labels: + k8s-app: ${app_name} + name: ${app_name} + namespace: ${namespace} +spec: + replicas: ${replicas} + revisionHistoryLimit: 10 + selector: + matchLabels: + k8s-app: ${app_name} + template: + metadata: + labels: + k8s-app: ${app_name} + spec: + containers: + - name: ${app_name} + image: ${image} + volumeMounts: + # Create on-disk volume to store exec logs + - mountPath: /config + name: config-volume + resources: + limits: + cpu: ${cpu_limit} + memory: ${memory_limit}Mi + requests: + cpu: ${cpu_request} + memory: ${memory_request}Mi + env: + - name: "HAYSTACK_OVERRIDES_CONFIG_PATH" + value: "/config/node-finder.conf" + - name: "HAYSTACK_GRAPHITE_HOST" + value: "${graphite_host}" + - name: "HAYSTACK_GRAPHITE_ENABLED" + value: "${graphite_enabled}" + - name: "HAYSTACK_GRAPHITE_PORT" + value: "${graphite_port}" + - name: "JAVA_XMS" + value: "${jvm_memory_limit}m" + - name: "JAVA_XMX" + value: "${jvm_memory_limit}m" + ${env_vars} + livenessProbe: + exec: + command: + - grep + - "true" + - /app/isHealthy + initialDelaySeconds: 30 + periodSeconds: 5 + failureThreshold: 2 + nodeSelector: + ${node_selecter_label} + volumes: + - name: config-volume + configMap: + name: ${configmap_name} + diff --git a/service-graph/deployment/terraform/node-finder/templates/node-finder_conf.tpl b/service-graph/deployment/terraform/node-finder/templates/node-finder_conf.tpl new file mode 100644 index 000000000..9a09cc1e5 --- /dev/null +++ b/service-graph/deployment/terraform/node-finder/templates/node-finder_conf.tpl @@ -0,0 +1,51 @@ +health.status.path = "/app/isHealthy" + +kafka { + close.timeout.ms = 30000 + + streams { + application.id = "haystack-service-graph-node-finder" + bootstrap.servers = "${kafka_endpoint}" + num.stream.threads = 4 + request.timeout.ms = 60000 + timestamp.extractor = "com.expedia.www.haystack.commons.kstreams.SpanTimestampExtractor" + commit.interval.ms = 3000 + auto.offset.reset = latest + } + + producer { + metrics { + topic = "metric-data-points" + // there are three types of encoders that are used on service and operation names: + // 1) periodreplacement: replaces all periods with 3 underscores + // 2) base64: base64 encodes the full name with a padding of _ + // 3) noop: does not perform any encoding + key.encoder = "${metricpoint_encoder_type}" + + } + service.call { + topic = "graph-nodes" + } + } + + consumer { + topic = "proto-spans" + } + + accumulator { + interval = 2500 + } + // collector tags allow service graph to collect tags from spans and have them available when querying service + // graph. Example: you can collect the tags service tier and infraprovider tags using value "[tier,infraprovider]" + collectorTags = ${collect_tags} + + node.metadata { + topic { + autocreate = true + name = "haystack-node-finder-metadata" + partition.count = 6 + replication.factor = 2 + } + } +} + diff --git a/service-graph/deployment/terraform/node-finder/variables.tf b/service-graph/deployment/terraform/node-finder/variables.tf new file mode 100644 index 000000000..5b45ecd8c --- /dev/null +++ b/service-graph/deployment/terraform/node-finder/variables.tf @@ -0,0 +1,24 @@ +variable "image" {} +variable "replicas" {} +variable "namespace" {} +variable "kafka_endpoint" {} +variable "metricpoint_encoder_type" {} +variable "graphite_hostname" {} +variable "graphite_port" {} +variable "graphite_enabled" {} +variable "enabled"{} +variable "kubectl_executable_name" {} +variable "kubectl_context_name" {} +variable "node_selecter_label"{} +variable "memory_request"{} +variable "memory_limit"{} +variable "jvm_memory_limit"{} +variable "cpu_request"{} +variable "cpu_limit"{} +variable "env_vars" {} +variable "termination_grace_period" { + default = 30 +} +variable "collect_tags" { + default = "[]" +} diff --git a/service-graph/deployment/terraform/outputs.tf b/service-graph/deployment/terraform/outputs.tf new file mode 100644 index 000000000..f7c451c2f --- /dev/null +++ b/service-graph/deployment/terraform/outputs.tf @@ -0,0 +1,7 @@ +output "graph_builder_hostname" { + value = "${module.graph-builder.hostname}" +} + +output "graph_builder_port" { + value = "${module.graph-builder.service_port}" +} \ No newline at end of file diff --git a/service-graph/deployment/terraform/snapshotter/templates/deployment_yaml.tpl b/service-graph/deployment/terraform/snapshotter/templates/deployment_yaml.tpl new file mode 100644 index 000000000..8d1dd19ae --- /dev/null +++ b/service-graph/deployment/terraform/snapshotter/templates/deployment_yaml.tpl @@ -0,0 +1,50 @@ +# ------------------- Deployment ------------------- # + +kind: Deployment +apiVersion: batch/v1beta2 +metadata: + labels: + k8s-app: ${app_name} + name: ${app_name} + namespace: ${namespace} +spec: + replicas: 1 + revisionHistoryLimit: 10 + selector: + matchLabels: + k8s-app: ${app_name} + template: + metadata: + labels: + k8s-app: ${app_name} + spec: + containers: + - name: ${app_name} + image: ${image} + volumeMounts: + # Create on-disk volume to store exec logs + - mountPath: /config + name: config-volume + resources: + limits: + cpu: ${cpu_limit} + memory: ${memory_limit}Mi + requests: + cpu: ${cpu_request} + memory: ${memory_request}Mi + env: + - name: "HAYSTACK_GRAPHITE_HOST" + value: "${graphite_host}" + - name: "HAYSTACK_GRAPHITE_PORT" + value: "${graphite_port}" + - name: "JAVA_XMS" + value: "${jvm_memory_limit}m" + - name: "JAVA_XMX" + value: "${jvm_memory_limit}m" + ${env_vars} + nodeSelector: + ${node_selecter_label} + volumes: + - name: config-volume + configMap: + name: ${configmap_name} \ No newline at end of file diff --git a/service-graph/deployment/terraform/snapshotter/templates/snapshotter_conf.tpl b/service-graph/deployment/terraform/snapshotter/templates/snapshotter_conf.tpl new file mode 100644 index 000000000..dd9e3c620 --- /dev/null +++ b/service-graph/deployment/terraform/snapshotter/templates/snapshotter_conf.tpl @@ -0,0 +1,3 @@ +snapshotter { + purge.age.ms = ${snapshotter_purge_age_ms} +} \ No newline at end of file diff --git a/service-graph/deployment/terraform/variables.tf b/service-graph/deployment/terraform/variables.tf new file mode 100644 index 000000000..66cb9df87 --- /dev/null +++ b/service-graph/deployment/terraform/variables.tf @@ -0,0 +1,14 @@ +variable "kafka_hostname" {} +variable "kafka_port" {} +variable "graphite_hostname" {} +variable "graphite_port" {} +variable "graphite_enabled" {} +variable "kubectl_context_name" {} +variable "kubectl_executable_name" {} +variable "namespace" {} +variable "node_selector_label"{} + +# service-graph config +variable "service-graph" { + type = "map" +} diff --git a/service-graph/graph-builder/Makefile b/service-graph/graph-builder/Makefile new file mode 100644 index 000000000..3d1deb4df --- /dev/null +++ b/service-graph/graph-builder/Makefile @@ -0,0 +1,11 @@ +.PHONY: integration_test release + +export DOCKER_ORG := expediadotcom +export DOCKER_IMAGE_NAME := haystack-service-graph-graph-builder +PWD := $(shell pwd) + +docker-image: + docker build -t $(DOCKER_IMAGE_NAME) -f build/docker/Dockerfile . + +release: docker-image + ../deployment/scripts/publish-to-docker-hub.sh diff --git a/service-graph/graph-builder/README.md b/service-graph/graph-builder/README.md new file mode 100644 index 000000000..c40bbaa25 --- /dev/null +++ b/service-graph/graph-builder/README.md @@ -0,0 +1,65 @@ +#Haystack : node-finder + +Information on what this component is all about is documented in the [README](../README.md) of the repository + +## Building + +``` +mvn clean verify +``` + +or + +``` +make docker-image +``` + +## Testing Locally + +* Download Kafka 0.11.0.x +* Start Zookeeper locally (from kafka home) +``` +bin/zookeeper-server-start.sh config/zookeeper.properties +``` +* Start Kafka locally (from kafka home) +``` +bin/kafka-server-start.sh config/server.properties +``` +* Create proto-spans topic (from kafka home) +``` +bin/kafka-topics.sh --create --zookeeper localhost:2181 --replication-factor 1 --partitions 1 --topic proto-spans +``` +* Create a local.conf override file +``` +cat local.conf + +health.status.path = "logs/isHealthy" + +kafka { + streams { + bootstrap.servers = "localhost:9092" + } +} +``` +* Build graph-builder application locally (graph-builder app root) +``` +mvn clean package +``` +* Start the node-finder (node-finder app root) +``` +export HAYSTACK_OVERRIDES_CONFIG_PATH=/local.conf +java -jar target/haystack-service-graph-node-finder.jar +``` + +* Start application (graph-builder app root) +``` +java -jar target/haystack-service-graph-graph-builder.jar +``` +* Send data to Kafka (refer to fakespans tool README) +``` +$GOBIN/fakespans --from-file fakespans.json --kafka-broker localhost:9092 +``` +* Check the output topics (from kafka home) +``` +curl http://localhost:8080/servicegraph +``` diff --git a/service-graph/graph-builder/build/docker/Dockerfile b/service-graph/graph-builder/build/docker/Dockerfile new file mode 100644 index 000000000..f2aac8f71 --- /dev/null +++ b/service-graph/graph-builder/build/docker/Dockerfile @@ -0,0 +1,25 @@ +FROM openjdk:8-jre +MAINTAINER Haystack + +ENV APP_NAME haystack-service-graph-graph-builder +ENV APP_HOME /app/bin +ENV JMXTRANS_AGENT jmxtrans-agent-1.2.6 +ENV DOCKERIZE_VERSION v0.6.1 + +ADD https://github.com/jwilder/dockerize/releases/download/${DOCKERIZE_VERSION}/dockerize-alpine-linux-amd64-${DOCKERIZE_VERSION}.tar.gz dockerize.tar.gz +RUN tar xzf dockerize.tar.gz +RUN chmod +x dockerize + +RUN mkdir -p ${APP_HOME} +RUN chmod a+w /app + +COPY target/${APP_NAME}.jar ${APP_HOME}/ +COPY build/docker/start-app.sh ${APP_HOME}/ +COPY build/docker/jmxtrans-agent.xml ${APP_HOME}/ + +RUN chmod +x ${APP_HOME}/start-app.sh +ADD https://github.com/jmxtrans/jmxtrans-agent/releases/download/${JMXTRANS_AGENT}/${JMXTRANS_AGENT}.jar ${APP_HOME}/ + +WORKDIR ${APP_HOME} + +ENTRYPOINT ["./start-app.sh"] diff --git a/service-graph/graph-builder/build/docker/jmxtrans-agent.xml b/service-graph/graph-builder/build/docker/jmxtrans-agent.xml new file mode 100644 index 000000000..6f53416f0 --- /dev/null +++ b/service-graph/graph-builder/build/docker/jmxtrans-agent.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ${HAYSTACK_GRAPHITE_HOST:monitoring-influxdb-graphite.kube-system.svc} + ${HAYSTACK_GRAPHITE_PORT:2003} + ${HAYSTACK_GRAPHITE_ENABLED:true} + + haystack.service-graph.graph-builder.#hostname#. + + 60 + diff --git a/service-graph/graph-builder/build/docker/start-app.sh b/service-graph/graph-builder/build/docker/start-app.sh new file mode 100755 index 000000000..694d74941 --- /dev/null +++ b/service-graph/graph-builder/build/docker/start-app.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +[ -z "$JAVA_XMS" ] && JAVA_XMS=1024m +[ -z "$JAVA_XMX" ] && JAVA_XMX=1024m + +set -e +JAVA_OPTS="${JAVA_OPTS} \ +-javaagent:${APP_HOME}/${JMXTRANS_AGENT}.jar=${APP_HOME}/jmxtrans-agent.xml \ +-XX:+UseG1GC \ +-Xmx${JAVA_XMX} \ +-Xms${JAVA_XMS} \ +-Dapplication.name=${APP_NAME} \ +-Dapplication.home=${APP_HOME}" + +if [[ -n "$SERVICE_DEBUG_ON" ]] && [[ "$SERVICE_DEBUG_ON" == true ]]; then + JAVA_OPTS="$JAVA_OPTS -Xdebug -Xrunjdwp:transport=dt_socket,address=5005,server=y" +fi + +exec java ${JAVA_OPTS} -jar "${APP_HOME}/${APP_NAME}.jar" diff --git a/service-graph/graph-builder/pom.xml b/service-graph/graph-builder/pom.xml new file mode 100644 index 000000000..d1c8df374 --- /dev/null +++ b/service-graph/graph-builder/pom.xml @@ -0,0 +1,222 @@ + + + + + haystack-service-graph + com.expedia.www + 1.0.15-SNAPSHOT + + + 4.0.0 + haystack-service-graph-graph-builder + jar + + + + + Apache License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + 1.1.0 + com.expedia.www.haystack.service.graph.graph.builder.App + ${project.artifactId}-${project.version} + + + + + org.apache.commons + commons-lang3 + + + + org.apache.httpcomponents + httpclient + + + + + org.apache.kafka + kafka_${scala.major.minor.version} + ${kafka-version} + + + org.slf4j + slf4j-log4j12 + + + + + org.apache.kafka + kafka-clients + ${kafka-version} + + + + com.expedia.www + haystack-logback-metrics-appender + + + + + org.eclipse.jetty + jetty-server + + + org.eclipse.jetty + jetty-servlet + + + org.apache.httpcomponents + fluent-hc + + + + + + org.apache.kafka + kafka-streams + ${kafka-version} + test + test + + + + org.apache.kafka + kafka-streams + ${kafka-version} + + + + org.apache.kafka + kafka_${scala.major.minor.version} + ${kafka-version} + + + org.slf4j + slf4j-log4j12 + + + test + test + + + + org.apache.kafka + kafka-clients + ${kafka-version} + test + test + + + + org.mockito + mockito-all + + + + + + ${finalName} + + + + org.scalatest + scalatest-maven-plugin + + + test + + test + + + org.expedia.www.haystack.commons.scalatest.IntegrationSuite + + + + integration-test + integration-test + + test + + + org.expedia.www.haystack.commons.scalatest.IntegrationSuite + + + + + + + org.apache.maven.plugins + maven-shade-plugin + + true + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + + + + package + + shade + + + + + reference.conf + + + ${mainClass} + + + + + + + + + net.alchim31.maven + scala-maven-plugin + + + + org.scalastyle + scalastyle-maven-plugin + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + diff --git a/service-graph/graph-builder/src/main/resources/app.conf b/service-graph/graph-builder/src/main/resources/app.conf new file mode 100644 index 000000000..c4120541b --- /dev/null +++ b/service-graph/graph-builder/src/main/resources/app.conf @@ -0,0 +1,55 @@ +health.status.path = "/app/isHealthy" + +kafka { + close.timeout.ms = 30000 + + streams { + application.id = "haystack-service-graph-graph-builder" + bootstrap.servers = "kafkasvc:9092" + num.stream.threads = 4 + request.timeout.ms = 60000 + commit.interval.ms = 3000 + auto.offset.reset = latest + timestamp.extractor = "org.apache.kafka.streams.processor.WallclockTimestampExtractor" + replication.factor = 1 + } + + rocksdb { + block.cache.size = 16777216 + block.size = 16384 + cache.index.and.filter.blocks = true + max.write.buffer.number = 2 + } + + consumer { + topic = "graph-nodes" + } + + producer { + topic = "service-graph" + } + + aggregate { + window.sec = 300 + retention.days = 7 + } +} + +service { + host = "localhost" + threads { + min = 1 + max = 5 + idle.timeout = 12000 + } + + http { + port = 8080 + idle.timeout = 12000 + } + + client { + connection.timeout = 1000 + socket.timeout = 1000 + } +} diff --git a/service-graph/graph-builder/src/main/resources/logback.xml b/service-graph/graph-builder/src/main/resources/logback.xml new file mode 100644 index 000000000..c45f62d7b --- /dev/null +++ b/service-graph/graph-builder/src/main/resources/logback.xml @@ -0,0 +1,27 @@ + + + + + + + true + + + + + + %d{yyyy-MM-dd HH:mm:ss:SSS} %thread, %level, %logger{70}, "%msg" %replace(%ex){'[\n]+', '\\n'}%nopex%n + + + + + + ${HAYSTACK_LOG_QUEUE_SIZE:-500} + ${HAYSTACK_LOG_DISCARD_THRESHOLD:-0} + + + + + + + diff --git a/service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/App.scala b/service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/App.scala new file mode 100644 index 000000000..d942f5edd --- /dev/null +++ b/service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/App.scala @@ -0,0 +1,144 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.service.graph.graph.builder + +import com.codahale.metrics.JmxReporter +import com.expedia.www.haystack.commons.health.{HealthStatusController, UpdateHealthStatusFile} +import com.expedia.www.haystack.commons.kstreams.app.ManagedKafkaStreams +import com.expedia.www.haystack.commons.metrics.MetricsSupport +import com.expedia.www.haystack.service.graph.graph.builder.config.AppConfiguration +import com.expedia.www.haystack.service.graph.graph.builder.config.entities.{KafkaConfiguration, ServiceConfiguration} +import com.expedia.www.haystack.service.graph.graph.builder.service.fetchers.{LocalOperationEdgesFetcher, LocalServiceEdgesFetcher, RemoteOperationEdgesFetcher, RemoteServiceEdgesFetcher} +import com.expedia.www.haystack.service.graph.graph.builder.service.resources._ +import com.expedia.www.haystack.service.graph.graph.builder.service.utils.QueryTimestampReader +import com.expedia.www.haystack.service.graph.graph.builder.service.{HttpService, ManagedHttpService} +import com.expedia.www.haystack.service.graph.graph.builder.stream.{ServiceGraphStreamSupplier, StreamSupplier} +import com.netflix.servo.util.VisibleForTesting +import org.apache.kafka.streams.KafkaStreams +import org.slf4j.LoggerFactory + +/** + * Starting point for graph-builder application + */ +object App extends MetricsSupport { + private val LOGGER = LoggerFactory.getLogger(App.getClass) + + def main(args: Array[String]): Unit = { + val appConfiguration = new AppConfiguration() + + // instantiate the application + // if any exception occurs during instantiation + // gracefully handles teardown and does system exit + val app = runApp(appConfiguration) + + if (app == null) { + System.exit(1) + } else { + // add a shutdown hook + Runtime.getRuntime.addShutdownHook(new Thread() { + override def run(): Unit = { + LOGGER.info("Shutdown hook is invoked, tearing down the application.") + if (app != null) app.stop() + } + }) + } + } + + @VisibleForTesting + def runApp(appConfiguration: AppConfiguration): ManagedApplication = { + val jmxReporter: JmxReporter = JmxReporter.forRegistry(metricRegistry).build() + val healthStatusController = new HealthStatusController + healthStatusController.addListener(new UpdateHealthStatusFile(appConfiguration.healthStatusFilePath)) + + var stream: KafkaStreams = null + var service: HttpService = null + try { + + // build kafka stream to create service graph + // it ingests graph edges and create service graph out of it + // graphs are stored as materialized ktable in stream state store + stream = createStream(appConfiguration.kafkaConfig, healthStatusController) + + // build http service to query current service graph + // it performs interactive query on ktable + service = createService(appConfiguration.serviceConfig, stream, appConfiguration.kafkaConfig) + + // wrap service and stream in a managed application instance + // ManagedApplication makes sure that startup/shutdown sequence is right + // and startup/shutdown errors are handling appropriately + val app = new ManagedApplication( + new ManagedHttpService(service), + new ManagedKafkaStreams(stream), + jmxReporter, + LoggerFactory.getLogger(classOf[ManagedApplication]) + ) + + // start the application + // if any exception occurs during startup + // gracefully handles teardown and does system exit + app.start() + + // mark the app as healthy + healthStatusController.setHealthy() + + app + } catch { + case ex: Exception => + LOGGER.error("Observed fatal exception instantiating the app", ex) + if(stream != null) stream.close() + if(service != null) service.close() + null + } + } + + @VisibleForTesting + def createStream(kafkaConfig: KafkaConfiguration, healthController: HealthStatusController): KafkaStreams = { + // service graph kafka stream supplier + val serviceGraphStreamSupplier = new ServiceGraphStreamSupplier(kafkaConfig) + + // create kstream using application topology + val streamsSupplier = new StreamSupplier( + serviceGraphStreamSupplier, + healthController, + kafkaConfig.streamsConfig, + kafkaConfig.consumerTopic) + + // build kstream app + streamsSupplier.get() + } + + @VisibleForTesting + def createService(serviceConfig: ServiceConfiguration, stream: KafkaStreams, kafkaConfig: KafkaConfiguration): HttpService = { + val storeName = kafkaConfig.producerTopic + val localOperationEdgesFetcher = new LocalOperationEdgesFetcher(stream, storeName) + val remoteOperationEdgesFetcher = new RemoteOperationEdgesFetcher(serviceConfig.client) + val localServiceEdgesFetcher = new LocalServiceEdgesFetcher(stream, storeName) + val remoteServiceEdgesFetcher = new RemoteServiceEdgesFetcher(serviceConfig.client) + + implicit val timestampReader: QueryTimestampReader = new QueryTimestampReader(kafkaConfig.aggregationWindowSec) + val servlets = Map( + "/operationgraph/local" -> new LocalOperationGraphResource(localOperationEdgesFetcher), + "/operationgraph" -> new GlobalOperationGraphResource(stream, storeName, serviceConfig, localOperationEdgesFetcher, remoteOperationEdgesFetcher), + "/servicegraph/local" -> new LocalServiceGraphResource(localServiceEdgesFetcher), + "/servicegraph" -> new GlobalServiceGraphResource(stream, storeName, serviceConfig, localServiceEdgesFetcher, remoteServiceEdgesFetcher), + "/isWorking" -> new IsWorkingResource + ) + + new HttpService(serviceConfig, servlets) + } +} \ No newline at end of file diff --git a/service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/ManagedApplication.scala b/service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/ManagedApplication.scala new file mode 100644 index 000000000..85e37b019 --- /dev/null +++ b/service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/ManagedApplication.scala @@ -0,0 +1,63 @@ +package com.expedia.www.haystack.service.graph.graph.builder + +import com.codahale.metrics.JmxReporter +import com.expedia.www.haystack.commons.kstreams.app.ManagedService +import com.expedia.www.haystack.commons.logger.LoggerUtils +import com.expedia.www.haystack.service.graph.graph.builder.ManagedApplication._ +import org.slf4j.Logger + +import scala.util.Try + +object ManagedApplication { + val StartMessage = "Starting the given topology and service" + val HttpStartMessage = "HTTP service started successfully" + val StreamStartMessage = "Kafka stream started successfully" + val HttpStopMessage = "Shutting down HTTP service" + val StreamStopMessage = "Shutting down Kafka stream" + val JmxReporterStopMessage = "Shutting down JMX Reporter" + val LoggerStopMessage = "Shutting down logger. Bye!" +} + +class ManagedApplication(service: ManagedService, stream: ManagedService, jmxReporter: JmxReporter, logger: Logger) { + + require(service != null) + require(stream != null) + require(jmxReporter != null) + require(logger != null) + + def start(): Unit = { + try { + jmxReporter.start() + logger.info(StartMessage) + + service.start() + logger.info(HttpStartMessage) + + stream.start() + logger.info(StreamStartMessage) + } catch { + case ex: Exception => + logger.error("Observed fatal exception while starting the app", ex) + stop() + System.exit(1) + } + } + + /** + * This method stops the given `StreamsRunner` and `JmxReporter` is they have been + * previously started. If not, this method does nothing + */ + def stop(): Unit = { + logger.info(HttpStopMessage) + Try(service.stop()) + + logger.info(StreamStopMessage) + Try(stream.stop()) + + logger.info(JmxReporterStopMessage) + Try(jmxReporter.close()) + + logger.info(LoggerStopMessage) + Try(LoggerUtils.shutdownLogger()) + } +} diff --git a/service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/config/AppConfiguration.scala b/service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/config/AppConfiguration.scala new file mode 100644 index 000000000..d925fd5e8 --- /dev/null +++ b/service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/config/AppConfiguration.scala @@ -0,0 +1,140 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.service.graph.graph.builder.config + +import java.util.Properties + +import com.expedia.www.haystack.commons.config.ConfigurationLoader +import com.expedia.www.haystack.service.graph.graph.builder.config.entities._ +import com.typesafe.config.Config +import org.apache.commons.lang3.StringUtils +import org.apache.kafka.streams.StreamsConfig +import org.apache.kafka.streams.Topology.AutoOffsetReset + +import scala.collection.JavaConverters._ + +/** + * This class reads the configuration from the given resource name using {@link ConfigurationLoader ConfigurationLoader} + * + * @param resourceName name of the resource file to load + */ +class AppConfiguration(resourceName: String) { + + require(StringUtils.isNotBlank(resourceName)) + + private val config = ConfigurationLoader.loadConfigFileWithEnvOverrides(resourceName = this.resourceName) + + /** + * default constructor. Loads config from resource name to "app.conf" + */ + def this() = this("app.conf") + + /** + * Location of the health status file + */ + val healthStatusFilePath: String = config.getString("health.status.path") + + /** + * Instance of {@link KafkaConfiguration KafkaConfiguration} to be used by the kstreams application + */ + lazy val kafkaConfig: KafkaConfiguration = { + + // verify if the applicationId and bootstrap server config are non empty + def verifyRequiredProps(props: Properties): Unit = { + require(StringUtils.isNotBlank(props.getProperty(StreamsConfig.APPLICATION_ID_CONFIG))) + require(StringUtils.isNotBlank(props.getProperty(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG))) + require(StringUtils.isNotBlank(props.getProperty(StreamsConfig.APPLICATION_SERVER_CONFIG))) + } + + def addProps(config: Config, props: Properties, prefix: (String) => String = identity): Unit = { + config.entrySet().asScala.foreach(kv => { + val propKeyName = prefix(kv.getKey) + props.setProperty(propKeyName, kv.getValue.unwrapped().toString) + }) + } + + val kafka = config.getConfig("kafka") + val streamsConfig = kafka.getConfig("streams") + val consumerConfig = kafka.getConfig("consumer") + val producerConfig = kafka.getConfig("producer") + + // add stream specific properties + val streamProps = new Properties + addProps(streamsConfig, streamProps) + // add stream application server config + if (StringUtils.isBlank(streamProps.getProperty(StreamsConfig.APPLICATION_SERVER_CONFIG))) { + streamProps.setProperty(StreamsConfig.APPLICATION_SERVER_CONFIG, s"${config.getString("service.host")}:${config.getInt("service.http.port")}") + } + + if (kafka.hasPath("rocksdb")) { + CustomRocksDBConfig.setRocksDbConfig(kafka.getConfig("rocksdb")) + streamProps.put(StreamsConfig.ROCKSDB_CONFIG_SETTER_CLASS_CONFIG, classOf[CustomRocksDBConfig]) + } + + // validate props + verifyRequiredProps(streamProps) + + // offset reset for kstream + val autoOffsetReset = + if (streamsConfig.hasPath("auto.offset.reset")) { + AutoOffsetReset.valueOf(streamsConfig.getString("auto.offset.reset").toUpperCase) + } else { + AutoOffsetReset.LATEST + } + + val aggregation = kafka.getConfig("aggregate") + val aggregationWindowSec = aggregation.getInt("window.sec") + val aggregationRetentionDays = aggregation.getInt("retention.days") + + KafkaConfiguration(new StreamsConfig(streamProps), + consumerConfig.getString("topic"), + producerConfig.getString("topic"), + autoOffsetReset, + kafka.getLong("close.timeout.ms"), + aggregationWindowSec, + aggregationRetentionDays + ) + } + + /** + * Instance of {@link ServiceConfiguration} to be used by servlet container + */ + lazy val serviceConfig: ServiceConfiguration = { + val service = config.getConfig("service") + val threads = service.getConfig("threads") + val http = service.getConfig("http") + val client = service.getConfig("client") + + ServiceConfiguration( + service.getString("host"), + ServiceThreadsConfiguration( + threads.getInt("min"), + threads.getInt("max"), + threads.getInt("idle.timeout") + ), + ServiceHttpConfiguration( + http.getInt("port"), + http.getLong("idle.timeout") + ), + ServiceClientConfiguration( + client.getInt("connection.timeout"), + client.getInt("socket.timeout") + ) + ) + } +} diff --git a/service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/config/entities/CustomRocksDBConfig.scala b/service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/config/entities/CustomRocksDBConfig.scala new file mode 100644 index 000000000..0ee350334 --- /dev/null +++ b/service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/config/entities/CustomRocksDBConfig.scala @@ -0,0 +1,52 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.service.graph.graph.builder.config.entities + +import java.util + +import com.expedia.www.haystack.service.graph.graph.builder.config.entities.CustomRocksDBConfig._ +import com.google.common.annotations.VisibleForTesting +import com.typesafe.config.{Config, ConfigRenderOptions} +import org.apache.kafka.streams.state.RocksDBConfigSetter +import org.rocksdb.{BlockBasedTableConfig, Options} +import org.slf4j.{Logger, LoggerFactory} + +object CustomRocksDBConfig { + protected val LOGGER: Logger = LoggerFactory.getLogger(classOf[CustomRocksDBConfig]) + + @VisibleForTesting var rocksDBConfig: Config = _ + def setRocksDbConfig(cfg: Config): Unit = rocksDBConfig = cfg +} + +class CustomRocksDBConfig extends RocksDBConfigSetter { + + override def setConfig(storeName: String, options: Options, configs: util.Map[String, AnyRef]): Unit = { + require(rocksDBConfig != null, "rocksdb config should not be empty or null") + + LOGGER.info("setting rocksdb configuration '{}'", + rocksDBConfig.root().render(ConfigRenderOptions.defaults().setOriginComments(false))) + + val tableConfig = new BlockBasedTableConfig + tableConfig.setBlockCacheSize(rocksDBConfig.getLong("block.cache.size")) + tableConfig.setBlockSize(rocksDBConfig.getLong("block.size")) + tableConfig.setCacheIndexAndFilterBlocks(rocksDBConfig.getBoolean("cache.index.and.filter.blocks")) + options.setTableFormatConfig(tableConfig) + options.setMaxWriteBufferNumber(rocksDBConfig.getInt("max.write.buffer.number")) + } +} diff --git a/service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/config/entities/KafkaConfiguration.scala b/service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/config/entities/KafkaConfiguration.scala new file mode 100644 index 000000000..5201cbf08 --- /dev/null +++ b/service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/config/entities/KafkaConfiguration.scala @@ -0,0 +1,45 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.service.graph.graph.builder.config.entities + +import org.apache.commons.lang3.StringUtils +import org.apache.kafka.streams.StreamsConfig +import org.apache.kafka.streams.Topology.AutoOffsetReset + +/** + * Case class holding required configuration for the node finder kstreams app + * @param streamsConfig valid instance of StreamsConfig + * @param consumerTopic topic name for incoming graph edges topic + * @param producerTopic topic name for materialized ktable changelogs + * @param autoOffsetReset Offset type for the kstreams app to start with + * @param closeTimeoutInMs time for closing a kafka topic + */ +case class KafkaConfiguration(streamsConfig: StreamsConfig, + consumerTopic: String, + producerTopic: String, + autoOffsetReset: AutoOffsetReset, + closeTimeoutInMs: Long, + aggregationWindowSec: Int, + aggregationRetentionDays: Int + ) { + require(streamsConfig != null) + require(StringUtils.isNotBlank(consumerTopic)) + require(StringUtils.isNotBlank(producerTopic)) + require(autoOffsetReset != null) + require(closeTimeoutInMs > 0) +} diff --git a/service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/config/entities/ServiceClientConfiguration.scala b/service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/config/entities/ServiceClientConfiguration.scala new file mode 100644 index 000000000..71d0f26f5 --- /dev/null +++ b/service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/config/entities/ServiceClientConfiguration.scala @@ -0,0 +1,28 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.service.graph.graph.builder.config.entities + +/** + * + * @param connectionTimeout + * @param socketTimeout + */ +case class ServiceClientConfiguration(connectionTimeout: Int, socketTimeout: Int) { + require(connectionTimeout > 0) + require(socketTimeout > 0) +} diff --git a/service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/config/entities/ServiceConfiguration.scala b/service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/config/entities/ServiceConfiguration.scala new file mode 100644 index 000000000..74964d0d8 --- /dev/null +++ b/service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/config/entities/ServiceConfiguration.scala @@ -0,0 +1,34 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.service.graph.graph.builder.config.entities + +import org.apache.commons.lang3.StringUtils + +/** + * Configuration for servlets and servlet container + * + * @param threads threads configuration of servelet container + * @param http http configuration of servelet container + * @param client configuration of http client + */ +case class ServiceConfiguration(host: String, threads: ServiceThreadsConfiguration, http: ServiceHttpConfiguration, client: ServiceClientConfiguration) { + require(StringUtils.isNotEmpty(host)) + require(threads != null) + require(http != null) + require(client != null) +} diff --git a/service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/config/entities/ServiceHttpConfiguration.scala b/service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/config/entities/ServiceHttpConfiguration.scala new file mode 100644 index 000000000..13773c0be --- /dev/null +++ b/service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/config/entities/ServiceHttpConfiguration.scala @@ -0,0 +1,28 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.service.graph.graph.builder.config.entities + +/** + * Http configuration for servlet container + * @param port port to use by servlet container + * @param idleTimeout http connection timeout + */ +case class ServiceHttpConfiguration(port: Int, idleTimeout: Long) { + require(port > 0) + require(idleTimeout > 0) +} diff --git a/service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/config/entities/ServiceThreadsConfiguration.scala b/service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/config/entities/ServiceThreadsConfiguration.scala new file mode 100644 index 000000000..add238673 --- /dev/null +++ b/service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/config/entities/ServiceThreadsConfiguration.scala @@ -0,0 +1,30 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.service.graph.graph.builder.config.entities + +/** + * Threads configuration for servlet container + * @param min minimum number of threads to use for running servlets + * @param max maximum number of threads to use for running servlets + * @param idleTimeout timeout for a thread + */ +case class ServiceThreadsConfiguration(min: Int, max: Int, idleTimeout: Int) { + require(min > 0) + require(max > min) + require(idleTimeout > 0) +} diff --git a/service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/model/EdgeStats.scala b/service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/model/EdgeStats.scala new file mode 100644 index 000000000..b65b38ddb --- /dev/null +++ b/service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/model/EdgeStats.scala @@ -0,0 +1,54 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.service.graph.graph.builder.model + +import com.expedia.www.haystack.commons.entities.{GraphEdge, TagKeys} + +import scala.collection.mutable + +/** + * Object to hold stats for graph edges + * + * @param count edge count seen so far + * @param lastSeen timestamp the edge was last seen, in ms + * @param errorCount error rate for this specific operation + */ +case class EdgeStats(count: Long, + lastSeen: Long, + errorCount: Long, + sourceTags: mutable.Map[String, String] = mutable.HashMap[String, String](), + destinationTags: mutable.Map[String, String] = mutable.HashMap[String, String]()) { + def update(e: GraphEdge): EdgeStats = { + this.sourceTags ++= e.source.tags + this.sourceTags.remove(TagKeys.ERROR_KEY) + this.destinationTags ++= e.destination.tags + this.destinationTags.remove(TagKeys.ERROR_KEY) + + val incrErrorCountBy = if (e.source.tags.getOrElse(TagKeys.ERROR_KEY, "false") == "true") 1 else 0 + EdgeStats( + count + 1, + lastSeen(e), + errorCount + incrErrorCountBy, + sourceTags, + destinationTags) + } + + private def lastSeen(e: GraphEdge): Long = { + if (e.sourceTimestamp == 0) System.currentTimeMillis() else Math.max(e.sourceTimestamp, this.lastSeen) + } +} diff --git a/service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/model/EdgeStatsSerde.scala b/service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/model/EdgeStatsSerde.scala new file mode 100644 index 000000000..0d4ca054f --- /dev/null +++ b/service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/model/EdgeStatsSerde.scala @@ -0,0 +1,60 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.service.graph.graph.builder.model + +import java.util + +import org.apache.kafka.common.serialization.{Deserializer, Serde, Serializer} +import org.json4s.DefaultFormats +import org.json4s.jackson.Serialization + +class EdgeStatsSerde extends Serde[EdgeStats] { + implicit val formats = DefaultFormats + + override def deserializer(): Deserializer[EdgeStats] = new EdgeStatsDeserializer + + override def serializer(): Serializer[EdgeStats] = new EdgeStatsSerializer + + override def configure(map: util.Map[String, _], b: Boolean): Unit = () + + override def close(): Unit = () +} + +class EdgeStatsSerializer extends Serializer[EdgeStats] { + implicit val formats = DefaultFormats + + override def serialize(topic: String, edgeStats: EdgeStats): Array[Byte] = { + Serialization.write(edgeStats).getBytes("utf-8") + } + + override def configure(map: util.Map[String, _], b: Boolean): Unit = () + + override def close(): Unit = () +} + +class EdgeStatsDeserializer extends Deserializer[EdgeStats] { + implicit val formats = DefaultFormats + + override def deserialize(topic: String, data: Array[Byte]): EdgeStats = { + if(data == null) EdgeStats(0, 0, 0) else Serialization.read[EdgeStats](new String(data)) + } + + override def configure(map: util.Map[String, _], b: Boolean): Unit = () + + override def close(): Unit = () +} diff --git a/service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/model/OperationGraph.scala b/service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/model/OperationGraph.scala new file mode 100644 index 000000000..b5cc01963 --- /dev/null +++ b/service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/model/OperationGraph.scala @@ -0,0 +1,26 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.service.graph.graph.builder.model + +/** + * Service graph + * @param edges list of edges in the graph + */ +case class OperationGraph(edges: Seq[OperationGraphEdge]) { + require(edges != null) +} diff --git a/service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/model/OperationGraphEdge.scala b/service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/model/OperationGraphEdge.scala new file mode 100644 index 000000000..58af7e180 --- /dev/null +++ b/service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/model/OperationGraphEdge.scala @@ -0,0 +1,40 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.service.graph.graph.builder.model + +import org.apache.commons.lang3.StringUtils + +/** + * A graph edge representing relationship between two services over an operation + * @param source source service + * @param destination destination service + * @param stats stats around the edge + * @param effectiveFrom start timestamp from which stats are collected + * @param effectiveTo end timestamp till which stats are collected + */ +case class OperationGraphEdge(source: String, + destination: String, + operation: String, + stats: EdgeStats, + effectiveFrom: Long, + effectiveTo: Long) { + require(StringUtils.isNotEmpty(source)) + require(StringUtils.isNotEmpty(destination)) + require(StringUtils.isNotEmpty(operation)) + require(stats != null) +} diff --git a/service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/model/ServiceGraph.scala b/service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/model/ServiceGraph.scala new file mode 100644 index 000000000..cf327cfb7 --- /dev/null +++ b/service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/model/ServiceGraph.scala @@ -0,0 +1,26 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.service.graph.graph.builder.model + +/** + * Service graph + * @param edges list of edges in the graph + */ +case class ServiceGraph(edges: Seq[ServiceGraphEdge]) { + require(edges != null) +} diff --git a/service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/model/ServiceGraphEdge.scala b/service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/model/ServiceGraphEdge.scala new file mode 100644 index 000000000..2c5905a25 --- /dev/null +++ b/service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/model/ServiceGraphEdge.scala @@ -0,0 +1,80 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.service.graph.graph.builder.model + +import scala.collection.mutable +/** + * A graph edge representing relationship between two services over an operation + * + * @param source source service + * @param destination destination service + * @param stats stats around the edge + * @param effectiveFrom start timestamp from which stats are collected + * @param effectiveTo end timestamp till which stats are collected + * + */ +case class ServiceGraphEdge(source: ServiceGraphVertex, + destination: ServiceGraphVertex, + stats: ServiceEdgeStats, + effectiveFrom: Long, + effectiveTo: Long) { + require(source != null) + require(destination != null) + require(stats != null) + + def mergeTags(first: Map[String, String], second: Map[String, String]): Map[String, String] = { + val merged = new mutable.HashMap[String, mutable.HashSet[String]]() + + def merge(tags: Map[String, String]) { + tags.foreach { + case (key, value) => + val valueSet = merged.getOrElseUpdate(key, new mutable.HashSet[String]()) + valueSet ++= value.split(",") + } + } + + merge(first) + merge(second) + + merged.mapValues(_.mkString(",")).toMap + } + + def +(other: ServiceGraphEdge): ServiceGraphEdge = { + val sourceVertex = this.source.copy(tags = mergeTags(other.source.tags, this.source.tags)) + val destinationVertex = this.destination.copy(tags = mergeTags(other.destination.tags, this.destination.tags)) + ServiceGraphEdge( + sourceVertex, + destinationVertex, + this.stats + other.stats, + Math.min(this.effectiveFrom, other.effectiveFrom), + Math.max(this.effectiveTo, other.effectiveTo)) + } +} + +case class ServiceGraphVertex(name: String, tags: Map[String, String] = Map()) + +case class ServiceEdgeStats(count: Long, + lastSeen: Long, + errorCount: Long) { + def +(other: ServiceEdgeStats): ServiceEdgeStats = { + ServiceEdgeStats( + this.count + other.count, + Math.max(this.lastSeen, other.lastSeen), + this.errorCount + other.errorCount) + } +} diff --git a/service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/service/HttpService.scala b/service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/service/HttpService.scala new file mode 100644 index 000000000..ef2220036 --- /dev/null +++ b/service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/service/HttpService.scala @@ -0,0 +1,68 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.service.graph.graph.builder.service + +import javax.servlet.Servlet + +import com.expedia.www.haystack.service.graph.graph.builder.config.entities.ServiceConfiguration +import org.eclipse.jetty.server.{HttpConfiguration, HttpConnectionFactory, Server, ServerConnector} +import org.eclipse.jetty.servlet.{ServletContextHandler, ServletHolder} +import org.eclipse.jetty.util.thread.QueuedThreadPool +import org.slf4j.LoggerFactory + +class HttpService(config: ServiceConfiguration, resources: Map[String, Servlet]) extends AutoCloseable{ + private val LOGGER = LoggerFactory.getLogger(classOf[HttpService]) + + // TODO move server creation to a supplier + private val server = { + // threadpool to run servlets + val threadPool = new QueuedThreadPool(config.threads.max, config.threads.min, config.threads.idleTimeout) + + // building jetty server + val server = new Server(threadPool) + + // configuring jetty's http parameters + val httpConnector = new ServerConnector(server, new HttpConnectionFactory(new HttpConfiguration)) + httpConnector.setPort(config.http.port) + httpConnector.setIdleTimeout(config.http.idleTimeout) + server.addConnector(httpConnector) + + // adding servlets + val context = new ServletContextHandler(server, "/") + resources.foreach( + resource => { + LOGGER.info(s"adding servlet ${resource._2} at ${resource._1}") + context.addServlet(new ServletHolder(resource._2), resource._1) + } + ) + + // built jetty server object + LOGGER.info("jetty server constructed") + server + } + + + def start(): Unit = { + server.start() + } + + def close(): Unit = { + server.stop() + server.destroy() + } +} diff --git a/service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/service/ManagedHttpService.scala b/service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/service/ManagedHttpService.scala new file mode 100644 index 000000000..1c704a33f --- /dev/null +++ b/service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/service/ManagedHttpService.scala @@ -0,0 +1,40 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.service.graph.graph.builder.service + +import java.util.concurrent.atomic.AtomicBoolean + +import com.expedia.www.haystack.commons.kstreams.app.ManagedService + +class ManagedHttpService(service: HttpService) extends ManagedService { + require(service != null) + private val isRunning: AtomicBoolean = new AtomicBoolean(false) + + override def start(): Unit = { + service.start() + isRunning.set(true) + } + + override def stop(): Unit = { + if(isRunning.getAndSet(false)) { + service.close() + } + } + + override def hasStarted: Boolean = isRunning.get() +} diff --git a/service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/service/fetchers/LocalOperationEdgesFetcher.scala b/service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/service/fetchers/LocalOperationEdgesFetcher.scala new file mode 100644 index 000000000..ca6fca32c --- /dev/null +++ b/service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/service/fetchers/LocalOperationEdgesFetcher.scala @@ -0,0 +1,50 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.service.graph.graph.builder.service.fetchers + +import com.expedia.www.haystack.commons.entities.GraphEdge +import com.expedia.www.haystack.service.graph.graph.builder.model.{EdgeStats, OperationGraphEdge} +import com.expedia.www.haystack.service.graph.graph.builder.service.utils.IOUtils +import org.apache.kafka.streams.kstream.Windowed +import org.apache.kafka.streams.state.{KeyValueIterator, QueryableStoreTypes, ReadOnlyWindowStore} +import org.apache.kafka.streams.{KafkaStreams, KeyValue} + +import scala.collection.JavaConverters._ + +class LocalOperationEdgesFetcher(streams: KafkaStreams, storeName: String) { + private lazy val store: ReadOnlyWindowStore[GraphEdge, EdgeStats] = + streams.store(storeName, QueryableStoreTypes.windowStore[GraphEdge, EdgeStats]()) + + def fetchEdges(from: Long, to: Long): List[OperationGraphEdge] = { + var iterator: KeyValueIterator[Windowed[GraphEdge], EdgeStats] = null + try { + iterator = store.fetchAll(from, to) + val edges = for (kv: KeyValue[Windowed[GraphEdge], EdgeStats] <- iterator.asScala) + yield OperationGraphEdge( + kv.key.key.source.name, + kv.key.key.destination.name, + kv.key.key.operation, + kv.value, + kv.key.window().start(), + Math.min(System.currentTimeMillis(), kv.key.window().end())) + edges.toList + } finally { + IOUtils.closeSafely(iterator) + } + } +} diff --git a/service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/service/fetchers/LocalServiceEdgesFetcher.scala b/service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/service/fetchers/LocalServiceEdgesFetcher.scala new file mode 100644 index 000000000..d8b8c34cf --- /dev/null +++ b/service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/service/fetchers/LocalServiceEdgesFetcher.scala @@ -0,0 +1,51 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.service.graph.graph.builder.service.fetchers + +import com.expedia.www.haystack.commons.entities.GraphEdge +import com.expedia.www.haystack.service.graph.graph.builder.model.{EdgeStats, ServiceEdgeStats, ServiceGraphEdge, ServiceGraphVertex} +import com.expedia.www.haystack.service.graph.graph.builder.service.utils.EdgesMerger._ +import com.expedia.www.haystack.service.graph.graph.builder.service.utils.IOUtils +import org.apache.kafka.streams.kstream.Windowed +import org.apache.kafka.streams.state.{KeyValueIterator, QueryableStoreTypes, ReadOnlyWindowStore} +import org.apache.kafka.streams.{KafkaStreams, KeyValue} + +import scala.collection.JavaConverters._ + +class LocalServiceEdgesFetcher(streams: KafkaStreams, storeName: String) { + private lazy val store: ReadOnlyWindowStore[GraphEdge, EdgeStats] = + streams.store(storeName, QueryableStoreTypes.windowStore[GraphEdge, EdgeStats]()) + + def fetchEdges(from: Long, to: Long): Seq[ServiceGraphEdge] = { + var iterator: KeyValueIterator[Windowed[GraphEdge], EdgeStats] = null + try { + iterator = store.fetchAll(from, to) + val serviceGraphEdges = + for (kv: KeyValue[Windowed[GraphEdge], EdgeStats] <- iterator.asScala) + yield ServiceGraphEdge( + ServiceGraphVertex(kv.key.key.source.name, kv.value.sourceTags.toMap), + ServiceGraphVertex(kv.key.key.destination.name, kv.value.destinationTags.toMap), + ServiceEdgeStats(kv.value.count, kv.value.lastSeen, kv.value.errorCount), + kv.key.window().start(), Math.min(System.currentTimeMillis(), kv.key.window().end())) + + getMergedServiceEdges(serviceGraphEdges.toSeq) + } finally { + IOUtils.closeSafely(iterator) + } + } +} diff --git a/service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/service/fetchers/RemoteOperationEdgesFetcher.scala b/service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/service/fetchers/RemoteOperationEdgesFetcher.scala new file mode 100644 index 000000000..ce6a92caa --- /dev/null +++ b/service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/service/fetchers/RemoteOperationEdgesFetcher.scala @@ -0,0 +1,64 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.service.graph.graph.builder.service.fetchers + +import java.util.concurrent.Executors + +import com.expedia.www.haystack.service.graph.graph.builder.config.entities.ServiceClientConfiguration +import com.expedia.www.haystack.service.graph.graph.builder.model.{OperationGraph, OperationGraphEdge} +import org.apache.http.client.fluent.Request +import org.apache.http.client.utils.URIBuilder +import org.json4s.DefaultFormats +import org.json4s.jackson.Serialization + +import scala.concurrent.{ExecutionContext, Future} +import scala.util.Try + +class RemoteOperationEdgesFetcher(clientConfig: ServiceClientConfiguration) extends AutoCloseable { + + private val dispatcher = ExecutionContext.fromExecutorService( + Executors.newFixedThreadPool(Math.min(Runtime.getRuntime.availableProcessors(), 2))) + + implicit val formats = DefaultFormats + + def fetchEdges(host: String, port: Int, from: Long, to: Long): Future[Seq[OperationGraphEdge]] = { + val request = new URIBuilder() + .setScheme("http") + .setPath("/operationgraph/local") + .setParameter("from", from.toString) + .setParameter("to", to.toString) + .setHost(host) + .setPort(port) + .build() + + Future { + val response = Request.Get(request) + .connectTimeout(clientConfig.connectionTimeout) + .socketTimeout(clientConfig.socketTimeout) + .execute() + .returnContent() + .asString() + + Serialization.read[OperationGraph](response).edges + }(dispatcher) + } + + override def close(): Unit = { + Try(this.dispatcher.shutdown()) + } +} diff --git a/service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/service/fetchers/RemoteServiceEdgesFetcher.scala b/service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/service/fetchers/RemoteServiceEdgesFetcher.scala new file mode 100644 index 000000000..0ac7c1a84 --- /dev/null +++ b/service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/service/fetchers/RemoteServiceEdgesFetcher.scala @@ -0,0 +1,62 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.service.graph.graph.builder.service.fetchers + +import java.util.concurrent.Executors + +import com.expedia.www.haystack.service.graph.graph.builder.config.entities.ServiceClientConfiguration +import com.expedia.www.haystack.service.graph.graph.builder.model.{ServiceGraph, ServiceGraphEdge} +import org.apache.http.client.fluent.Request +import org.apache.http.client.utils.URIBuilder +import org.json4s.DefaultFormats +import org.json4s.jackson.Serialization + +import scala.concurrent.{ExecutionContext, Future} +import scala.util.Try + +class RemoteServiceEdgesFetcher(clientConfig: ServiceClientConfiguration) extends AutoCloseable { + implicit val formats = DefaultFormats + + private val dispatcher = ExecutionContext.fromExecutorService( + Executors.newFixedThreadPool(Math.min(Runtime.getRuntime.availableProcessors(), 2))) + + def fetchEdges(host: String, port: Int, from: Long, to: Long): Future[Seq[ServiceGraphEdge]] = { + val uri = new URIBuilder() + .setScheme("http") + .setPath("/servicegraph/local") + .setParameter("from", from.toString) + .setParameter("to", to.toString) + .setHost(host) + .setPort(port) + .build() + + Future { + val response = Request.Get(uri) + .connectTimeout(clientConfig.connectionTimeout) + .socketTimeout(clientConfig.socketTimeout) + .execute() + .returnContent() + .asString() + Serialization.read[ServiceGraph](response).edges + }(dispatcher) + } + + override def close(): Unit = { + Try(dispatcher.shutdown()) + } +} diff --git a/service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/service/resources/GlobalOperationGraphResource.scala b/service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/service/resources/GlobalOperationGraphResource.scala new file mode 100644 index 000000000..b5cac4c64 --- /dev/null +++ b/service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/service/resources/GlobalOperationGraphResource.scala @@ -0,0 +1,72 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.service.graph.graph.builder.service.resources + +import javax.servlet.http.HttpServletRequest + +import com.expedia.www.haystack.service.graph.graph.builder.config.entities.ServiceConfiguration +import com.expedia.www.haystack.service.graph.graph.builder.model.{OperationGraph, OperationGraphEdge} +import com.expedia.www.haystack.service.graph.graph.builder.service.fetchers.{LocalOperationEdgesFetcher, RemoteOperationEdgesFetcher} +import com.expedia.www.haystack.service.graph.graph.builder.service.utils.QueryTimestampReader +import org.apache.kafka.streams.KafkaStreams +import org.slf4j.LoggerFactory + +import scala.collection.JavaConverters._ +import scala.collection.mutable +import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.duration._ +import scala.concurrent.{Await, Future} + +class GlobalOperationGraphResource(streams: KafkaStreams, + storeName: String, + serviceConfig: ServiceConfiguration, + localEdgesFetcher: LocalOperationEdgesFetcher, + remoteEdgesFetcher: RemoteOperationEdgesFetcher)(implicit val timestampReader: QueryTimestampReader) +extends Resource("operationgraph") { + private val LOGGER = LoggerFactory.getLogger(classOf[GlobalOperationGraphResource]) + private val globalEdgeCount = metricRegistry.histogram("operationgraph.global.edges") + + protected override def get(request: HttpServletRequest): OperationGraph = { + val from = timestampReader.fromTimestamp(request) + val to = timestampReader.toTimestamp(request) + + // get list of all hosts containing service-graph store + // fetch local service graphs from all hosts + // and merge local graphs to create global graph + val edgesListFuture: Iterable[Future[Seq[OperationGraphEdge]]] = streams + .allMetadataForStore(storeName) + .asScala + .map(host => { + if (host.host() == serviceConfig.host) { + LOGGER.info(s"operation graph from local returned is ivnoked") + Future(localEdgesFetcher.fetchEdges(from, to)) + } else { + LOGGER.info(s"operation graph from ${host.host()} is invoked") + remoteEdgesFetcher.fetchEdges(host.host(), host.port(), from, to) + } + }) + + val singleResultFuture = Future.sequence(edgesListFuture) + val edgesList = Await + .result(singleResultFuture, serviceConfig.client.socketTimeout.millis) + .foldLeft(mutable.ListBuffer[OperationGraphEdge]())((buffer, coll) => buffer ++= coll) + + globalEdgeCount.update(edgesList.length) + OperationGraph(edgesList) + } +} diff --git a/service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/service/resources/GlobalServiceGraphResource.scala b/service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/service/resources/GlobalServiceGraphResource.scala new file mode 100644 index 000000000..dfd988a52 --- /dev/null +++ b/service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/service/resources/GlobalServiceGraphResource.scala @@ -0,0 +1,75 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.service.graph.graph.builder.service.resources + +import javax.servlet.http.HttpServletRequest + +import com.expedia.www.haystack.service.graph.graph.builder.config.entities.ServiceConfiguration +import com.expedia.www.haystack.service.graph.graph.builder.model.{ServiceGraph, ServiceGraphEdge} +import com.expedia.www.haystack.service.graph.graph.builder.service.fetchers.{LocalServiceEdgesFetcher, RemoteServiceEdgesFetcher} +import com.expedia.www.haystack.service.graph.graph.builder.service.utils.EdgesMerger._ +import com.expedia.www.haystack.service.graph.graph.builder.service.utils.QueryTimestampReader +import org.apache.kafka.streams.KafkaStreams +import org.slf4j.LoggerFactory + +import scala.collection.JavaConverters._ +import scala.collection.mutable +import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.duration._ +import scala.concurrent.{Await, Future} + +class GlobalServiceGraphResource(streams: KafkaStreams, + storeName: String, + serviceConfig: ServiceConfiguration, + localEdgesFetcher: LocalServiceEdgesFetcher, + remoteEdgesFetcher: RemoteServiceEdgesFetcher) (implicit val timestampReader: QueryTimestampReader) + extends Resource("servicegraph") { + private val LOGGER = LoggerFactory.getLogger(classOf[LocalServiceGraphResource]) + private val globalEdgeCount = metricRegistry.histogram("servicegraph.global.edges") + + protected override def get(request: HttpServletRequest): ServiceGraph = { + val from = timestampReader.fromTimestamp(request) + val to = timestampReader.toTimestamp(request) + + // get list of all hosts containing service-graph store + // fetch local service graphs from all hosts + // and merge local graphs to create global graph + val edgesListFuture: Iterable[Future[Seq[ServiceGraphEdge]]] = streams + .allMetadataForStore(storeName) + .asScala + .map(host => { + if (host.host() == serviceConfig.host) { + LOGGER.info(s"service graph from local invoked") + Future(localEdgesFetcher.fetchEdges(from, to)) + } + else { + LOGGER.info(s"service graph from ${host.host()} for edges is invoked") + remoteEdgesFetcher.fetchEdges(host.host(), host.port(), from, to) + } + }) + + val singleResultFuture = Future.sequence(edgesListFuture) + val edges = Await + .result(singleResultFuture, serviceConfig.client.socketTimeout.millis) + .foldLeft(mutable.ListBuffer[ServiceGraphEdge]())((buffer, coll) => buffer ++= coll) + + val mergedEdgeList = getMergedServiceEdges(edges) + globalEdgeCount.update(mergedEdgeList.length) + ServiceGraph(mergedEdgeList) + } +} diff --git a/service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/service/resources/IsWorkingResource.scala b/service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/service/resources/IsWorkingResource.scala new file mode 100644 index 000000000..5086e253a --- /dev/null +++ b/service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/service/resources/IsWorkingResource.scala @@ -0,0 +1,27 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.service.graph.graph.builder.service.resources + +import javax.servlet.http.HttpServletRequest + +class IsWorkingResource() extends Resource("isworking") { + + protected override def get(request: HttpServletRequest): IsWorking = new IsWorking() + + class IsWorking(isWorking: Boolean = true) +} diff --git a/service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/service/resources/LocalOperationGraphResource.scala b/service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/service/resources/LocalOperationGraphResource.scala new file mode 100644 index 000000000..304cfebf9 --- /dev/null +++ b/service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/service/resources/LocalOperationGraphResource.scala @@ -0,0 +1,38 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.service.graph.graph.builder.service.resources + +import javax.servlet.http.HttpServletRequest + +import com.expedia.www.haystack.service.graph.graph.builder.model.OperationGraph +import com.expedia.www.haystack.service.graph.graph.builder.service.fetchers.{LocalOperationEdgesFetcher, LocalServiceEdgesFetcher} +import com.expedia.www.haystack.service.graph.graph.builder.service.utils.QueryTimestampReader + +class LocalOperationGraphResource(localEdgesFetcher: LocalOperationEdgesFetcher) + (implicit val timestampReader: QueryTimestampReader) extends Resource("operationgraph.local") { + private val edgeCount = metricRegistry.histogram("operationgraph.local.edges") + + protected override def get(request: HttpServletRequest): OperationGraph = { + val from = timestampReader.fromTimestamp(request) + val to = timestampReader.toTimestamp(request) + + val localGraph = OperationGraph(localEdgesFetcher.fetchEdges(from, to)) + edgeCount.update(localGraph.edges.length) + localGraph + } +} diff --git a/service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/service/resources/LocalServiceGraphResource.scala b/service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/service/resources/LocalServiceGraphResource.scala new file mode 100644 index 000000000..56c5c68c1 --- /dev/null +++ b/service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/service/resources/LocalServiceGraphResource.scala @@ -0,0 +1,39 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.service.graph.graph.builder.service.resources + +import javax.servlet.http.HttpServletRequest + +import com.expedia.www.haystack.service.graph.graph.builder.model.ServiceGraph +import com.expedia.www.haystack.service.graph.graph.builder.service.fetchers.LocalServiceEdgesFetcher +import com.expedia.www.haystack.service.graph.graph.builder.service.utils.QueryTimestampReader + +class LocalServiceGraphResource(localEdgesFetcher: LocalServiceEdgesFetcher)(implicit val timestampReader: QueryTimestampReader) + extends Resource("servicegraph.local") { + + private val edgeCount = metricRegistry.histogram("servicegraph.local.edges") + + protected override def get(request: HttpServletRequest): ServiceGraph = { + val from = timestampReader.fromTimestamp(request) + val to = timestampReader.toTimestamp(request) + + val localGraph = ServiceGraph(localEdgesFetcher.fetchEdges(from, to)) + edgeCount.update(localGraph.edges.length) + localGraph + } +} diff --git a/service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/service/resources/Resource.scala b/service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/service/resources/Resource.scala new file mode 100644 index 000000000..d7327c5c7 --- /dev/null +++ b/service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/service/resources/Resource.scala @@ -0,0 +1,63 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.service.graph.graph.builder.service.resources + +import javax.servlet.http.{HttpServlet, HttpServletRequest, HttpServletResponse} + +import com.expedia.www.haystack.commons.metrics.MetricsSupport +import org.apache.http.entity.ContentType +import org.json4s.DefaultFormats +import org.json4s.jackson.Serialization +import org.slf4j.LoggerFactory + +import scala.util.{Failure, Success, Try} + +abstract class Resource(endpointName: String) extends HttpServlet with MetricsSupport { + private val LOGGER = LoggerFactory.getLogger(classOf[Resource]) + private val timer = metricRegistry.timer(endpointName) + private val failureCount = metricRegistry.meter(s"$endpointName.failure") + + implicit val formats = DefaultFormats + + protected override def doGet(request: HttpServletRequest, response: HttpServletResponse): Unit = { + val time = timer.time() + + Try(get(request)) match { + case Success(getResponse) => + response.setContentType(ContentType.APPLICATION_JSON.getMimeType) + response.setStatus(HttpServletResponse.SC_OK) + response.getWriter.print(Serialization.write(getResponse)) + LOGGER.info(s"accesslog: ${request.getRequestURI} completed successfully") + + case Failure(ex) => + response.setContentType(ContentType.APPLICATION_JSON.getMimeType) + response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR) + response.getWriter.print(Serialization.write(new Error(ex.getMessage))) + failureCount.mark() + LOGGER.error(s"accesslog: ${request.getRequestURI} failed", ex) + } + + response.getWriter.flush() + time.stop() + } + + // endpoint method for child resources to inherit + protected def get(request: HttpServletRequest): Object + + class Error(message: String, error: Boolean = true) +} diff --git a/service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/service/utils/EdgesMerger.scala b/service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/service/utils/EdgesMerger.scala new file mode 100644 index 000000000..d3bf59f0e --- /dev/null +++ b/service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/service/utils/EdgesMerger.scala @@ -0,0 +1,54 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.service.graph.graph.builder.service.utils + +import com.expedia.www.haystack.service.graph.graph.builder.model.{EdgeStats, OperationGraphEdge, ServiceGraphEdge} + +object EdgesMerger { + def getMergedServiceEdges(serviceGraphEdges: Seq[ServiceGraphEdge]): Seq[ServiceGraphEdge] = { + // group by source and destination service + val groupedEdges = serviceGraphEdges.groupBy(edge => ServicePair(edge.source.name, edge.destination.name)) + + // go through edges grouped by source and destination + // add counts for all edges in group to get total count for a source destination pair + // get latest last seen for all edges in group to last seen for a source destination pair + groupedEdges.map { + case (_, edge) => edge.reduce((e1, e2) => e1 + e2) + }.toSeq + } + + def getMergedOperationEdges(operationGraphEdges: Seq[OperationGraphEdge]): Seq[OperationGraphEdge] = { + // group by source and destination service + val groupedEdges = operationGraphEdges.groupBy(edge => OperationTrio(edge.source, edge.destination, edge.operation)) + + // go through edges grouped by source and destination + // add counts for all edges in group to get total count for an operation trio + // get latest last seen for all edges in group to last seen for an operation trio + groupedEdges.map( + group => group._2 + .reduce((e1, e2) => OperationGraphEdge(group._1.source, group._1.destination, group._1.operation, + EdgeStats(e1.stats.count + e2.stats.count, Math.max(e1.stats.lastSeen, e2.stats.lastSeen), e1.stats + .errorCount + e2.stats.errorCount), Math.min(e1.effectiveFrom, e2.effectiveFrom), Math.max(e1.effectiveTo, e2.effectiveTo)))) + .toSeq + } + + private case class ServicePair(source: String, destination: String) + + private case class OperationTrio(source: String, destination: String, operation: String) +} diff --git a/service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/service/utils/IOUtils.scala b/service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/service/utils/IOUtils.scala new file mode 100644 index 000000000..2c1fe19c3 --- /dev/null +++ b/service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/service/utils/IOUtils.scala @@ -0,0 +1,35 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.service.graph.graph.builder.service.utils + +import java.io.Closeable + +import org.slf4j.{Logger, LoggerFactory} + +object IOUtils { + protected val LOGGER: Logger = LoggerFactory.getLogger(IOUtils.getClass) + + def closeSafely(resource: Closeable): Unit = { + try { + if (resource != null) resource.close() + } catch { + case ex: Exception => LOGGER.error(s"Fail to close the resource with error", ex) + } + } +} diff --git a/service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/service/utils/QueryTimestampReader.scala b/service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/service/utils/QueryTimestampReader.scala new file mode 100644 index 000000000..4f779afb3 --- /dev/null +++ b/service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/service/utils/QueryTimestampReader.scala @@ -0,0 +1,54 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.service.graph.graph.builder.service.utils + +import java.time.Instant +import java.time.temporal.ChronoUnit +import javax.servlet.http.HttpServletRequest + +import org.apache.commons.lang3.StringUtils + +class QueryTimestampReader(aggregateWindowSec: Long) { + + def toTimestamp(request: HttpServletRequest): Long = { + if (StringUtils.isEmpty(request.getParameter("to"))) { + Instant.now().toEpochMilli + } else { + extractTime(request, "to") + } + } + + def fromTimestamp(request: HttpServletRequest): Long = { + val timestamp = if (StringUtils.isEmpty(request.getParameter("from"))) { + Instant.now().minus(24, ChronoUnit.HOURS).toEpochMilli + } else { + extractTime(request, "from") + } + adjustTimeWithAggregateWindow(timestamp) + + } + + private def extractTime(request: HttpServletRequest, key: String): Long = { + request.getParameter(key).toLong + } + + private def adjustTimeWithAggregateWindow(epochMillis: Long): Long = { + val result = Math.floor(epochMillis / (aggregateWindowSec * 1000)).toLong + result * aggregateWindowSec * 1000 + } +} diff --git a/service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/stream/ServiceGraphStreamSupplier.scala b/service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/stream/ServiceGraphStreamSupplier.scala new file mode 100644 index 000000000..ce5105556 --- /dev/null +++ b/service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/stream/ServiceGraphStreamSupplier.scala @@ -0,0 +1,84 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.service.graph.graph.builder.stream + +import java.util.concurrent.TimeUnit +import java.util.function.Supplier + +import com.expedia.www.haystack.commons.entities.GraphEdge +import com.expedia.www.haystack.commons.kstreams.GraphEdgeTimestampExtractor +import com.expedia.www.haystack.commons.kstreams.serde.graph.{GraphEdgeKeySerde, GraphEdgeValueSerde} +import com.expedia.www.haystack.service.graph.graph.builder.config.entities.KafkaConfiguration +import com.expedia.www.haystack.service.graph.graph.builder.model.{EdgeStats, EdgeStatsSerde} +import org.apache.kafka.streams.kstream._ +import org.apache.kafka.streams.{Consumed, StreamsBuilder, Topology} + +class ServiceGraphStreamSupplier(kafkaConfiguration: KafkaConfiguration) extends Supplier[Topology] { + override def get(): Topology = initialize(new StreamsBuilder) + + private def tumblingWindow(): TimeWindows = { + TimeWindows + .of(TimeUnit.SECONDS.toMillis(kafkaConfiguration.aggregationWindowSec)) + .until(TimeUnit.DAYS.toMillis(kafkaConfiguration.aggregationRetentionDays)) + } + + private def initialize(builder: StreamsBuilder): Topology = { + + val initializer: Initializer[EdgeStats] = () => EdgeStats(0, 0, 0) + + val aggregator: Aggregator[GraphEdge, GraphEdge, EdgeStats] = { + (_: GraphEdge, v: GraphEdge, stats: EdgeStats) => stats.update(v) + } + + builder + // + // read edges from graph-nodes topic + // graphEdge is both the key and value + // use graph edge timestamp + .stream( + kafkaConfiguration.consumerTopic, + Consumed.`with`( + new GraphEdgeKeySerde, + new GraphEdgeValueSerde, + new GraphEdgeTimestampExtractor, + kafkaConfiguration.autoOffsetReset + ) + ) + // + // group by key for doing aggregations on edges + // this will not cause any repartition + .groupByKey( + Serialized.`with`(new GraphEdgeKeySerde, new GraphEdgeValueSerde) + ) + // + // create tumbling windows for edges + .windowedBy(tumblingWindow()).aggregate( + initializer, + // calculate stats for edges + // keep the resulting ktable as materialized view in memory + // enabled logging to persist ktable changelog topic and replicated to multiple brokers + aggregator, Materialized.as(kafkaConfiguration + .producerTopic) + .withKeySerde(new GraphEdgeKeySerde) + .withValueSerde(new EdgeStatsSerde) + .withCachingEnabled()) + + // build stream topology and return + builder.build() + } +} \ No newline at end of file diff --git a/service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/stream/StreamSupplier.scala b/service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/stream/StreamSupplier.scala new file mode 100644 index 000000000..966e0e22e --- /dev/null +++ b/service-graph/graph-builder/src/main/scala/com.expedia.www.haystack.service.graph.graph.builder/stream/StreamSupplier.scala @@ -0,0 +1,107 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.service.graph.graph.builder.stream + +import java.util.Properties +import java.util.concurrent.TimeUnit +import java.util.function.Supplier + +import com.expedia.www.haystack.commons.health.HealthStatusController +import com.expedia.www.haystack.commons.kstreams.app.StateChangeListener +import org.apache.kafka.clients.admin.AdminClient +import org.apache.kafka.streams.{KafkaStreams, StreamsConfig, Topology} +import org.slf4j.LoggerFactory + +import scala.util.Try + +/** + * Factory class to create a KafkaStreams instance and wrap it as a simple service {@see ManagedKafkaStreams} + * + * Optionally this class can check the presence of consuming topic + * + * @param topologySupplier A supplier that creates and returns a Kafka Stream Topology + * @param healthController health controller + * @param streamsConfig Configuration instance for KafkaStreams + * @param consumerTopic Optional consuming topic name + */ +//noinspection ScalaDocInlinedTag,ScalaDocParserErrorInspection +class StreamSupplier(topologySupplier: Supplier[Topology], + healthController: HealthStatusController, + streamsConfig: StreamsConfig, + consumerTopic: String, + var adminClient: AdminClient = null) extends Supplier[KafkaStreams] { + + require(topologySupplier != null, "streamsBuilder is required") + require(healthController != null, "healthStatusController is required") + require(streamsConfig != null, "streamsConfig is required") + require(consumerTopic != null && !consumerTopic.isEmpty, "consumerTopic is required") + if(adminClient == null) { + adminClient = AdminClient.create(getBootstrapProperties) + } + + private val LOGGER = LoggerFactory.getLogger(classOf[StreamSupplier]) + + /** + * creates a new instance of KafkaStreams application wrapped as a {@link ManagedService} instance + * + * @return instance of ManagedService + */ + override def get(): KafkaStreams = { + checkConsumerTopic() + + val listener = new StateChangeListener(healthController) + val streams = new KafkaStreams(topologySupplier.get(), streamsConfig) + streams.setStateListener(listener) + streams.setUncaughtExceptionHandler(listener) + streams.cleanUp() + + streams + } + + private def checkConsumerTopic(): Unit = { + LOGGER.info(s"checking for the consumer topic $consumerTopic") + try { + val present = adminClient.listTopics().names().get().contains(consumerTopic) + if (!present) { + throw new TopicNotPresentException(consumerTopic, + s"Topic '$consumerTopic' is configured as a consumer and it is not present") + } + } + finally { + Try(adminClient.close(5, TimeUnit.SECONDS)) + } + } + + private def getBootstrapProperties: Properties = { + val properties = new Properties() + properties.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, streamsConfig.getList(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG)) + properties + } + + /** + * Custom RuntimeException that represents a required Kafka topic not being present + * + * @param topic Name of the topic that is missing + * @param message Message + */ + class TopicNotPresentException(topic: String, message: String) extends RuntimeException(message) { + def getTopic: String = topic + } +} + diff --git a/service-graph/graph-builder/src/test/java/org/expedia/www/haystack/commons/scalatest/IntegrationSuite.java b/service-graph/graph-builder/src/test/java/org/expedia/www/haystack/commons/scalatest/IntegrationSuite.java new file mode 100644 index 000000000..fe13e09e4 --- /dev/null +++ b/service-graph/graph-builder/src/test/java/org/expedia/www/haystack/commons/scalatest/IntegrationSuite.java @@ -0,0 +1,29 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package org.expedia.www.haystack.commons.scalatest; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@org.scalatest.TagAnnotation +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD, ElementType.TYPE}) +public @interface IntegrationSuite { +} diff --git a/service-graph/graph-builder/src/test/resources/integration/kafka-server.properties b/service-graph/graph-builder/src/test/resources/integration/kafka-server.properties new file mode 100644 index 000000000..860ae817c --- /dev/null +++ b/service-graph/graph-builder/src/test/resources/integration/kafka-server.properties @@ -0,0 +1,51 @@ +# The id of the broker. This must be set to a unique integer for each broker. +broker.id=0 + +# The port the socket server listens on +port=9092 + +# The number of threads handling network requests +num.network.threads=2 + +# The number of threads doing disk I/O +num.io.threads=8 + +# The send buffer (SO_SNDBUF) used by the socket server +socket.send.buffer.bytes=1048576 + +# The receive buffer (SO_RCVBUF) used by the socket server +socket.receive.buffer.bytes=1048576 + +# The maximum size of a request that the socket server will accept (protection against OOM) +socket.request.max.bytes=104857600 + +# A comma seperated list of directories under which to store log files +log.dirs=target/kafka-logs + +# The default number of log partitions per topic. More partitions allow greater +# parallelism for consumption, but this will also result in more files across +# the brokers. +num.partitions=1 + +# The minimum age of a log file to be eligible for deletion +log.retention.hours=168 + +# The maximum size of a log segment file. When this size is reached a new log segment will be created. +log.segment.bytes=536870912 + +# The interval at which log segments are checked to see if they can be deleted according +# to the retention policies +log.retention.check.interval.ms=60000 + +# By default the log cleaner is disabled and the log retention policy will default to just delete segments after their retention expires. +# If log.cleaner.enable=true is set the cleaner will be enabled and individual logs can then be marked for log compaction. +log.cleaner.enable=false + +# Timeout in ms for connecting to zookeeper +zookeeper.connection.timeout.ms=1000000 + +#auto create topics +auto.create.topics.enable=true + +default.replication.factor=1 +offsets.topic.replication.factor=1 \ No newline at end of file diff --git a/service-graph/graph-builder/src/test/resources/integration/local.conf b/service-graph/graph-builder/src/test/resources/integration/local.conf new file mode 100644 index 000000000..057e156fb --- /dev/null +++ b/service-graph/graph-builder/src/test/resources/integration/local.conf @@ -0,0 +1,60 @@ +health.status.path = "/app/isHealthy" + +kafka { + close.timeout.ms = 30000 + + streams { + application.id = "haystack-service-graph-graph-builder" + bootstrap.servers = "localhost:9092" + num.stream.threads = 4 + request.timeout.ms = 60000 + commit.interval.ms = 3000 + auto.offset.reset = latest + timestamp.extractor = "org.apache.kafka.streams.processor.WallclockTimestampExtractor" + } + + rocksdb { + block.cache.size = 16777216 + block.size = 16384 + cache.index.and.filter.blocks = true + max.write.buffer.number = 2 + } + + consumer { + topic = "graph-nodes" + } + + producer { + topic = "service-graph" + config = { + cleanup.policy = "compact" + } + } + + aggregate { + window.sec = 300 + retention.days = 3 + } +} + +service { + host = "localhost" + + threads { + min = 5 + max = 10 + idle.timeout = 12000 + } + + http { + port = 8080 + idle.timeout = 12000 + } + + client { + connection.timeout = 1000 + socket.timeout = 1000 + } +} + +haystack.graphite.host = "monitoring-influxdb-graphite.kube-system.svc" diff --git a/service-graph/graph-builder/src/test/resources/integration/zookeeper.properties b/service-graph/graph-builder/src/test/resources/integration/zookeeper.properties new file mode 100644 index 000000000..75e8c6506 --- /dev/null +++ b/service-graph/graph-builder/src/test/resources/integration/zookeeper.properties @@ -0,0 +1,6 @@ +# the directory where the snapshot is stored. +dataDir=target/zookeeper +# the port at which the clients will connect +clientPort=2181 +# disable the per-ip limit on the number of connections since this is a non-production config +maxClientCnxns=0 \ No newline at end of file diff --git a/service-graph/graph-builder/src/test/resources/log4j.properties b/service-graph/graph-builder/src/test/resources/log4j.properties new file mode 100644 index 000000000..fa7f75bf8 --- /dev/null +++ b/service-graph/graph-builder/src/test/resources/log4j.properties @@ -0,0 +1,5 @@ +log4j.rootLogger=OFF, stdout + +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=[%d] %p %m (%c)%n \ No newline at end of file diff --git a/service-graph/graph-builder/src/test/resources/logback-test.xml b/service-graph/graph-builder/src/test/resources/logback-test.xml new file mode 100644 index 000000000..f7171463c --- /dev/null +++ b/service-graph/graph-builder/src/test/resources/logback-test.xml @@ -0,0 +1,24 @@ + + + + + + + true + + + + + + %d{yyyy-MM-dd HH:mm:ss:SSS} %thread, %level, %logger{70}, "%msg" %replace(%ex){'[\n]+', '\\n'}%nopex%n + + + + + + + + + + + diff --git a/service-graph/graph-builder/src/test/resources/logback.xml b/service-graph/graph-builder/src/test/resources/logback.xml new file mode 100644 index 000000000..c7d7bf222 --- /dev/null +++ b/service-graph/graph-builder/src/test/resources/logback.xml @@ -0,0 +1,35 @@ + + + + + + + true + + + + + + %d{yyyy-MM-dd HH:mm:ss:SSS} %thread, %level, %logger{70}, "%msg" %replace(%ex){'[\n]+', '\\n'}%nopex%n + + + + + + ${HAYSTACK_LOG_QUEUE_SIZE:-500} + ${HAYSTACK_LOG_DISCARD_THRESHOLD:-0} + + + + + ${HAYSTACK_GRAPHITE_HOST:-monitoring-influxdb-graphite.kube-system.svc} + ${HAYSTACK_GRAPHITE_PORT:-2003} + service-graph-node-finder + + + + + + + diff --git a/service-graph/graph-builder/src/test/resources/test/test.conf b/service-graph/graph-builder/src/test/resources/test/test.conf new file mode 100644 index 000000000..b2312f0f4 --- /dev/null +++ b/service-graph/graph-builder/src/test/resources/test/test.conf @@ -0,0 +1,56 @@ +health.status.path = "/app/isHealthy" + +kafka { + close.timeout.ms = 30000 + + streams { + application.id = "haystack-service-graph-graph-builder" + bootstrap.servers = "localhost:9092" + num.stream.threads = 4 + request.timeout.ms = 60000 + commit.interval.ms = 3000 + auto.offset.reset = latest + timestamp.extractor = "org.apache.kafka.streams.processor.WallclockTimestampExtractor" + } + + rocksdb { + block.cache.size = 16777216 + block.size = 16384 + cache.index.and.filter.blocks = true + max.write.buffer.number = 2 + } + + consumer { + topic = "graph-nodes" + } + + producer { + topic = "service-graph" + } + + aggregate { + window.sec = 3600 + retention.days = 3 + } +} + +service { + host = "localhost" + threads { + min = 1 + max = 5 + idle.timeout = 12000 + } + + http { + port = 8080 + idle.timeout = 12000 + } + + client { + connection.timeout = 1000 + socket.timeout = 1000 + } +} + +haystack.graphite.host = "monitoring-influxdb-graphite.kube-system.svc" diff --git a/service-graph/graph-builder/src/test/resources/test/test_application_server_set.conf b/service-graph/graph-builder/src/test/resources/test/test_application_server_set.conf new file mode 100644 index 000000000..25bd919c5 --- /dev/null +++ b/service-graph/graph-builder/src/test/resources/test/test_application_server_set.conf @@ -0,0 +1,57 @@ +health.status.path = "/app/isHealthy" + +kafka { + close.timeout.ms = 30000 + + streams { + application.id = "haystack-service-graph-graph-builder" + application.server = "127.0.0.1:1002" + bootstrap.servers = "localhost:9092" + num.stream.threads = 4 + request.timeout.ms = 60000 + commit.interval.ms = 3000 + auto.offset.reset = latest + timestamp.extractor = "org.apache.kafka.streams.processor.WallclockTimestampExtractor" + } + + rocksdb { + block.cache.size = 16777216 + block.size = 16384 + cache.index.and.filter.blocks = true + max.write.buffer.number = 2 + } + + consumer { + topic = "graph-nodes" + } + + producer { + topic = "service-graph" + } + + aggregate { + window.sec = 3600 + retention.days = 3 + } +} + +service { + host = "localhost" + threads { + min = 1 + max = 5 + idle.timeout = 12000 + } + + http { + port = 8080 + idle.timeout = 12000 + } + + client { + connection.timeout = 1000 + socket.timeout = 1000 + } +} + +haystack.graphite.host = "monitoring-influxdb-graphite.kube-system.svc" diff --git a/service-graph/graph-builder/src/test/resources/test/test_no_app_id.conf b/service-graph/graph-builder/src/test/resources/test/test_no_app_id.conf new file mode 100644 index 000000000..0968c2011 --- /dev/null +++ b/service-graph/graph-builder/src/test/resources/test/test_no_app_id.conf @@ -0,0 +1,55 @@ +health.status.path = "/app/isHealthy" + +kafka { + close.timeout.ms = 30000 + + streams { + bootstrap.servers = "localhost:9092" + num.stream.threads = 4 + request.timeout.ms = 60000 + commit.interval.ms = 3000 + auto.offset.reset = latest + timestamp.extractor = "org.apache.kafka.streams.processor.WallclockTimestampExtractor" + } + + rocksdb { + block.cache.size = 16777216 + block.size = 16384 + cache.index.and.filter.blocks = true + max.write.buffer.number = 2 + } + + consumer { + topic = "graph-nodes" + } + + producer { + topic = "service-graph" + } + + aggregate { + window.sec = 3600 + retention.days = 3 + } +} + +service { + host = "localhost" + threads { + min = 1 + max = 5 + idle.timeout = 12000 + } + + http { + port = 8080 + idle.timeout = 12000 + } + + client { + connection.timeout = 1000 + socket.timeout = 1000 + } +} + +haystack.graphite.host = "monitoring-influxdb-graphite.kube-system.svc" diff --git a/service-graph/graph-builder/src/test/resources/test/test_no_bootstrap.conf b/service-graph/graph-builder/src/test/resources/test/test_no_bootstrap.conf new file mode 100644 index 000000000..e25cb39f8 --- /dev/null +++ b/service-graph/graph-builder/src/test/resources/test/test_no_bootstrap.conf @@ -0,0 +1,55 @@ +health.status.path = "/app/isHealthy" + +kafka { + close.timeout.ms = 30000 + + streams { + application.id = "haystack-service-graph-graph-builder" + num.stream.threads = 4 + request.timeout.ms = 60000 + commit.interval.ms = 3000 + auto.offset.reset = latest + timestamp.extractor = "org.apache.kafka.streams.processor.WallclockTimestampExtractor" + } + + rocksdb { + block.cache.size = 16777216 + block.size = 16384 + cache.index.and.filter.blocks = true + max.write.buffer.number = 2 + } + + consumer { + topic = "graph-nodes" + } + + producer { + topic = "service-graph" + } + + aggregate { + window.sec = 3600 + retention.days = 3 + } +} + +service { + host = "localhost" + threads { + min = 1 + max = 5 + idle.timeout = 12000 + } + + http { + port = 8080 + idle.timeout = 12000 + } + + client { + connection.timeout = 1000 + socket.timeout = 1000 + } +} + +haystack.graphite.host = "monitoring-influxdb-graphite.kube-system.svc" diff --git a/service-graph/graph-builder/src/test/resources/test/test_no_consumer.conf b/service-graph/graph-builder/src/test/resources/test/test_no_consumer.conf new file mode 100644 index 000000000..09344d81f --- /dev/null +++ b/service-graph/graph-builder/src/test/resources/test/test_no_consumer.conf @@ -0,0 +1,52 @@ +health.status.path = "/app/isHealthy" + +kafka { + close.timeout.ms = 30000 + + streams { + application.id = "haystack-service-graph-graph-builder" + bootstrap.servers = "localhost:9092" + num.stream.threads = 4 + request.timeout.ms = 60000 + commit.interval.ms = 3000 + auto.offset.reset = latest + timestamp.extractor = "org.apache.kafka.streams.processor.WallclockTimestampExtractor" + } + + rocksdb { + block.cache.size = 16777216 + block.size = 16384 + cache.index.and.filter.blocks = true + max.write.buffer.number = 2 + } + + producer { + topic = "service-graph" + } + + aggregate { + window.sec = 3600 + retention.days = 3 + } +} + +service { + host = "localhost" + threads { + min = 1 + max = 5 + idle.timeout = 12000 + } + + http { + port = 8080 + idle.timeout = 12000 + } + + client { + connection.timeout = 1000 + socket.timeout = 1000 + } +} + +haystack.graphite.host = "monitoring-influxdb-graphite.kube-system.svc" diff --git a/service-graph/graph-builder/src/test/resources/test/test_no_producer.conf b/service-graph/graph-builder/src/test/resources/test/test_no_producer.conf new file mode 100644 index 000000000..9389fb361 --- /dev/null +++ b/service-graph/graph-builder/src/test/resources/test/test_no_producer.conf @@ -0,0 +1,53 @@ +health.status.path = "/app/isHealthy" + +kafka { + close.timeout.ms = 30000 + + streams { + application.id = "haystack-service-graph-graph-builder" + bootstrap.servers = "localhost:9092" + num.stream.threads = 4 + request.timeout.ms = 60000 + commit.interval.ms = 3000 + auto.offset.reset = latest + timestamp.extractor = "org.apache.kafka.streams.processor.WallclockTimestampExtractor" + } + + rocksdb { + block.cache.size = 16777216 + block.size = 16384 + cache.index.and.filter.blocks = true + max.write.buffer.number = 2 + } + + consumer { + topic = "graph-nodes" + } + + aggregate { + window.sec = 3600 + retention.days = 3 + } +} + +service { + host = "localhost" + + threads { + min = 1 + max = 5 + idle.timeout = 12000 + } + + http { + port = 8080 + idle.timeout = 12000 + } + + client { + connection.timeout = 1000 + socket.timeout = 1000 + } +} + +haystack.graphite.host = "monitoring-influxdb-graphite.kube-system.svc" diff --git a/service-graph/graph-builder/src/test/scala/com/expedia/www/haystack/service/graph/graph/builder/AppSpec.scala b/service-graph/graph-builder/src/test/scala/com/expedia/www/haystack/service/graph/graph/builder/AppSpec.scala new file mode 100644 index 000000000..bd5d9b2e0 --- /dev/null +++ b/service-graph/graph-builder/src/test/scala/com/expedia/www/haystack/service/graph/graph/builder/AppSpec.scala @@ -0,0 +1,249 @@ +package com.expedia.www.haystack.service.graph.graph.builder + +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import java.util.Properties + +import com.expedia.www.haystack.commons.entities.{GraphEdge, GraphVertex, TagKeys} +import com.expedia.www.haystack.commons.health.HealthStatusController +import com.expedia.www.haystack.commons.kstreams.serde.graph.{GraphEdgeKeySerde, GraphEdgeValueSerde} +import com.expedia.www.haystack.service.graph.graph.builder.config.AppConfiguration +import com.expedia.www.haystack.service.graph.graph.builder.kafka.KafkaController +import com.expedia.www.haystack.service.graph.graph.builder.model.{EdgeStats, OperationGraph, ServiceGraph} +import com.expedia.www.haystack.service.graph.graph.builder.service.HttpService +import org.apache.http.client.fluent.Request +import org.apache.kafka.clients.producer.{KafkaProducer, ProducerRecord} +import org.apache.kafka.streams.KafkaStreams +import org.apache.kafka.streams.state.{QueryableStoreTypes, ReadOnlyWindowStore} +import org.expedia.www.haystack.commons.scalatest.IntegrationSuite +import org.json4s.DefaultFormats +import org.json4s.jackson.Serialization +import org.scalatest.BeforeAndAfterAll + +import scala.collection.JavaConverters._ +import scala.util.Random + +@IntegrationSuite +class AppSpec extends TestSpec with BeforeAndAfterAll { + + val kafkaController: KafkaController = createKafkaController() + private val appConfig = new AppConfiguration("integration/local.conf") + var stream: KafkaStreams = _ + var service: HttpService = _ + + implicit val formats = DefaultFormats + + override def beforeAll { + //start kafka and zk + kafkaController.startService() + + //ensure test topics are present + kafkaController.createTopics(List(appConfig.kafkaConfig.consumerTopic)) + + //start topology + stream = App.createStream(appConfig.kafkaConfig, new HealthStatusController) + stream.start() + + //start service + service = App.createService(appConfig.serviceConfig, stream, appConfig.kafkaConfig) + service.start() + + //time for kstreams to initialize completely + Thread.sleep(20000) + } + + describe("graph-builder application") { + it("should add new edges in ktable") { + Given("running stream topology") + + When("getting new edges") + //send test data to source topic + val producer = kafkaController.createProducer( + appConfig.kafkaConfig.consumerTopic, + new GraphEdgeKeySerde().serializer(), new GraphEdgeValueSerde().serializer() + ) + + val random = new Random + val source = random.nextString(4) + val destination = random.nextString(4) + val operation = random.nextString(4) + val time = System.currentTimeMillis() + + //send sample data + produceRecord(producer, source, destination, operation, time) + + Then("edges should be added to edges ktable") + //read data from ktable to validate + val store: ReadOnlyWindowStore[GraphEdge, EdgeStats] = + stream.store(appConfig.kafkaConfig.producerTopic, QueryableStoreTypes.windowStore[GraphEdge, EdgeStats]()) + + val storeIterator = store.all() + val filteredEdges = storeIterator.asScala.toList.filter( + edge => { + val gEdge = edge.key.key + gEdge.source == GraphVertex(source) && gEdge.destination == GraphVertex(destination) && gEdge.operation == operation && gEdge.sourceTimestamp == 0 + }) + + filteredEdges.length should be(1) + filteredEdges.head.value.count should be(1) + } + + it("should add only one row for duplicate edges in ktable") { + Given("running stream topology") + + When("getting duplicate edges") + //send test data to source topic + val producer = kafkaController.createProducer( + appConfig.kafkaConfig.consumerTopic, + new GraphEdgeKeySerde().serializer(), new GraphEdgeValueSerde().serializer()) + + val random = new Random + val source = random.nextString(4) + val destination = random.nextString(4) + val operation = random.nextString(4) + val time = System.currentTimeMillis() + + //send sample data + produceDuplicateRecord(producer, 3, source, destination, operation, time) + + Then("only one edge should be added to edges ktable") + //read data from ktable to validate + val store: ReadOnlyWindowStore[GraphEdge, EdgeStats] = + stream.store(appConfig.kafkaConfig.producerTopic, QueryableStoreTypes.windowStore[GraphEdge, EdgeStats]()) + + val storeIterator = store.all() + val filteredEdges = storeIterator.asScala.toList.filter( + edge => { + val gEdge = edge.key.key + gEdge.source == GraphVertex(source) && gEdge.destination == GraphVertex(destination) && gEdge.operation == operation && gEdge.sourceTimestamp == 0 + }) + + filteredEdges.length should be(1) + filteredEdges.head.value.count should be(3) + } + + it("should make servicegraph queriable through http") { + Given("running stream topology") + + When("getting new edge") + //send test data to source topic + val producer = kafkaController.createProducer( + appConfig.kafkaConfig.consumerTopic, + new GraphEdgeKeySerde().serializer(), new GraphEdgeValueSerde().serializer()) + val random = new Random + val source = random.nextInt().toString + val destination = random.nextInt().toString + val operation = random.nextString(4) + val time = System.currentTimeMillis() + + //send sample data + produceRecord(producer, source, destination, operation, time, Map("tag1" -> "testtagval1", TagKeys.ERROR_KEY -> "true")) + + Then("servicegraph endpoint should return the new edge") + val edgeJson = Request + .Get(s"http://localhost:${appConfig.serviceConfig.http.port}/servicegraph") + .execute() + .returnContent() + .asString() + + val serviceGraph = Serialization.read[ServiceGraph](edgeJson) + val filteredEdges = serviceGraph.edges.filter( + edge => edge.source.name == source && edge.destination.name == destination) + + filteredEdges.length should be(1) + filteredEdges.head.stats.count shouldBe 1 + filteredEdges.head.stats.errorCount shouldBe 1 + filteredEdges.head.source.tags.size should be(1) + filteredEdges.head.source.tags.get("tag1") should be (Some("testtagval1")) + } + + it("should make operationgraph queriable through http") { + Given("running stream topology") + + When("getting new edge") + //send test data to source topic + val producer = kafkaController.createProducer( + appConfig.kafkaConfig.consumerTopic, + new GraphEdgeKeySerde().serializer(), new GraphEdgeValueSerde().serializer()) + val random = new Random + val source = random.nextInt().toString + val destination = random.nextInt().toString + val operation = random.nextInt().toString + val time = System.currentTimeMillis() + + //send sample data + produceRecord(producer, source, destination, operation, time) + + Then("operationgraph endpoint should return the new edge") + val edgeJson = Request + .Get(s"http://localhost:${appConfig.serviceConfig.http.port}/operationgraph") + .execute() + .returnContent() + .asString() + + val operationGraph = Serialization.read[OperationGraph](edgeJson) + val filteredEdges = operationGraph.edges.filter( + edge => edge.source == source && edge.destination == destination && edge.operation == operation) + + filteredEdges.length should be(1) + } + } + + override def afterAll { + //stop service & topology + service.close() + stream.close() + + //stop kafka and zk + kafkaController.stopService() + } + + private def createKafkaController(): KafkaController = { + val zkProperties = new Properties + zkProperties.load(classOf[AppSpec].getClassLoader.getResourceAsStream("integration/zookeeper.properties")) + + val kafkaProperties = new Properties + kafkaProperties.load(classOf[AppSpec].getClassLoader.getResourceAsStream("integration/kafka-server.properties")) + + new KafkaController(kafkaProperties, zkProperties) + } + + private def produceRecord(producer: KafkaProducer[GraphEdge, GraphEdge], source: String, destination: String, + operation: String, time: Long, sourceEdgetags: Map[String, String] = Map()): Unit = { + sendRecord(producer, source, destination, operation, time, sourceEdgetags) + + // flush and sleep for couple of seconds for streams to process + producer.flush() + Thread.sleep(2000) + } + + private def produceDuplicateRecord(producer: KafkaProducer[GraphEdge, GraphEdge], count: Int, source: String, destination: String, operation: String, time: Long): Unit = { + for (i <- 0 until count) sendRecord(producer, source, destination, operation, time) + + // flush and sleep for couple of seconds for streams to process + producer.flush() + Thread.sleep(2000) + } + + private def sendRecord(producer: KafkaProducer[GraphEdge, GraphEdge], source: String, destination: String, + operation: String, time: Long, sourceEdgeTags: Map[String, String] = Map()): Unit = { + val edge = GraphEdge(GraphVertex(source, sourceEdgeTags), GraphVertex(destination), operation, time) + producer.send(new ProducerRecord[GraphEdge, GraphEdge](appConfig.kafkaConfig.consumerTopic, edge, edge)) + } +} diff --git a/service-graph/graph-builder/src/test/scala/com/expedia/www/haystack/service/graph/graph/builder/ManagedApplicationSpec.scala b/service-graph/graph-builder/src/test/scala/com/expedia/www/haystack/service/graph/graph/builder/ManagedApplicationSpec.scala new file mode 100644 index 000000000..e7cdb4280 --- /dev/null +++ b/service-graph/graph-builder/src/test/scala/com/expedia/www/haystack/service/graph/graph/builder/ManagedApplicationSpec.scala @@ -0,0 +1,125 @@ +package com.expedia.www.haystack.service.graph.graph.builder + +import java.security.Permission + +import com.codahale.metrics.JmxReporter +import com.expedia.www.haystack.commons.kstreams.app.ManagedService +import com.expedia.www.haystack.service.graph.graph.builder.ManagedApplication._ +import org.mockito.Mockito.{times, verify, verifyNoMoreInteractions, when} +import org.scalatest.mockito.MockitoSugar +import org.scalatest.{BeforeAndAfterAll, FunSpec, SequentialNestedSuiteExecution} +import org.slf4j.Logger + +sealed case class ExitException(status: Int) extends SecurityException("System.exit() was called") { +} + +sealed class NoExitSecurityManager extends SecurityManager { + override def checkPermission(perm: Permission): Unit = {} + + override def checkPermission(perm: Permission, context: Object): Unit = {} + + override def checkExit(status: Int): Unit = { + super.checkExit(status) + throw ExitException(status) + } +} + +class ManagedApplicationSpec extends FunSpec with MockitoSugar with BeforeAndAfterAll with SequentialNestedSuiteExecution { + + override def beforeAll(): Unit = System.setSecurityManager(new NoExitSecurityManager()) + override def afterAll(): Unit = System.setSecurityManager(null) + + describe("ManagedApplication constructor") { + val (mocks: List[AnyRef], service: ManagedService, stream: ManagedService, jmxReporter: JmxReporter, logger: Logger) = createAndBundleMocks + it ("should throw an IllegalArgumentException if passed a null service") { + assertThrows[IllegalArgumentException] { + new ManagedApplication(null, stream, jmxReporter, logger) + } + } + it ("should throw an IllegalArgumentException if passed a null stream") { + assertThrows[IllegalArgumentException] { + new ManagedApplication(service, null, jmxReporter, logger) + } + } + it ("should throw an IllegalArgumentException if passed a null jmxReporter") { + assertThrows[IllegalArgumentException] { + new ManagedApplication(service, stream, null, logger) + } + } + it ("should throw an IllegalArgumentException if passed a null logger") { + assertThrows[IllegalArgumentException] { + new ManagedApplication(service, stream, jmxReporter, null) + } + } + verifyNoMoreInteractionsForAllMocks(mocks) + } + + describe("ManagedApplication start") { + val (mocks: List[AnyRef], service: ManagedService, stream: ManagedService, jmxReporter: JmxReporter, logger: Logger) = createAndBundleMocks + val managedApplication = new ManagedApplication(service, stream, jmxReporter, logger) + it ("should start all dependencies when called") { + managedApplication.start() + verify(service).start() + verify(logger).info(StartMessage) + verify(stream).start() + verify(logger).info(HttpStartMessage) + verify(jmxReporter).start() + verify(logger).info(StreamStartMessage) + } + it ("should call System.exit() when an exception is thrown") { + when(service.start()).thenThrow(new NullPointerException) + assertThrows[ExitException] { + managedApplication.start() + } + verify(service, times(2)).start() + } + verifyNoMoreInteractionsForAllMocks(mocks) + } + + describe("ManagedApplication stop") { + val (mocks: List[AnyRef], service: ManagedService, stream: ManagedService, jmxReporter: JmxReporter, logger: Logger) = createAndBundleMocks + it ("should stop all dependencies when called") { + val managedApplication = new ManagedApplication(service, stream, jmxReporter, logger) + managedApplication.stop() + verify(logger).info(HttpStopMessage) + verify(service).stop() + verify(logger).info(StreamStopMessage) + verify(stream).stop() + verify(logger).info(JmxReporterStopMessage) + verify(jmxReporter).close() + verify(logger).info(LoggerStopMessage) + } + verifyNoMoreInteractionsForAllMocks(mocks) + } + + private def createMocks(): List[AnyRef] = + { + val service = mock[ManagedService] + val stream = mock[ManagedService] + val jmxReporter = mock[JmxReporter] + val logger = mock[Logger] + List(service, stream, jmxReporter, logger) + } + + private def createAndBundleMocks = { + val mocks = createMocks() + val (service: ManagedService, stream: ManagedService, jmxReporter: JmxReporter, logger: Logger) = bundleMocks(mocks) + (mocks, service, stream, jmxReporter, logger) + } + + private def bundleMocks(mocks: List[AnyRef]) = { + val service = mocks.head.asInstanceOf[ManagedService] + val stream = mocks(1).asInstanceOf[ManagedService] + val jmxReporter = mocks(2).asInstanceOf[JmxReporter] + val logger = mocks(3).asInstanceOf[Logger] + (service, stream, jmxReporter, logger) + } + + private def verifyNoMoreInteractionsForAllMocks(mocks: List[AnyRef]): Unit = { + verifyNoMoreInteractions(mocks.head) + verifyNoMoreInteractions(mocks(1)) + verifyNoMoreInteractions(mocks(2)) + verifyNoMoreInteractions(mocks(3)) + } + +} diff --git a/service-graph/graph-builder/src/test/scala/com/expedia/www/haystack/service/graph/graph/builder/TestSpec.scala b/service-graph/graph-builder/src/test/scala/com/expedia/www/haystack/service/graph/graph/builder/TestSpec.scala new file mode 100644 index 000000000..8f29d6ae0 --- /dev/null +++ b/service-graph/graph-builder/src/test/scala/com/expedia/www/haystack/service/graph/graph/builder/TestSpec.scala @@ -0,0 +1,23 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.service.graph.graph.builder + +import org.scalatest.easymock.EasyMockSugar +import org.scalatest.{FunSpec, GivenWhenThen, Matchers} + +trait TestSpec extends FunSpec with GivenWhenThen with Matchers with EasyMockSugar \ No newline at end of file diff --git a/service-graph/graph-builder/src/test/scala/com/expedia/www/haystack/service/graph/graph/builder/config/AppConfigurationSpec.scala b/service-graph/graph-builder/src/test/scala/com/expedia/www/haystack/service/graph/graph/builder/config/AppConfigurationSpec.scala new file mode 100644 index 000000000..9e23caacf --- /dev/null +++ b/service-graph/graph-builder/src/test/scala/com/expedia/www/haystack/service/graph/graph/builder/config/AppConfigurationSpec.scala @@ -0,0 +1,113 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.service.graph.graph.builder.config + +import com.expedia.www.haystack.service.graph.graph.builder.TestSpec +import com.expedia.www.haystack.service.graph.graph.builder.config.entities.CustomRocksDBConfig +import com.typesafe.config.ConfigException +import org.apache.kafka.streams.StreamsConfig +import org.apache.kafka.streams.processor.WallclockTimestampExtractor +import org.rocksdb.{BlockBasedTableConfig, Options} + +import scala.collection.JavaConverters._ + +class AppConfigurationSpec extends TestSpec { + describe("loading application configuration") { + it("should fail creating KafkaConfiguration if no application id is specified") { + Given("a test configuration file") + val file = "test/test_no_app_id.conf" + + When("Application configuration is loaded") + + Then("it should throw an exception") + intercept[IllegalArgumentException] { + new AppConfiguration(file).kafkaConfig + } + } + + it("should fail creating KafkaConfiguration if no bootstrap is specified") { + Given("a test configuration file") + val file = "test/test_no_bootstrap.conf" + + When("Application configuration is loaded") + + Then("it should throw an exception") + intercept[IllegalArgumentException] { + new AppConfiguration(file).kafkaConfig + } + } + + it("should fail creating KafkaConfiguration if no consumer is specified") { + Given("a test configuration file") + val file = "test/test_no_consumer.conf" + + When("Application configuration is loaded") + + Then("it should throw an exception") + intercept[ConfigException] { + new AppConfiguration(file).kafkaConfig + } + } + + it("should fail creating KafkaConfiguration if no producer is specified") { + Given("a test configuration file") + val file = "test/test_no_producer.conf" + + When("Application configuration is loaded") + + Then("it should throw an exception") + intercept[ConfigException] { + new AppConfiguration(file).kafkaConfig + } + } + + it("should create KafkaConfiguration and ServiceConfiguration as specified") { + Given("a test configuration file") + val file = "test/test.conf" + + When("Application configuration is loaded and KafkaConfiguration is obtained") + val config = new AppConfiguration(file) + + Then("it should load as expected") + config.kafkaConfig.streamsConfig.defaultTimestampExtractor() shouldBe a [WallclockTimestampExtractor] + config.kafkaConfig.consumerTopic should be ("graph-nodes") + config.serviceConfig.http.port should be (8080) + config.serviceConfig.threads.max should be(5) + config.serviceConfig.client.connectionTimeout should be(1000) + val rocksDbOptions = new Options() + new CustomRocksDBConfig().setConfig("", rocksDbOptions, Map[String, AnyRef]().asJava) + val blockConfig = rocksDbOptions.tableFormatConfig().asInstanceOf[BlockBasedTableConfig] + blockConfig.blockCacheSize() shouldBe 16777216l + blockConfig.blockSize() shouldBe 16384l + blockConfig.cacheIndexAndFilterBlocks() shouldBe true + rocksDbOptions.maxWriteBufferNumber() shouldBe 2 + config.kafkaConfig.streamsConfig.values().get(StreamsConfig.APPLICATION_SERVER_CONFIG).toString shouldBe "localhost:8080" + } + + it("should allow for the application server to be set in the config file") { + Given("a test configuration file") + val file = "test/test_application_server_set.conf" + + When("Application configuration is loaded and KafkaConfiguration is obtained") + val config = new AppConfiguration(file) + + Then("it should load the application.server expected") + config.kafkaConfig.streamsConfig.values().get(StreamsConfig.APPLICATION_SERVER_CONFIG).toString shouldBe "127.0.0.1:1002" + } + } +} diff --git a/service-graph/graph-builder/src/test/scala/com/expedia/www/haystack/service/graph/graph/builder/kafka/KafkaController.scala b/service-graph/graph-builder/src/test/scala/com/expedia/www/haystack/service/graph/graph/builder/kafka/KafkaController.scala new file mode 100644 index 000000000..8ebb2e7b3 --- /dev/null +++ b/service-graph/graph-builder/src/test/scala/com/expedia/www/haystack/service/graph/graph/builder/kafka/KafkaController.scala @@ -0,0 +1,89 @@ +package com.expedia.www.haystack.service.graph.graph.builder.kafka + +import java.util.Properties +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean + +import kafka.server.RunningAsBroker +import org.apache.kafka.clients.CommonClientConfigs +import org.apache.kafka.clients.admin.{AdminClient, NewTopic} +import org.apache.kafka.clients.consumer.{ConsumerConfig, KafkaConsumer} +import org.apache.kafka.clients.producer.{KafkaProducer, ProducerConfig} +import org.apache.kafka.common.serialization.{Deserializer, Serializer} +import org.slf4j.LoggerFactory + +import scala.collection.JavaConverters._ +import scala.util.Try + +class KafkaController(kafkaProperties: Properties, zooKeeperProperties: Properties) { + require(kafkaProperties != null) + require(zooKeeperProperties != null) + + private val LOGGER = LoggerFactory.getLogger(classOf[KafkaController]) + + private val zkPort = zooKeeperProperties.getProperty("clientPort").toInt + private val kafkaPort = kafkaProperties.getProperty("port").toInt + + lazy val zkUrl: String = "localhost:" + zkPort + lazy val kafkaUrl: String = "localhost:" + kafkaPort + + private val kafkaPropertiesWithZk = new Properties + kafkaPropertiesWithZk.putAll(kafkaProperties) + kafkaPropertiesWithZk.put("zookeeper.connect", zkUrl) + private val kafkaServer = new KafkaLocal(kafkaPropertiesWithZk) + + def startService(): Unit = { + //start zk + val zookeeper = new ZooKeeperLocal(zooKeeperProperties) + new Thread(zookeeper).start() + Thread.sleep(2000) + + //start kafka + kafkaServer.start() + Thread.sleep(2000) + + // check kafka status + if (kafkaServer.state().currentState != RunningAsBroker.state) { + throw new IllegalStateException("Kafka server is not in a running state") + } + + //lifecycle message + LOGGER.info("Kafka started and listening : {}", kafkaUrl) + } + + def stopService(): Unit = { + //stop kafka + kafkaServer.stop() + + //lifecycle message + LOGGER.info("Kafka stopped") + } + + def createTopics(topics: List[String]): Unit = { + if (topics.nonEmpty) { + val adminClient = AdminClient.create(getBootstrapProperties) + try { + adminClient.createTopics(topics.map(topic => new NewTopic(topic, 1, 1)).asJava) + adminClient.listTopics().names().get().forEach(s => LOGGER.info("Available topic : {}", s)) + } + finally { + Try(adminClient.close(5, TimeUnit.SECONDS)) + } + } + } + + def createProducer[K, V] (topic: String, keySerializer: Serializer[K], + valueSerializer: Serializer[V]) : KafkaProducer[K, V] = { + val properties = getBootstrapProperties + properties.put(ProducerConfig.CLIENT_ID_CONFIG, topic + "Producer") + new KafkaProducer[K, V](properties, keySerializer, valueSerializer) + } + + private def getBootstrapProperties: Properties = { + val properties = new Properties() + properties.put(CommonClientConfigs.BOOTSTRAP_SERVERS_CONFIG, List(kafkaUrl).asJava) + properties + } +} + +class InvalidStateException(message: String) extends RuntimeException(message) {} diff --git a/service-graph/graph-builder/src/test/scala/com/expedia/www/haystack/service/graph/graph/builder/kafka/KafkaLocal.scala b/service-graph/graph-builder/src/test/scala/com/expedia/www/haystack/service/graph/graph/builder/kafka/KafkaLocal.scala new file mode 100644 index 000000000..7775c4407 --- /dev/null +++ b/service-graph/graph-builder/src/test/scala/com/expedia/www/haystack/service/graph/graph/builder/kafka/KafkaLocal.scala @@ -0,0 +1,23 @@ +package com.expedia.www.haystack.service.graph.graph.builder.kafka + +import java.util.Properties + +import kafka.metrics.KafkaMetricsReporter +import kafka.server.{BrokerState, KafkaConfig, KafkaServer} + +class KafkaLocal(val kafkaProperties: Properties) { + val kafkaConfig: KafkaConfig = KafkaConfig.fromProps(kafkaProperties) + val kafka: KafkaServer = new KafkaServer(kafkaConfig, kafkaMetricsReporters = List[KafkaMetricsReporter]()) + + def start(): Unit = { + kafka.startup() + } + + def stop(): Unit = { + kafka.shutdown() + } + + def state(): BrokerState = { + kafka.brokerState + } +} diff --git a/service-graph/graph-builder/src/test/scala/com/expedia/www/haystack/service/graph/graph/builder/kafka/ZooKeeperLocal.scala b/service-graph/graph-builder/src/test/scala/com/expedia/www/haystack/service/graph/graph/builder/kafka/ZooKeeperLocal.scala new file mode 100644 index 000000000..a5a8dafa3 --- /dev/null +++ b/service-graph/graph-builder/src/test/scala/com/expedia/www/haystack/service/graph/graph/builder/kafka/ZooKeeperLocal.scala @@ -0,0 +1,31 @@ +package com.expedia.www.haystack.service.graph.graph.builder.kafka + +import java.io.IOException +import java.util.Properties + +import org.apache.zookeeper.server.quorum.QuorumPeerConfig +import org.apache.zookeeper.server.{ServerConfig, ZooKeeperServerMain} +import org.slf4j.LoggerFactory + + +object ZooKeeperLocal { + private val LOGGER = LoggerFactory.getLogger(classOf[ZooKeeperLocal]) +} + +class ZooKeeperLocal(val zkProperties: Properties) extends Runnable { + private val quorumConfiguration = new QuorumPeerConfig + quorumConfiguration.parseProperties(zkProperties) + private val configuration = new ServerConfig + configuration.readFrom(quorumConfiguration) + private val zooKeeperServer = new ZooKeeperServerMain + + override def run(): Unit = { + try { + zooKeeperServer.runFromConfig(configuration) + } + catch { + case e: IOException => + ZooKeeperLocal.LOGGER.error("Zookeeper startup failed.", e) + } + } +} diff --git a/service-graph/graph-builder/src/test/scala/com/expedia/www/haystack/service/graph/graph/builder/model/EdgeStatsSerdeSpec.scala b/service-graph/graph-builder/src/test/scala/com/expedia/www/haystack/service/graph/graph/builder/model/EdgeStatsSerdeSpec.scala new file mode 100644 index 000000000..0a1753fc7 --- /dev/null +++ b/service-graph/graph-builder/src/test/scala/com/expedia/www/haystack/service/graph/graph/builder/model/EdgeStatsSerdeSpec.scala @@ -0,0 +1,61 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.service.graph.graph.builder.model + +import com.expedia.www.haystack.service.graph.graph.builder.TestSpec + +class EdgeStatsSerdeSpec extends TestSpec { + + describe("EdgeStateSerde ") { + it("should serialize EdgeState") { + Given("a valid EdgeState object") + val edgeStats = EdgeStats(0, 0, 0) + + And("EdgeState serializer") + val serializer = new EdgeStatsSerde().serializer() + + When("EdgeState is serialized") + val bytes = serializer.serialize("", edgeStats) + + Then("it should generate valid byte stream") + bytes.nonEmpty should be(true) + } + + it("should deserialize serialized EdgeState") { + Given("a valid EdgeState object") + val edgeStats = EdgeStats(1, 1, 1) + + And("serialized EdgeState") + val serializer = new EdgeStatsSerde().serializer() + val bytes = serializer.serialize("", edgeStats) + + And("EdgeState deserializer") + val deserializer = new EdgeStatsSerde().deserializer() + + When("EdgeState byte is deserialized") + val deserializedEdgeStats = deserializer.deserialize("", bytes) + + Then("it should generate valid byte stream") + deserializedEdgeStats should not be null + deserializedEdgeStats.count should be(1) + deserializedEdgeStats.lastSeen should be(1) + deserializedEdgeStats.errorCount should be(1) + } + } +} diff --git a/service-graph/graph-builder/src/test/scala/com/expedia/www/haystack/service/graph/graph/builder/model/EdgeStatsSpec.scala b/service-graph/graph-builder/src/test/scala/com/expedia/www/haystack/service/graph/graph/builder/model/EdgeStatsSpec.scala new file mode 100644 index 000000000..9273e25f7 --- /dev/null +++ b/service-graph/graph-builder/src/test/scala/com/expedia/www/haystack/service/graph/graph/builder/model/EdgeStatsSpec.scala @@ -0,0 +1,90 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.service.graph.graph.builder.model + +import com.expedia.www.haystack.commons.entities.TagKeys.ERROR_KEY +import com.expedia.www.haystack.commons.entities.{GraphEdge, GraphVertex} +import org.scalatest.mockito.MockitoSugar +import org.scalatest.{FunSpec, GivenWhenThen, Matchers} + +import scala.collection.mutable + +class EdgeStatsSpec extends FunSpec with GivenWhenThen with Matchers with MockitoSugar { + + private val vertexName1 = "vertexName1" + private val vertexName2 = "vertexName2" + private val mutableTagsa: mutable.Map[String, String] = mutable.Map("mutableKeya" -> "mutableValuea", ERROR_KEY -> "true") + private val mutableTagsb: mutable.Map[String, String] = mutable.Map("mutableKeyb" -> "mutableValueb", ERROR_KEY -> "true") + private val immutableTagsA: Map[String, String] = Map("immutableKeyA" -> "immutableValueA", ERROR_KEY -> "true") + private val immutableTagsB: Map[String, String] = Map("immutableKeyB" -> "immutableValueB", ERROR_KEY -> "true") + private val graphVertex1A: GraphVertex = GraphVertex(vertexName1, immutableTagsA) + private val graphVertex2B: GraphVertex = GraphVertex(vertexName2, immutableTagsB) + private val graphVertexWithNoTags: GraphVertex = GraphVertex(vertexName2, Map.empty) + private val operationX = "operationX" + private val sourceTimestamp12 = 12 + private val currentTimeAtStartOfTest = System.currentTimeMillis() + private val edgeStatsWithNoTags = EdgeStats(count = 0, lastSeen = 0, errorCount = 0) + private val graphEdgeWithNoTags = GraphEdge(graphVertexWithNoTags, graphVertexWithNoTags, operationX, 0) + + describe("EdgeStats constructor") { + it("should use empty Maps for tags if no tags were specified") { + assert(edgeStatsWithNoTags.sourceTags.isEmpty) + assert(edgeStatsWithNoTags.destinationTags.isEmpty) + } + } + + describe("EdgeStats update") { + { + val graphEdge = GraphEdge(graphVertex1A, graphVertex2B, operationX, sourceTimestamp12) + val edgeStats = EdgeStats(count = 0, lastSeen = 0, errorCount = 0, + sourceTags = mutableTagsa, destinationTags = mutableTagsb) + val updatedEdgeStats = edgeStats.update(graphEdge) + it("should collect tags") { + updatedEdgeStats.sourceTags.contains("mutableKeyb") + updatedEdgeStats.sourceTags.contains("immutableKeyB") + updatedEdgeStats.destinationTags.contains("mutableKeya") + updatedEdgeStats.destinationTags.contains("immutableKeyA") + } + it("should clear error keys from the tags") { + updatedEdgeStats.sourceTags.size shouldEqual 2 + updatedEdgeStats.destinationTags.size shouldEqual 2 + } + it("should count errors passed in from the graph edge source tags") { + updatedEdgeStats.errorCount shouldEqual 1 + } + it("should assume no errors if the source tags map does not contain an error key") { + edgeStatsWithNoTags.update(graphEdgeWithNoTags).errorCount shouldEqual 0 + } + } + it("should calculate last seen from System.currentTimeMillis if source timestamp is 0") { + val graphEdge = GraphEdge(graphVertex1A, graphVertex2B, operationX, 0) + val edgeStats = EdgeStats(count = 0, lastSeen = 0, errorCount = 0, + sourceTags = mutableTagsa, destinationTags = mutableTagsb) + val updatedEdgeStats = edgeStats.update(graphEdge) + assert(updatedEdgeStats.lastSeen >= currentTimeAtStartOfTest) + } + it("should calculate last seen from source timestamp if source timestamp is not 0") { + val graphEdge = GraphEdge(graphVertex1A, graphVertex2B, operationX, sourceTimestamp12) + val edgeStats = EdgeStats(count = 0, lastSeen = 0, errorCount = 0, + sourceTags = mutableTagsa, destinationTags = mutableTagsb) + val updatedEdgeStats = edgeStats.update(graphEdge) + updatedEdgeStats.lastSeen shouldEqual sourceTimestamp12 + } + } +} diff --git a/service-graph/graph-builder/src/test/scala/com/expedia/www/haystack/service/graph/graph/builder/model/ServiceGraphEdgeSpec.scala b/service-graph/graph-builder/src/test/scala/com/expedia/www/haystack/service/graph/graph/builder/model/ServiceGraphEdgeSpec.scala new file mode 100644 index 000000000..5b846726f --- /dev/null +++ b/service-graph/graph-builder/src/test/scala/com/expedia/www/haystack/service/graph/graph/builder/model/ServiceGraphEdgeSpec.scala @@ -0,0 +1,55 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.service.graph.graph.builder.model + +import com.expedia.www.haystack.service.graph.graph.builder.TestSpec + +class ServiceGraphEdgeSpec extends TestSpec { + + describe("ServiceGraphEdge") { + it("should merge Service graph objects accurately") { + Given("valid ServiceGraphEdge objects") + val serviceGraph1 = ServiceGraphEdge( + ServiceGraphVertex("src", Map("X-HAYSTACK-INFRASTRUCTURE-PROVIDER" -> "aws")), + ServiceGraphVertex("dest", Map("X-HAYSTACK-INFRASTRUCTURE-PROVIDER" -> "dc")), + ServiceEdgeStats(10, 15000, 3), 0, 10000) + + val serviceGraph2 = ServiceGraphEdge( + ServiceGraphVertex("src", Map("X-HAYSTACK-INFRASTRUCTURE-PROVIDER" -> "dc")), + ServiceGraphVertex("dest", Map("X-HAYSTACK-INFRASTRUCTURE-PROVIDER" -> "dc")), + ServiceEdgeStats(15, 16000, 5), 0, 10000) + + val serviceGraph3 = ServiceGraphEdge( + ServiceGraphVertex("src", Map("X-HAYSTACK-INFRASTRUCTURE-PROVIDER" -> "aws")), + ServiceGraphVertex("dest", Map("X-HAYSTACK-INFRASTRUCTURE-PROVIDER" -> "dc")), + ServiceEdgeStats(20, 17000, 8), 0, 10000) + + When("Merging service graph objects") + val serviceGraph4 = serviceGraph1 + serviceGraph2 + val serviceGraph5 = serviceGraph3 + serviceGraph4 + + serviceGraph5.source.tags.get("X-HAYSTACK-INFRASTRUCTURE-PROVIDER").get should be ("aws,dc") + serviceGraph5.destination.tags.get("X-HAYSTACK-INFRASTRUCTURE-PROVIDER").get should be ("dc") + serviceGraph5.stats.count should be (45) + serviceGraph5.stats.errorCount should be (16) + serviceGraph5.stats.lastSeen should be (17000) + } + + } +} diff --git a/service-graph/graph-builder/src/test/scala/com/expedia/www/haystack/service/graph/graph/builder/service/ManagedHttpServiceSpec.scala b/service-graph/graph-builder/src/test/scala/com/expedia/www/haystack/service/graph/graph/builder/service/ManagedHttpServiceSpec.scala new file mode 100644 index 000000000..52751468f --- /dev/null +++ b/service-graph/graph-builder/src/test/scala/com/expedia/www/haystack/service/graph/graph/builder/service/ManagedHttpServiceSpec.scala @@ -0,0 +1,58 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.service.graph.graph.builder.service + +import org.mockito.Mockito.{verify, verifyNoMoreInteractions} +import org.scalatest.mockito.MockitoSugar +import org.scalatest.{FunSpec, Matchers} + +class ManagedHttpServiceSpec extends FunSpec with Matchers with MockitoSugar { + describe("ManagedHttpService constructor") { + it ("should require the service argument to be non-null") { + an [IllegalArgumentException] should be thrownBy new ManagedHttpService(null) + } + } + + describe("ManagedHttpService.start()") { + val httpService = mock[HttpService] + val managedHttpService = new ManagedHttpService(httpService) + it("should call the service's start() method and set isRunning to true") { + managedHttpService.start() + verify(httpService).start() + assert(managedHttpService.hasStarted) + } + verifyNoMoreInteractions(httpService) + } + + describe("ManagedHttpService.stop()") { + val httpService = mock[HttpService] + val managedHttpService = new ManagedHttpService(httpService) + it("should not call the service's stop() method if the service is not running") { + managedHttpService.stop() + verifyNoMoreInteractions(httpService) + } + it("should call the service's close() method and set isRunning to false if the service is running") { + managedHttpService.start() + managedHttpService.stop() + verify(httpService).start() + verify(httpService).close() + assert(!managedHttpService.hasStarted) + verifyNoMoreInteractions(httpService) + } + } +} diff --git a/service-graph/graph-builder/src/test/scala/com/expedia/www/haystack/service/graph/graph/builder/service/resources/LocalOperationGraphResourceSpec.scala b/service-graph/graph-builder/src/test/scala/com/expedia/www/haystack/service/graph/graph/builder/service/resources/LocalOperationGraphResourceSpec.scala new file mode 100644 index 000000000..9c1a8ce34 --- /dev/null +++ b/service-graph/graph-builder/src/test/scala/com/expedia/www/haystack/service/graph/graph/builder/service/resources/LocalOperationGraphResourceSpec.scala @@ -0,0 +1,49 @@ +package com.expedia.www.haystack.service.graph.graph.builder.service.resources + +import com.expedia.www.haystack.service.graph.graph.builder.model.{OperationGraph, OperationGraphEdge} +import com.expedia.www.haystack.service.graph.graph.builder.service.fetchers.LocalOperationEdgesFetcher +import com.expedia.www.haystack.service.graph.graph.builder.service.utils.QueryTimestampReader +import javax.servlet.http.HttpServletRequest +import org.mockito.Mockito +import org.mockito.Mockito.{verify, when} +import org.scalatest.mockito.MockitoSugar +import org.scalatest.{FunSpec, Matchers} + +class LocalOperationGraphResourceSpec extends FunSpec with Matchers with MockitoSugar { + private implicit val timestampReader: QueryTimestampReader = mock[QueryTimestampReader] + private class LocalOperationGraphResourceChild(localEdgesFetcher: LocalOperationEdgesFetcher) + extends LocalOperationGraphResource(localEdgesFetcher: LocalOperationEdgesFetcher) { + override def get(request: HttpServletRequest): OperationGraph = { + super.get(request) + } + } + + describe("LocalOperationGraphResource.get()") { + val localEdgesFetcher = mock[LocalOperationEdgesFetcher] + val request = mock[HttpServletRequest] + val operationGraphEdges = mock[List[OperationGraphEdge]] + + val OperationGraphEdgesLength = 42 + val From: Long = 271828 + val To: Long = 371415 + + val localOperationGraphResource = new LocalOperationGraphResourceChild(localEdgesFetcher) + + it ("should read an OperationGraph with the correct timestamps") { + when(timestampReader.fromTimestamp(request)).thenReturn(From) + when(timestampReader.toTimestamp(request)).thenReturn(To) + when(localEdgesFetcher.fetchEdges(From, To)).thenReturn(operationGraphEdges) + when(operationGraphEdges.length).thenReturn(OperationGraphEdgesLength) + + val localGraph = localOperationGraphResource.get(request) + + assert(localGraph.edges == operationGraphEdges) + + verify(timestampReader).fromTimestamp(request) + verify(timestampReader).toTimestamp(request) + verify(localEdgesFetcher).fetchEdges(From, To) + verify(operationGraphEdges).length + Mockito.verifyNoMoreInteractions(localEdgesFetcher, request, operationGraphEdges) + } + } +} diff --git a/service-graph/graph-builder/src/test/scala/com/expedia/www/haystack/service/graph/graph/builder/service/utils/EdgesMergerSpec.scala b/service-graph/graph-builder/src/test/scala/com/expedia/www/haystack/service/graph/graph/builder/service/utils/EdgesMergerSpec.scala new file mode 100644 index 000000000..e2797476d --- /dev/null +++ b/service-graph/graph-builder/src/test/scala/com/expedia/www/haystack/service/graph/graph/builder/service/utils/EdgesMergerSpec.scala @@ -0,0 +1,103 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.service.graph.graph.builder.service.utils + +import java.lang.Math.{max, min} + +import com.expedia.www.haystack.service.graph.graph.builder.model._ +import org.scalatest.{FunSpec, Matchers} + +class EdgesMergerSpec extends FunSpec with Matchers { + + describe("EdgesMerger.getMergedServiceEdges()") { + val stats1 = ServiceEdgeStats(1, 10, 100) + val stats3 = ServiceEdgeStats(3, 30, 300) + val stats5 = ServiceEdgeStats(5, 50, 500) + val stats7 = ServiceEdgeStats(7, 70, 700) + val vertexA: ServiceGraphVertex = ServiceGraphVertex("serviceGraphVertexA", Map.empty) + val vertexB: ServiceGraphVertex = ServiceGraphVertex("serviceGraphVertexB", Map.empty) + val vertexC: ServiceGraphVertex = ServiceGraphVertex("serviceGraphVertexC", Map.empty) + val edgeAB1 = ServiceGraphEdge(vertexA, vertexB, stats1, 1000, 10000) + val edgeAB3 = ServiceGraphEdge(vertexA, vertexB, stats3, 3000, 30000) + val edgeAC5 = ServiceGraphEdge(vertexA, vertexC, stats5, 7000, 70000) + val edgeBC7 = ServiceGraphEdge(vertexB, vertexC, stats7, 15000, 150000) + it("should create two edges when source matches but destination does not") { + val mergedEdges = EdgesMerger.getMergedServiceEdges(Seq(edgeAB1, edgeAC5)) + mergedEdges.size should equal(2) + mergedEdges should contain(edgeAB1) + mergedEdges should contain(edgeAC5) + } + it("should create two edges when destination matches but source does not") { + val mergedEdges = EdgesMerger.getMergedServiceEdges(Seq(edgeAC5, edgeBC7)) + mergedEdges.size should equal(2) + mergedEdges should contain(edgeAC5) + mergedEdges should contain(edgeBC7) + } + it("should merge two edges when source and destination match") { + val mergedEdges = EdgesMerger.getMergedServiceEdges(Seq(edgeAB1, edgeAB3)) + mergedEdges.size should equal(1) + val mergedEdge: ServiceGraphEdge = mergedEdges.head + mergedEdge.source should equal(vertexA) + mergedEdge.destination should equal(vertexB) + mergedEdge.effectiveFrom should equal(min(edgeAB1.effectiveFrom, edgeAB3.effectiveFrom)) + mergedEdge.effectiveTo should equal(max(edgeAB1.effectiveTo, edgeAB3.effectiveTo)) + mergedEdge.stats.count should equal(edgeAB1.stats.count + edgeAB3.stats.count) + mergedEdge.stats.lastSeen should equal(max(edgeAB1.stats.lastSeen, edgeAB3.stats.lastSeen)) + mergedEdge.stats.errorCount should equal(edgeAB1.stats.errorCount + edgeAB3.stats.errorCount) + } + } + + describe("EdgesMerger.getMergedOperationEdge()") { + val stats1 = EdgeStats(1, 10, 100) + val stats3 = EdgeStats(3, 30, 300) + val stats5 = EdgeStats(5, 50, 500) + val stats7 = EdgeStats(7, 70, 700) + val stats9 = EdgeStats(9, 90, 900) + val edgeAX1: OperationGraphEdge = OperationGraphEdge("sourceA", "destinationX", "operation1", stats1, 1000, 10000) + val edgeAX3: OperationGraphEdge = OperationGraphEdge("sourceA", "destinationX", "operation3", stats3, 3000, 30000) + val edgeAY3: OperationGraphEdge = OperationGraphEdge("sourceA", "destinationY", "operation3", stats5, 7000, 70000) + val edgeBY3a: OperationGraphEdge = OperationGraphEdge("sourceB", "destinationY", "operation3", stats7, 15000, 150000) + val edgeBY3b: OperationGraphEdge = OperationGraphEdge("sourceB", "destinationY", "operation3", stats7, 31000, 310000) + it ("should create two edges when source and destination match but operation does not") { + val mergedEdges = EdgesMerger.getMergedOperationEdges(Seq(edgeAX1, edgeAX3)) + mergedEdges.size should equal(2) + mergedEdges should contain(edgeAX1) + mergedEdges should contain(edgeAX3) + } + it ("should create two edges when source and operation match but destination does not") { + val mergedEdges = EdgesMerger.getMergedOperationEdges(Seq(edgeAX3, edgeAY3)) + mergedEdges.size should equal(2) + mergedEdges should contain(edgeAX3) + mergedEdges should contain(edgeAY3) + } + it ("should merge two edges when source, destination and operation match") { + val mergedEdges = EdgesMerger.getMergedOperationEdges(Seq(edgeBY3a, edgeBY3b)) + mergedEdges.size should equal(1) + val mergedEdge = mergedEdges.head + mergedEdge.source should equal(edgeBY3a.source) + mergedEdge.destination should equal(edgeBY3b.destination) + mergedEdge.operation should equal(edgeBY3a.operation) + mergedEdge.stats.count should equal(edgeBY3a.stats.count + edgeBY3b.stats.count) + mergedEdge.stats.lastSeen should equal(max(edgeBY3a.stats.lastSeen, edgeBY3b.stats.lastSeen)) + mergedEdge.stats.errorCount should equal(edgeBY3a.stats.errorCount + edgeBY3b.stats.errorCount) + mergedEdge.effectiveFrom should equal(min(edgeBY3a.effectiveFrom, edgeBY3b.effectiveFrom)) + mergedEdge.effectiveTo should equal(max(edgeBY3a.effectiveTo, edgeBY3b.effectiveTo)) + } + } +} diff --git a/service-graph/graph-builder/src/test/scala/com/expedia/www/haystack/service/graph/graph/builder/stream/StreamSupplierSpec.scala b/service-graph/graph-builder/src/test/scala/com/expedia/www/haystack/service/graph/graph/builder/stream/StreamSupplierSpec.scala new file mode 100644 index 000000000..5d9c861b3 --- /dev/null +++ b/service-graph/graph-builder/src/test/scala/com/expedia/www/haystack/service/graph/graph/builder/stream/StreamSupplierSpec.scala @@ -0,0 +1,66 @@ +package com.expedia.www.haystack.service.graph.graph.builder.stream + +import java.util +import java.util.Collections +import java.util.concurrent.TimeUnit +import java.util.function.Supplier + +import com.expedia.www.haystack.commons.health.HealthStatusController +import org.apache.kafka.clients.admin.{AdminClient, ListTopicsResult} +import org.apache.kafka.common.KafkaFuture +import org.apache.kafka.streams.{StreamsConfig, Topology} +import org.mockito.Mockito.{reset, verify, verifyNoMoreInteractions, when} +import org.scalatest.mockito.MockitoSugar +import org.scalatest.{FunSpec, Matchers} + +class StreamSupplierSpec extends FunSpec with Matchers with MockitoSugar { + private val ConsumerTopic = "ConsumerTopic" + + private val topologySupplier = mock[Supplier[Topology]] + private val healthController = mock[HealthStatusController] + private val streamsConfig = mock[StreamsConfig] + + describe("StreamSupplier constructor") { + it ("should require the topologySupplier argument to be non-null") { + an [IllegalArgumentException] should be thrownBy + new StreamSupplier(null, healthController, streamsConfig, ConsumerTopic) + } + it ("should require the healthController argument to be non-null") { + an [IllegalArgumentException] should be thrownBy + new StreamSupplier(topologySupplier, null, streamsConfig, ConsumerTopic) + } + it ("should require the streamsConfig argument to be non-null") { + an [IllegalArgumentException] should be thrownBy + new StreamSupplier(topologySupplier, healthController, null, ConsumerTopic) + } + it ("should require the consumerTopic argument to be non-null") { + an [IllegalArgumentException] should be thrownBy + new StreamSupplier(topologySupplier, healthController, streamsConfig, null) + } + verifyNoMoreInteractions(topologySupplier, healthController, streamsConfig) + } + + private val adminClient = mock[AdminClient] + private val listTopicsResult: ListTopicsResult = mock[ListTopicsResult] + private val kafkaFuture: KafkaFuture[util.Set[String]] = mock[KafkaFuture[util.Set[String]]] + + describe("StreamSupplier.get()") { + it("should throw an exception if the consumer topic does exist") { + when(adminClient.listTopics()).thenReturn(listTopicsResult) + when(listTopicsResult.names()).thenReturn(kafkaFuture) + val nonExistentTopic = "NonExistent" + ConsumerTopic + when(kafkaFuture.get()).thenReturn(Collections.singleton(ConsumerTopic)) + + val streamSupplier = new StreamSupplier(topologySupplier, healthController, streamsConfig, nonExistentTopic, adminClient) + val thrown = the [streamSupplier.TopicNotPresentException] thrownBy streamSupplier.get + thrown.getTopic shouldEqual nonExistentTopic + + verify(adminClient).listTopics() + verify(listTopicsResult).names() + verify(kafkaFuture).get() + verify(adminClient).close(5, TimeUnit.SECONDS) + verifyNoMoreInteractions(topologySupplier, healthController, streamsConfig, adminClient, listTopicsResult, kafkaFuture) + reset(topologySupplier, healthController, streamsConfig, adminClient, listTopicsResult, kafkaFuture) + } + } +} diff --git a/service-graph/node-finder/Makefile b/service-graph/node-finder/Makefile new file mode 100644 index 000000000..d8c273917 --- /dev/null +++ b/service-graph/node-finder/Makefile @@ -0,0 +1,11 @@ +.PHONY: integration_test release + +export DOCKER_ORG := expediadotcom +export DOCKER_IMAGE_NAME := haystack-service-graph-node-finder +PWD := $(shell pwd) + +docker-image: + docker build -t $(DOCKER_IMAGE_NAME) -f build/docker/Dockerfile . + +release: docker-image + ../deployment/scripts/publish-to-docker-hub.sh diff --git a/service-graph/node-finder/README.md b/service-graph/node-finder/README.md new file mode 100644 index 000000000..6d494b026 --- /dev/null +++ b/service-graph/node-finder/README.md @@ -0,0 +1,65 @@ +#Haystack : node-finder + +Information on what this component is all about is documented in the [README](../README.md) of the repository + +## Building + +``` +mvn clean verify +``` + +or + +``` +make docker-image +``` + +## Testing Locally + +* Download Kafka 0.11.0.x +* Start Zookeeper locally (from kafka home) +``` +bin/zookeeper-server-start.sh config/zookeeper.properties +``` +* Start Kafka locally (from kafka home) +``` +bin/kafka-server-start.sh config/server.properties +``` +* Create proto-spans topic (from kafka home) +``` +bin/kafka-topics.sh --create --zookeeper localhost:2181 --replication-factor 1 --partitions 1 --topic proto-spans +``` +* Create a local.conf override file +``` +cat local.conf + +health.status.path = "logs/isHealthy" + +kafka { + streams { + bootstrap.servers = "localhost:9092" + } + accumulator { + interval = 1000 + } +} +``` +* Build node-finder application locally (node-finder app root) +``` +mvn clean package +``` +* Start the application (node-finder app root) +``` +export HAYSTACK_OVERRIDES_CONFIG_PATH=/local.conf +java -jar target/haystack-service-graph-node-finder.jar +``` +* Send data to Kafka (refer to fakespans tool README) +``` +$GOBIN/fakespans --from-file fakespans.json --kafka-broker localhost:9092 +``` +* Check the output topics (from kafka home) +``` +bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic graph-nodes --from-beginning + +bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic metricpoints --from-beginning +``` diff --git a/service-graph/node-finder/build/docker/Dockerfile b/service-graph/node-finder/build/docker/Dockerfile new file mode 100644 index 000000000..16201a90e --- /dev/null +++ b/service-graph/node-finder/build/docker/Dockerfile @@ -0,0 +1,24 @@ +FROM openjdk:8-jre +MAINTAINER Haystack + +ENV APP_NAME haystack-service-graph-node-finder +ENV APP_HOME /app/bin +ENV JMXTRANS_AGENT jmxtrans-agent-1.2.6 +ENV DOCKERIZE_VERSION v0.6.1 + +ADD https://github.com/jwilder/dockerize/releases/download/${DOCKERIZE_VERSION}/dockerize-alpine-linux-amd64-${DOCKERIZE_VERSION}.tar.gz dockerize.tar.gz +RUN tar xzf dockerize.tar.gz +RUN chmod +x dockerize + +RUN mkdir -p ${APP_HOME} + +COPY target/${APP_NAME}.jar ${APP_HOME}/ +COPY build/docker/start-app.sh ${APP_HOME}/ +COPY build/docker/jmxtrans-agent.xml ${APP_HOME}/ + +RUN chmod +x ${APP_HOME}/start-app.sh +ADD https://github.com/jmxtrans/jmxtrans-agent/releases/download/${JMXTRANS_AGENT}/${JMXTRANS_AGENT}.jar ${APP_HOME}/ + +WORKDIR ${APP_HOME} + +ENTRYPOINT ["./start-app.sh"] diff --git a/service-graph/node-finder/build/docker/jmxtrans-agent.xml b/service-graph/node-finder/build/docker/jmxtrans-agent.xml new file mode 100644 index 000000000..0ae96bfef --- /dev/null +++ b/service-graph/node-finder/build/docker/jmxtrans-agent.xml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ${HAYSTACK_GRAPHITE_HOST:monitoring-influxdb-graphite.kube-system.svc} + ${HAYSTACK_GRAPHITE_PORT:2003} + ${HAYSTACK_GRAPHITE_ENABLED:true} + + haystack.service-graph.node-finder.#hostname#. + + 60 + diff --git a/service-graph/node-finder/build/docker/start-app.sh b/service-graph/node-finder/build/docker/start-app.sh new file mode 100755 index 000000000..719b69531 --- /dev/null +++ b/service-graph/node-finder/build/docker/start-app.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +[ -z "$JAVA_XMS" ] && JAVA_XMS=1024m +[ -z "$JAVA_XMX" ] && JAVA_XMX=1024m + +set -e +JAVA_OPTS="${JAVA_OPTS} \ +-javaagent:${APP_HOME}/${JMXTRANS_AGENT}.jar=${APP_HOME}/jmxtrans-agent.xml \ +-XX:+UseG1GC \ +-Xloggc:/var/log/gc.log \ +-XX:+PrintGCDetails \ +-XX:+PrintGCDateStamps \ +-XX:+UseGCLogFileRotation \ +-XX:NumberOfGCLogFiles=5 \ +-XX:GCLogFileSize=2M \ +-Xmx${JAVA_XMX} \ +-Xms${JAVA_XMS} \ +-Dapplication.name=${APP_NAME} \ +-Dapplication.home=${APP_HOME}" + +if [[ -n "$SERVICE_DEBUG_ON" ]] && [[ "$SERVICE_DEBUG_ON" == true ]]; then + JAVA_OPTS="$JAVA_OPTS -Xdebug -Xrunjdwp:transport=dt_socket,address=5005,server=y" +fi + +exec java ${JAVA_OPTS} -jar "${APP_HOME}/${APP_NAME}.jar" diff --git a/service-graph/node-finder/pom.xml b/service-graph/node-finder/pom.xml new file mode 100644 index 000000000..3d2c1bed1 --- /dev/null +++ b/service-graph/node-finder/pom.xml @@ -0,0 +1,191 @@ + + + + + haystack-service-graph + com.expedia.www + 1.0.15-SNAPSHOT + + + 4.0.0 + haystack-service-graph-node-finder + jar + + + + + Apache License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + 1.1.0 + com.expedia.www.haystack.service.graph.node.finder.App + ${project.artifactId}-${project.version} + + + + + + org.apache.commons + commons-lang3 + + + + + org.apache.kafka + kafka_${scala.major.minor.version} + ${kafka-version} + + + org.slf4j + slf4j-log4j12 + + + + + org.apache.kafka + kafka-clients + ${kafka-version} + + + + org.apache.kafka + kafka-streams + ${kafka-version} + + + + com.expedia.www + haystack-logback-metrics-appender + + + + org.msgpack + msgpack-core + + + + org.json4s + json4s-ext_${scala.major.minor.version} + + + + + + ${finalName} + + + org.scalatest + scalatest-maven-plugin + + + test + + test + + + org.expedia.www.haystack.commons.scalatest.IntegrationSuite + + + + integration-test + integration-test + + test + + + org.expedia.www.haystack.commons.scalatest.IntegrationSuite + + + + + + + org.scoverage + scoverage-maven-plugin + + true + + com.expedia.www.haystack.service.graph.node.finder;com.expedia.www.haystack.commons.kstreams.app.Main;com.expedia.www.haystack.commons.kstreams.app.StreamsFactory + + + + + + + org.apache.maven.plugins + maven-shade-plugin + + true + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + + + + package + + shade + + + + + reference.conf + + + ${mainClass} + + + + + + + + + net.alchim31.maven + scala-maven-plugin + + + + org.scalastyle + scalastyle-maven-plugin + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + diff --git a/service-graph/node-finder/src/main/resources/app.conf b/service-graph/node-finder/src/main/resources/app.conf new file mode 100644 index 000000000..e1d885c52 --- /dev/null +++ b/service-graph/node-finder/src/main/resources/app.conf @@ -0,0 +1,43 @@ +health.status.path = "/app/isHealthy" + +kafka { + close.timeout.ms = 30000 + + streams { + application.id = "haystack-service-graph-node-finder" + bootstrap.servers = "kafkasvc:9092" + num.stream.threads = 4 + request.timeout.ms = 60000 + commit.interval.ms = 3000 + auto.offset.reset = latest + timestamp.extractor = "com.expedia.www.haystack.commons.kstreams.SpanTimestampExtractor" + producer.linger.ms = 500 + state.dir = "/app" + replication.factor = 2 + } + + producer { + metrics.topic = "metric-data-points" + service.call.topic = "graph-nodes" + } + + consumer { + topic = "proto-spans" + } + + //config for span accumulator to wait for matching span after which it will drop the span + accumulator { + interval = 2500 //in milliSec + } + + collectorTags = [] + + node.metadata { + topic { + autocreate = true + name = "haystack-node-finder-metadata" + partition.count = 1 + replication.factor = 1 + } + } +} diff --git a/service-graph/node-finder/src/main/resources/logback.xml b/service-graph/node-finder/src/main/resources/logback.xml new file mode 100644 index 000000000..c45f62d7b --- /dev/null +++ b/service-graph/node-finder/src/main/resources/logback.xml @@ -0,0 +1,27 @@ + + + + + + + true + + + + + + %d{yyyy-MM-dd HH:mm:ss:SSS} %thread, %level, %logger{70}, "%msg" %replace(%ex){'[\n]+', '\\n'}%nopex%n + + + + + + ${HAYSTACK_LOG_QUEUE_SIZE:-500} + ${HAYSTACK_LOG_DISCARD_THRESHOLD:-0} + + + + + + + diff --git a/service-graph/node-finder/src/main/scala/com/expedia/www/haystack/service/graph/node/finder/App.scala b/service-graph/node-finder/src/main/scala/com/expedia/www/haystack/service/graph/node/finder/App.scala new file mode 100644 index 000000000..e6e7ff038 --- /dev/null +++ b/service-graph/node-finder/src/main/scala/com/expedia/www/haystack/service/graph/node/finder/App.scala @@ -0,0 +1,70 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.service.graph.node.finder + +import com.expedia.www.haystack.commons.health.{HealthStatusController, UpdateHealthStatusFile} +import com.expedia.www.haystack.commons.kstreams.app.{Main, StateChangeListener, StreamsFactory, StreamsRunner} +import com.expedia.www.haystack.service.graph.node.finder.app.Streams +import com.expedia.www.haystack.service.graph.node.finder.app.metadata.TopicCreator +import com.expedia.www.haystack.service.graph.node.finder.config.AppConfiguration +import com.netflix.servo.util.VisibleForTesting + +/** + * Starting point for node-finder application + */ +object App extends Main { + /** + * Creates a valid instance of StreamsRunner. + * + * StreamsRunner is created with a valid StreamsFactory instance and a listener that observes + * state changes of the kstreams application. + * + * StreamsFactory in turn is created with a Topology Supplier and kafka.StreamsConfig. Any failure in + * StreamsFactory is gracefully handled by StreamsRunner to shut the application off + * + * Core logic of this application is in the `app.Streams` instance - which is a topology supplier. The + * topology of this application is built in this class. + * + * @return A valid instance of `StreamsRunner` + */ + override def createStreamsRunner(): StreamsRunner = { + val appConfiguration = new AppConfiguration() + + val healthStatusController = new HealthStatusController + healthStatusController.addListener(new UpdateHealthStatusFile(appConfiguration.healthStatusFilePath)) + + val stateChangeListener = new StateChangeListener(healthStatusController) + + createStreamsRunner(appConfiguration, stateChangeListener) + } + + @VisibleForTesting + def createStreamsRunner(appConfiguration: AppConfiguration, + stateChangeListener: StateChangeListener): StreamsRunner = { + //create the topology provider + val kafkaConfig = appConfiguration.kafkaConfig + + TopicCreator.makeMetadataTopicReady(kafkaConfig) + + val streams = new Streams(kafkaConfig) + + val streamsFactory = new StreamsFactory(streams, kafkaConfig.streamsConfig, kafkaConfig.protoSpanTopic) + + new StreamsRunner(streamsFactory, stateChangeListener) + } +} diff --git a/service-graph/node-finder/src/main/scala/com/expedia/www/haystack/service/graph/node/finder/app/GraphNodeProducer.scala b/service-graph/node-finder/src/main/scala/com/expedia/www/haystack/service/graph/node/finder/app/GraphNodeProducer.scala new file mode 100644 index 000000000..611c00de3 --- /dev/null +++ b/service-graph/node-finder/src/main/scala/com/expedia/www/haystack/service/graph/node/finder/app/GraphNodeProducer.scala @@ -0,0 +1,62 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.service.graph.node.finder.app + +import com.expedia.www.haystack.commons.metrics.MetricsSupport +import com.expedia.www.haystack.service.graph.node.finder.model.SpanPair +import org.apache.kafka.streams.processor.{Processor, ProcessorContext, ProcessorSupplier} +import org.slf4j.LoggerFactory + +class GraphNodeProducerSupplier extends ProcessorSupplier[String, SpanPair] { + override def get(): Processor[String, SpanPair] = new GraphNodeProducer +} + +class GraphNodeProducer extends Processor[String, SpanPair] with MetricsSupport { + private var context: ProcessorContext = _ + private val processMeter = metricRegistry.meter("graph.node.producer.process") + private val forwardMeter = metricRegistry.meter("graph.node.producer.emit") + private val LOGGER = LoggerFactory.getLogger(classOf[GraphNodeProducer]) + + override def init(context: ProcessorContext): Unit = { + this.context = context + } + + override def process(key: String, spanPair: SpanPair): Unit = { + processMeter.mark() + + if (LOGGER.isDebugEnabled) { + LOGGER.debug(s"Received message ($key, $spanPair)") + } + + spanPair.getGraphEdge match { + case Some(graphEdge) => + context.forward(graphEdge, graphEdge) + forwardMeter.mark() + if (LOGGER.isDebugEnabled) { + LOGGER.debug(s"Graph edge : (${spanPair.getId}, $graphEdge") + } + case None => + } + + context.commit() + } + + override def punctuate(timestamp: Long): Unit = {} + + override def close(): Unit = {} +} diff --git a/service-graph/node-finder/src/main/scala/com/expedia/www/haystack/service/graph/node/finder/app/LatencyProducer.scala b/service-graph/node-finder/src/main/scala/com/expedia/www/haystack/service/graph/node/finder/app/LatencyProducer.scala new file mode 100644 index 000000000..83b67e4a4 --- /dev/null +++ b/service-graph/node-finder/src/main/scala/com/expedia/www/haystack/service/graph/node/finder/app/LatencyProducer.scala @@ -0,0 +1,62 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.service.graph.node.finder.app + +import com.expedia.www.haystack.commons.metrics.MetricsSupport +import com.expedia.www.haystack.service.graph.node.finder.model.SpanPair +import org.apache.kafka.streams.processor.{Processor, ProcessorContext, ProcessorSupplier} +import org.slf4j.LoggerFactory + +class LatencyProducerSupplier() extends ProcessorSupplier[String, SpanPair] { + override def get(): Processor[String, SpanPair] = new LatencyProducer() +} + +class LatencyProducer() extends Processor[String, SpanPair] with MetricsSupport { + private var context: ProcessorContext = _ + private val processMeter = metricRegistry.meter("latency.producer.process") + private val forwardMeter = metricRegistry.meter("latency.producer.emit") + private val LOGGER = LoggerFactory.getLogger(classOf[LatencyProducer]) + + override def init(context: ProcessorContext): Unit = { + this.context = context + } + + override def process(key: String, spanPair: SpanPair): Unit = { + processMeter.mark() + + if (LOGGER.isDebugEnabled) { + LOGGER.debug(s"Received message ($key, $spanPair)") + } + + spanPair.getLatency match { + case Some(metricData) => + context.forward(metricData.getMetricDefinition.getKey, metricData) + forwardMeter.mark() + if (LOGGER.isInfoEnabled()) { + LOGGER.info(s"Latency Metric: $metricData") + } + case None => + } + + context.commit() + } + + override def punctuate(timestamp: Long): Unit = {} + + override def close(): Unit = {} +} diff --git a/service-graph/node-finder/src/main/scala/com/expedia/www/haystack/service/graph/node/finder/app/SpanAccumulator.scala b/service-graph/node-finder/src/main/scala/com/expedia/www/haystack/service/graph/node/finder/app/SpanAccumulator.scala new file mode 100644 index 000000000..9b1276d1b --- /dev/null +++ b/service-graph/node-finder/src/main/scala/com/expedia/www/haystack/service/graph/node/finder/app/SpanAccumulator.scala @@ -0,0 +1,191 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.service.graph.node.finder.app + +import com.expedia.open.tracing.Span +import com.expedia.www.haystack.commons.graph.GraphEdgeTagCollector +import com.expedia.www.haystack.commons.metrics.MetricsSupport +import com.expedia.www.haystack.service.graph.node.finder.model.{LightSpan, ServiceNodeMetadata, SpanPair, SpanPairBuilder} +import com.expedia.www.haystack.service.graph.node.finder.utils.SpanUtils +import com.netflix.servo.util.VisibleForTesting +import org.apache.commons.lang3.StringUtils +import org.apache.kafka.streams.processor._ +import org.apache.kafka.streams.state.KeyValueStore +import org.slf4j.LoggerFactory + +import scala.collection.mutable + +class SpanAccumulatorSupplier(storeName: String, + accumulatorInterval: Int, + tagCollector: GraphEdgeTagCollector) extends + ProcessorSupplier[String, Span] { + override def get(): Processor[String, Span] = new SpanAccumulator(storeName, accumulatorInterval, tagCollector) +} + +class SpanAccumulator(storeName: String, + accumulatorInterval: Int, + tagCollector: GraphEdgeTagCollector) + extends Processor[String, Span] with MetricsSupport { + + private val LOGGER = LoggerFactory.getLogger(classOf[SpanAccumulator]) + private val processMeter = metricRegistry.meter("span.accumulator.process") + private val aggregateMeter = metricRegistry.meter("span.accumulator.aggregate") + private val forwardMeter = metricRegistry.meter("span.accumulator.emit") + + // map to store spanId -> span data. Used for checking child-parent relationship + private var spanMap = mutable.HashMap[String, mutable.HashSet[LightSpan]]() + + // map to store parentSpanId -> span data. Used for checking child-parent relationship + private var parentSpanMap = mutable.HashMap[String, mutable.HashSet[LightSpan]]() + + private var processorContext: ProcessorContext = _ + private var metadataStore: KeyValueStore[String, ServiceNodeMetadata] = _ + + override def init(context: ProcessorContext): Unit = { + processorContext = context + context.schedule(accumulatorInterval, PunctuationType.STREAM_TIME, getPunctuator(context)) + metadataStore = context.getStateStore(storeName).asInstanceOf[KeyValueStore[String, ServiceNodeMetadata]] + LOGGER.info(s"${this.getClass.getSimpleName} initialized") + } + + override def process(key: String, span: Span): Unit = { + processMeter.mark() + + //find the span type + val spanType = SpanUtils.getSpanType(span) + + if (SpanUtils.isAccumulableSpan(span)) { + + val lightSpan = LightSpan(span.getSpanId, + span.getParentSpanId, + span.getStartTime / 1000, //startTime is in microseconds, so divide it by 1000 to send MS + span.getServiceName, + span.getOperationName, + span.getDuration, + spanType, + tagCollector.collectTags(span)) + + //add new light span to the span map and parent map + spanMap.getOrElseUpdate(span.getSpanId, mutable.HashSet[LightSpan]()).add(lightSpan) + if (StringUtils.isNotEmpty(span.getParentSpanId)) { + parentSpanMap.getOrElseUpdate(span.getParentSpanId, mutable.HashSet[LightSpan]()).add(lightSpan) + } + + processSpan(lightSpan) foreach { + spanPair => + if (isValidMerge(spanPair)) forward(processorContext, spanPair) + cleanupSpanMap(spanPair) + aggregateMeter.mark() + } + } + } + + override def punctuate(timestamp: Long): Unit = {} + + override def close(): Unit = {} + + //forward all complete spans + private def forward(context: ProcessorContext, spanPair: SpanPair): Unit = { + LOGGER.debug("Forwarding complete SpanPair: {}", spanPair) + context.forward(spanPair.getId, spanPair) + forwardMeter.mark() + } + + /** + * process the given light span to check whether it can form a span pair + * + * @param span incoming span to be processed + * @return sequence of span pair whether complete or incomplete + */ + private def processSpan(span: LightSpan): Seq[SpanPair] = { + val possibleSpanPairs = spanMap(span.spanId) + + //matched span, whether complete or incomplete based on their service + val spanPairs = mutable.ListBuffer[SpanPair]() + + //same spanId is present in spanMap + if (possibleSpanPairs.size > 1) { + spanPairs += SpanPairBuilder.createSpanPair(possibleSpanPairs.head, possibleSpanPairs.tail.head) + } else { + //look for its parent ie if its parentId is in span map + spanMap.get(span.parentSpanId) match { + case Some(parentSpan) => spanPairs += SpanPairBuilder.createSpanPair(parentSpan.head, span) + case _ => + } + //look for its child ie if its spanId is in parent map + parentSpanMap.get(span.spanId) match { + case Some(childSpans) => spanPairs ++= childSpans.map(childSpan => SpanPairBuilder.createSpanPair(childSpan, span)) + case _ => + } + } + spanPairs + } + + @VisibleForTesting + def getPunctuator(context: ProcessorContext): Punctuator = { + (timestamp: Long) => { + //we keep a span only until timeToKeep time and leave the rest in place and see + //if they get their matching span pair before timeToKeep + val timeToKeep = timestamp - accumulatorInterval //in milliSec + LOGGER.debug(s"Punctuate called with $timestamp. TimeToKeep is $timeToKeep. Map sizes are ${spanMap.values.flatten[LightSpan].size} & ${parentSpanMap.size}") + + //if the span is within the time limit, we will keep them, otherwise discard + spanMap = spanMap.filter { + case (_, ls) => ls.exists(sp => sp.isLaterThan(timeToKeep)) + } + parentSpanMap = parentSpanMap.filter { + case (_, ls) => ls.exists(sp => sp.isLaterThan(timeToKeep)) + } + + // commit the current processing progress + context.commit() + } + } + + @VisibleForTesting + def spanCount: Int = spanMap.values.flatten[LightSpan].size + + @VisibleForTesting + def internalSpanMap = spanMap.toMap + + /** + * spans in a span pair to be cleaned up from the parent span map. + * Not removing it from spanMap since there could be multiple children for it in case of same service. + * + * @param spanPair span pair with client / server spans + */ + private def cleanupSpanMap(spanPair: SpanPair): Unit = { + spanPair.getBackingSpans.foreach(ls => { + parentSpanMap.remove(ls.spanId) + }) + } + + private def isValidMerge(spanPair: SpanPair): Boolean = { + if (spanPair.isComplete) { + val metadata = metadataStore.get(spanPair.getServerSpan.serviceName) + if (metadata == null) { + true + } else { + // if current merge matches with the recorded style, or it is shared span merge style, then accept it + (metadata.useSharedSpan == spanPair.IsSharedSpan) || spanPair.IsSharedSpan + } + } else { + false + } + } +} diff --git a/service-graph/node-finder/src/main/scala/com/expedia/www/haystack/service/graph/node/finder/app/Streams.scala b/service-graph/node-finder/src/main/scala/com/expedia/www/haystack/service/graph/node/finder/app/Streams.scala new file mode 100644 index 000000000..3816304b8 --- /dev/null +++ b/service-graph/node-finder/src/main/scala/com/expedia/www/haystack/service/graph/node/finder/app/Streams.scala @@ -0,0 +1,234 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.service.graph.node.finder.app + +import java.util.function.Supplier + +import com.expedia.www.haystack.commons.graph.GraphEdgeTagCollector +import com.expedia.www.haystack.commons.kstreams.serde.SpanSerde +import com.expedia.www.haystack.commons.kstreams.serde.graph.GraphEdgeKeySerde +import com.expedia.www.haystack.commons.kstreams.serde.graph.GraphEdgeValueSerde +import com.expedia.www.haystack.commons.kstreams.serde.metricdata.{MetricDataSerializer, MetricTankSerde} +import com.expedia.www.haystack.service.graph.node.finder.app.metadata.MetadataProducerSupplier +import com.expedia.www.haystack.service.graph.node.finder.app.metadata.MetadataStoreUpdateProcessorSupplier +import com.expedia.www.haystack.service.graph.node.finder.config.KafkaConfiguration +import com.expedia.www.haystack.service.graph.node.finder.model.MetadataStoreBuilder +import com.expedia.www.haystack.service.graph.node.finder.model.ServiceNodeMetadataSerde +import com.netflix.servo.util.VisibleForTesting +import org.apache.kafka.common.serialization.Serdes +import org.apache.kafka.common.serialization.StringDeserializer +import org.apache.kafka.common.serialization.StringSerializer +import org.apache.kafka.streams.Topology + +class Streams(kafkaConfiguration: KafkaConfiguration) extends Supplier[Topology] { + + private val PROTO_SPANS = "proto-spans" + private val SPAN_ACCUMULATOR = "span-accumulator" + private val LATENCY_PRODUCER = "latency-producer" + private val GRAPH_NODE_PRODUCER = "nodes-n-edges-producer" + private val METRIC_SINK = "metric-sink" + private val GRAPH_NODE_SINK = "graph-nodes-sink" + + private val METADATA_STORE_PROCESSOR = "metadata-store-processor" + private val METADATA_SOURCE_NODE = "metadata-source-node" + private val METADATA_PRODUCER = "metadata-producer" + private val METADATA_SINK = "metadata-sink" + + override def get(): Topology = initialize(new Topology) + + /** + * This provides a topology that is shown in the flow chart below + * + * +---------------+ + * | | + * | proto-spans | + * | | + * +-------+-------+ + * | + * +---------V----------+ + * | | + * +----+ span-accumulator +----+ + * | | | | + * | +--------------------+ | + * | | + * +---------V---------+ +------------V------------+ + * | | | | + * | latency-producer | | nodes-n-edges-producer | + * | | | | + * +---------+---------+ +------------+------------+ + * | | + * +--------V--------+ +---------V---------+ + * | | | | + * | metric-sink | | graph-nodes-sink | + * | | | | + * +-----------------+ +-------------------+ + * + * Source: + * + * proto-spans : Reads a Kafka topic of spans serialized in protobuf format + * + * Processors: + * + * span-accumulator : Aggregates incoming spans for specified time to find matching client-server spans + * latency-producer : Computes and emits network latency from the span pairs produced by span-accumulator + * nodes-n-edges-producer : This processor produces, from the span pairs produced by span-accumulator, a simple + * graph relationship between the services in the form: service --(operation)--> service + * Sinks: + * + * metric-sink : Output of latency-producer (MetricPoint) is serialized using MessagePack and sent to a Kafka topic + * graph-nodes-sink : Output of nodes-n-edges-producer is serialized a json string and sent to a Kafka topic + * + * @return the Topology + */ + @VisibleForTesting + def initialize(topology: Topology): Topology = { + //add source + addSource(PROTO_SPANS, topology) + + //add span accumulator. This step will aggregate spans + //by message id. This will emit spans with client-server + //relationship after specified number of seconds + addAccumulator(SPAN_ACCUMULATOR, topology, PROTO_SPANS) + + //add latency producer. This is downstream of accumulator + //this will parse a span with client-server relationship and + //emit a metric point on the latency for that service-operation pair + addLatencyProducer(LATENCY_PRODUCER, topology, SPAN_ACCUMULATOR) + + //add graph node producer. This is downstream of accumulator + //for each client-server span emitted by the accumulator, this will + //produce a service - operation - service data point for building + //the edges between the nodes in a graph + addGraphNodeProducer(GRAPH_NODE_PRODUCER, topology, SPAN_ACCUMULATOR) + + //add sink for latency producer + addMetricSink(METRIC_SINK, kafkaConfiguration.metricsTopic, topology, LATENCY_PRODUCER) + + //add sink for graph node producer + addGraphNodeSink(GRAPH_NODE_SINK, kafkaConfiguration.serviceCallTopic, topology, GRAPH_NODE_PRODUCER) + + //add metadata processor and a sink for metadata store + addMetadataProducer(METADATA_PRODUCER, topology, SPAN_ACCUMULATOR) + addMetadataStoreSink(METADATA_SINK, topology, METADATA_PRODUCER) + + //return the topology built + topology + } + + private def addSource(stepName: String, topology: Topology) : Unit = { + //add a source + topology.addSource( + kafkaConfiguration.autoOffsetReset, + stepName, + kafkaConfiguration.timestampExtractor, + new StringDeserializer, + (new SpanSerde).deserializer(), + kafkaConfiguration.protoSpanTopic) + } + + private def addAccumulator(accumulatorName: String, topology: Topology, sourceName: String) : Unit = { + val tags = + if (kafkaConfiguration.collectorTags != null) + kafkaConfiguration.collectorTags.toSet[String] + else + Set[String]() + + topology.addProcessor( + accumulatorName, + new SpanAccumulatorSupplier(kafkaConfiguration.metadataConfig.topic, kafkaConfiguration.accumulatorInterval, + new GraphEdgeTagCollector(tags)), + sourceName + ) + + topology.addGlobalStore(MetadataStoreBuilder.storeBuilder(kafkaConfiguration.metadataConfig), + METADATA_SOURCE_NODE, + Serdes.String().deserializer(), + new ServiceNodeMetadataSerde().deserializer(), + kafkaConfiguration.metadataConfig.topic, + METADATA_STORE_PROCESSOR, + new MetadataStoreUpdateProcessorSupplier(kafkaConfiguration.metadataConfig.topic)) + } + + private def addLatencyProducer(latencyProducerName: String, + topology: Topology, + accumulatorName: String) : Unit = { + topology.addProcessor( + latencyProducerName, + new LatencyProducerSupplier(), + accumulatorName + ) + } + + private def addGraphNodeProducer(graphNodeProducerName: String, + topology: Topology, + accumulatorName: String) = { + topology.addProcessor( + graphNodeProducerName, + new GraphNodeProducerSupplier(), + accumulatorName + ) + } + + private def addMetricSink(metricSinkName: String, + metricsTopic: String, + topology: Topology, + latencyProducerName: String): Unit = { + topology.addSink( + metricSinkName, + metricsTopic, + new StringSerializer, + new MetricDataSerializer, + latencyProducerName + ) + } + + private def addGraphNodeSink(graphNodeSinkName: String, + serviceCallTopic: String, + topology: Topology, + graphNodeProducerName: String): Unit = { + topology.addSink( + graphNodeSinkName, + serviceCallTopic, + new GraphEdgeKeySerde().serializer(), + new GraphEdgeValueSerde().serializer(), + graphNodeProducerName + ) + } + + private def addMetadataStoreSink(sinkName: String, + topology: Topology, + producerName: String): Unit = { + topology.addSink( + sinkName, + kafkaConfiguration.metadataConfig.topic, + Serdes.String().serializer(), + new ServiceNodeMetadataSerde().serializer(), + producerName + ) + } + + private def addMetadataProducer(processorName: String, + topology: Topology, + producerName: String): Unit = { + topology.addProcessor( + processorName, + new MetadataProducerSupplier(kafkaConfiguration.metadataConfig.topic), + producerName + ) + } +} diff --git a/service-graph/node-finder/src/main/scala/com/expedia/www/haystack/service/graph/node/finder/app/metadata/MetadataProducerSupplier.scala b/service-graph/node-finder/src/main/scala/com/expedia/www/haystack/service/graph/node/finder/app/metadata/MetadataProducerSupplier.scala new file mode 100644 index 000000000..c2bca5dd8 --- /dev/null +++ b/service-graph/node-finder/src/main/scala/com/expedia/www/haystack/service/graph/node/finder/app/metadata/MetadataProducerSupplier.scala @@ -0,0 +1,48 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.service.graph.node.finder.app.metadata + +import com.expedia.www.haystack.service.graph.node.finder.model.{ServiceNodeMetadata, SpanPair} +import org.apache.kafka.streams.processor.{Processor, ProcessorContext, ProcessorSupplier} +import org.apache.kafka.streams.state.KeyValueStore + +class MetadataProducerSupplier(metadataStoreName: String) extends ProcessorSupplier[String, SpanPair] { + override def get(): Processor[String, SpanPair] = new MetadataProducer(metadataStoreName) +} + +class MetadataProducer(metadataStoreName: String) extends Processor[String, SpanPair] { + private var context: ProcessorContext = _ + private var store: KeyValueStore[String, ServiceNodeMetadata] = _ + + override def init(context: ProcessorContext): Unit = { + this.context = context + this.store = context.getStateStore(metadataStoreName).asInstanceOf[KeyValueStore[String, ServiceNodeMetadata]] + } + + override def process(key: String, spanPair: SpanPair): Unit = { + // emit the metadata only if service uses SharedSpan merge style + if (this.store.get(spanPair.getServerSpan.serviceName) == null && spanPair.IsSharedSpan) { + context.forward(spanPair.getServerSpan.serviceName, ServiceNodeMetadata(spanPair.IsSharedSpan)) + } + } + + override def punctuate(timestamp: Long): Unit = () + + override def close(): Unit = () +} \ No newline at end of file diff --git a/service-graph/node-finder/src/main/scala/com/expedia/www/haystack/service/graph/node/finder/app/metadata/MetadataStoreUpdateProcessorSupplier.scala b/service-graph/node-finder/src/main/scala/com/expedia/www/haystack/service/graph/node/finder/app/metadata/MetadataStoreUpdateProcessorSupplier.scala new file mode 100644 index 000000000..9438b4d79 --- /dev/null +++ b/service-graph/node-finder/src/main/scala/com/expedia/www/haystack/service/graph/node/finder/app/metadata/MetadataStoreUpdateProcessorSupplier.scala @@ -0,0 +1,47 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.service.graph.node.finder.app.metadata + +import com.expedia.www.haystack.service.graph.node.finder.model.ServiceNodeMetadata +import org.apache.kafka.streams.processor.{Processor, ProcessorContext, ProcessorSupplier} +import org.apache.kafka.streams.state.KeyValueStore + +import scala.util.Try + +class MetadataStoreUpdateProcessorSupplier(storeName: String) extends ProcessorSupplier[String, ServiceNodeMetadata] { + override def get(): Processor[String, ServiceNodeMetadata] = new MetadataStoreUpdateProcessor(storeName) +} + +class MetadataStoreUpdateProcessor(storeName: String) extends Processor[String, ServiceNodeMetadata] { + private var store: KeyValueStore[String, ServiceNodeMetadata] = _ + + override def init(context: ProcessorContext): Unit = { + store = context.getStateStore(storeName).asInstanceOf[KeyValueStore[String, ServiceNodeMetadata]] + } + + override def process(key: String, value: ServiceNodeMetadata): Unit = { + store.put(key, value) + } + + override def punctuate(timestamp: Long): Unit = () + + override def close(): Unit = { + Try(store.close()) + } +} \ No newline at end of file diff --git a/service-graph/node-finder/src/main/scala/com/expedia/www/haystack/service/graph/node/finder/app/metadata/TopicCreator.scala b/service-graph/node-finder/src/main/scala/com/expedia/www/haystack/service/graph/node/finder/app/metadata/TopicCreator.scala new file mode 100644 index 000000000..248c62817 --- /dev/null +++ b/service-graph/node-finder/src/main/scala/com/expedia/www/haystack/service/graph/node/finder/app/metadata/TopicCreator.scala @@ -0,0 +1,69 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.service.graph.node.finder.app.metadata + +import java.util.Properties +import java.util.concurrent.{ExecutionException, TimeUnit} + +import com.expedia.www.haystack.service.graph.node.finder.config.KafkaConfiguration +import org.apache.kafka.clients.CommonClientConfigs +import org.apache.kafka.clients.admin.{AdminClient, NewTopic} +import org.apache.kafka.common.config.TopicConfig +import org.apache.kafka.common.errors.TopicExistsException +import org.slf4j.LoggerFactory + +import scala.collection.JavaConverters._ +import scala.util.Try + +object TopicCreator { + private val LOGGER = LoggerFactory.getLogger(TopicCreator.getClass) + + def makeMetadataTopicReady(config: KafkaConfiguration): Unit = { + if(!config.metadataConfig.autoCreate) + return + + val properties = new Properties() + properties.put(CommonClientConfigs.BOOTSTRAP_SERVERS_CONFIG, config.streamsConfig.getList(CommonClientConfigs.BOOTSTRAP_SERVERS_CONFIG)) + val adminClient = AdminClient.create(properties) + try { + val overridesConfig = new java.util.HashMap[String, String]() + overridesConfig.put(TopicConfig.CLEANUP_POLICY_CONFIG, TopicConfig.CLEANUP_POLICY_COMPACT) + val topics = new NewTopic( + config.metadataConfig.topic, + config.metadataConfig.partitionCount, + config.metadataConfig.replicationFactor.toShort).configs(overridesConfig) + adminClient.createTopics(List(topics).asJava).values().entrySet().asScala.foreach(entry => { + try { + entry.getValue.get() + } catch { + case ex: ExecutionException => + if (ex.getCause.isInstanceOf[TopicExistsException]) { + LOGGER.info(s"metadata topic '${config.metadataConfig}' already exists!") + } else { + throw new RuntimeException(s"Fail to create the metadata topic ${config.metadataConfig}", ex) + } + case ex: Exception => throw new RuntimeException(s"Fail to create the metadata topic ${config.metadataConfig}", ex) + } + }) + } + finally { + Try(adminClient.close(5, TimeUnit.SECONDS)) + } + } +} diff --git a/service-graph/node-finder/src/main/scala/com/expedia/www/haystack/service/graph/node/finder/config/AppConfiguration.scala b/service-graph/node-finder/src/main/scala/com/expedia/www/haystack/service/graph/node/finder/config/AppConfiguration.scala new file mode 100644 index 000000000..81b71d34d --- /dev/null +++ b/service-graph/node-finder/src/main/scala/com/expedia/www/haystack/service/graph/node/finder/config/AppConfiguration.scala @@ -0,0 +1,117 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.service.graph.node.finder.config + +import java.util.Properties + +import com.expedia.www.haystack.commons.config.ConfigurationLoader +import com.expedia.www.haystack.commons.kstreams.SpanTimestampExtractor +import com.typesafe.config.Config +import org.apache.commons.lang3.StringUtils +import org.apache.kafka.streams.StreamsConfig +import org.apache.kafka.streams.Topology.AutoOffsetReset +import org.apache.kafka.streams.processor.TimestampExtractor + +import scala.collection.JavaConverters._ + +/** + * This class reads the configuration from the given resource name using {@link ConfigurationLoader ConfigurationLoader} + * + * @param resourceName name of the resource file to load + */ +class AppConfiguration(resourceName: String) { + + require(StringUtils.isNotBlank(resourceName)) + + private val config = ConfigurationLoader.loadConfigFileWithEnvOverrides(resourceName = this.resourceName) + + /** + * default constructor. Loads config from resource name to "app.conf" + */ + def this() = this("app.conf") + + /** + * Location of the health status file + */ + val healthStatusFilePath: String = config.getString("health.status.path") + + /** + * Instance of {@link KafkaConfiguration KafkaConfiguration} to be used by the kstreams application + */ + lazy val kafkaConfig: KafkaConfiguration = { + + // verify if the applicationId and bootstrap server config are non empty + def verifyRequiredProps(props: Properties): Unit = { + require(StringUtils.isNotBlank(props.getProperty(StreamsConfig.APPLICATION_ID_CONFIG))) + require(StringUtils.isNotBlank(props.getProperty(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG))) + } + + def addProps(config: Config, props: Properties, prefix: (String) => String = identity): Unit = { + config.entrySet().asScala.foreach(kv => { + val propKeyName = prefix(kv.getKey) + props.setProperty(propKeyName, kv.getValue.unwrapped().toString) + }) + } + + val kafka = config.getConfig("kafka") + val producerConfig = kafka.getConfig("producer") + val consumerConfig = kafka.getConfig("consumer") + val streamsConfig = kafka.getConfig("streams") + + val props = new Properties + + // add stream specific properties + addProps(streamsConfig, props) + + // validate props + verifyRequiredProps(props) + + val timestampExtractor = Option(props.getProperty("timestamp.extractor")) match { + case Some(timeStampExtractorClass) => + Class.forName(timeStampExtractorClass).newInstance().asInstanceOf[TimestampExtractor] + case None => + new SpanTimestampExtractor + } + + + //set timestamp extractor + props.setProperty("timestamp.extractor", timestampExtractor.getClass.getName) + + val collectorTags: List[String] = if (kafka.hasPath("collectorTags")) kafka.getStringList("collectorTags").asScala + .toList + else List() + + val metadataTopicConfig = kafka.getConfig("node.metadata.topic") + + KafkaConfiguration(new StreamsConfig(props), + producerConfig.getString("metrics.topic"), + producerConfig.getString("service.call.topic"), + consumerConfig.getString("topic"), + if (streamsConfig.hasPath("auto.offset.reset")) { + AutoOffsetReset.valueOf(streamsConfig.getString("auto.offset.reset").toUpperCase) + } + else { + AutoOffsetReset.LATEST + }, + timestampExtractor, + kafka.getInt("accumulator.interval"), + kafka.getLong("close.timeout.ms"), + NodeMetadataConfiguration(metadataTopicConfig.getBoolean("autocreate"), metadataTopicConfig.getString("name"), metadataTopicConfig.getInt("partition.count"), metadataTopicConfig.getInt("replication.factor")), + collectorTags) + } +} diff --git a/service-graph/node-finder/src/main/scala/com/expedia/www/haystack/service/graph/node/finder/config/KafkaConfiguration.scala b/service-graph/node-finder/src/main/scala/com/expedia/www/haystack/service/graph/node/finder/config/KafkaConfiguration.scala new file mode 100644 index 000000000..bf4de3e25 --- /dev/null +++ b/service-graph/node-finder/src/main/scala/com/expedia/www/haystack/service/graph/node/finder/config/KafkaConfiguration.scala @@ -0,0 +1,56 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.service.graph.node.finder.config + +import org.apache.commons.lang3.StringUtils +import org.apache.kafka.streams.StreamsConfig +import org.apache.kafka.streams.Topology.AutoOffsetReset +import org.apache.kafka.streams.processor.TimestampExtractor + +/** + * Case class holding required configuration for the node finder kstreams app + * @param streamsConfig valid instance of StreamsConfig + * @param metricsTopic topic name for latency metrics + * @param serviceCallTopic topic name for service call relationship information + * @param protoSpanTopic topic from where Spans serialized in protobuf to be consumed + * @param autoOffsetReset Offset type for the kstreams app to start with + * @param timestampExtractor instance of timestamp extractor + * @param accumulatorInterval interval to aggregate spans to look for client and server spans + * @param closeTimeoutInMs time for closing a kafka topic + * @param metadataConfig configuration for metadata kakfa topic + * @param collectorTags Tags to be collected when generating graph edges + */ +case class KafkaConfiguration(streamsConfig: StreamsConfig, + metricsTopic: String, + serviceCallTopic: String, + protoSpanTopic: String, + autoOffsetReset: AutoOffsetReset, + timestampExtractor: TimestampExtractor, + accumulatorInterval: Int, + closeTimeoutInMs: Long, + metadataConfig: NodeMetadataConfiguration, + collectorTags: List[String]) { + require(streamsConfig != null) + require(StringUtils.isNotBlank(metricsTopic)) + require(StringUtils.isNotBlank(serviceCallTopic)) + require(StringUtils.isNotBlank(protoSpanTopic)) + require(autoOffsetReset != null) + require(timestampExtractor != null) + require(closeTimeoutInMs > 0) + require(collectorTags != null) +} diff --git a/service-graph/node-finder/src/main/scala/com/expedia/www/haystack/service/graph/node/finder/config/NodeMetadataConfiguration.scala b/service-graph/node-finder/src/main/scala/com/expedia/www/haystack/service/graph/node/finder/config/NodeMetadataConfiguration.scala new file mode 100644 index 000000000..0850a8e6c --- /dev/null +++ b/service-graph/node-finder/src/main/scala/com/expedia/www/haystack/service/graph/node/finder/config/NodeMetadataConfiguration.scala @@ -0,0 +1,21 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.service.graph.node.finder.config + +case class NodeMetadataConfiguration(autoCreate: Boolean, topic: String, partitionCount: Int, replicationFactor: Int) diff --git a/service-graph/node-finder/src/main/scala/com/expedia/www/haystack/service/graph/node/finder/model/LightSpan.scala b/service-graph/node-finder/src/main/scala/com/expedia/www/haystack/service/graph/node/finder/model/LightSpan.scala new file mode 100644 index 000000000..9fea6f5f7 --- /dev/null +++ b/service-graph/node-finder/src/main/scala/com/expedia/www/haystack/service/graph/node/finder/model/LightSpan.scala @@ -0,0 +1,98 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.service.graph.node.finder.model + +import com.expedia.www.haystack.service.graph.node.finder.utils.SpanType +import com.expedia.www.haystack.service.graph.node.finder.utils.SpanType.SpanType +import org.apache.commons.lang3.StringUtils + +/** + * Light weight representation of a Span with minimal information required + * + * @param spanId Unique identity of the Span + * @param parentSpanId spanId of its Parent span + * @param time Timestamp associated with a Span in MilliSeconds (i.e., StartTime) + * @param serviceName Service name of the span + * @param operationName Operation name of the span + * @param duration duration of the Span in micro seconds + * @param spanType type of the span + */ +case class LightSpan(spanId: String, + parentSpanId: String, + time: Long, // in epoch millis + serviceName: String, + operationName: String, + duration: Long, // in micros + spanType: SpanType, + tags: Map[String, String]) extends Equals { + require(StringUtils.isNotBlank(spanId)) + require(time > 0) + require(StringUtils.isNotBlank(serviceName)) + require(StringUtils.isNoneBlank(operationName)) + require(spanType != null) + + private val durationInMillis = duration / 1000L + /** + * check whether this light span is later than the given cutOffTime + * + * @param cutOffTime time in epoch millis to be compared + * @return true if this span is later than the given cutOffTime time else false + */ + def isLaterThan(cutOffTime: Long): Boolean = (time + durationInMillis - cutOffTime) > 0 + + override def canEqual(that: Any): Boolean = { + that.isInstanceOf[LightSpan] + } + + override def equals(that: Any): Boolean = { + that match { + case that: LightSpan => + that.canEqual(this) && + this.spanId == that.spanId && + this.parentSpanId == that.parentSpanId && + this.serviceName == that.serviceName + case _ => false + } + } + + override def hashCode(): Int = { + 41 * ( + 41 * ( + 41 + spanId.hashCode + ) + parentSpanId.hashCode + ) + serviceName.hashCode + } +} + +/** + * Builder class for LightSpan + */ +object LightSpanBuilder { + + /** + * update span type to an existing span + * + * @param span span to be updated + * @param spanType span type to be updated in a given span + * @return + */ + def updateSpanTypeIfAbsent(span: LightSpan, spanType: SpanType): LightSpan = { + if (span.spanType == SpanType.OTHER) span.copy(spanType = spanType) else span + } +} diff --git a/service-graph/node-finder/src/main/scala/com/expedia/www/haystack/service/graph/node/finder/model/ServiceNodeMetadata.scala b/service-graph/node-finder/src/main/scala/com/expedia/www/haystack/service/graph/node/finder/model/ServiceNodeMetadata.scala new file mode 100644 index 000000000..c28e24c5a --- /dev/null +++ b/service-graph/node-finder/src/main/scala/com/expedia/www/haystack/service/graph/node/finder/model/ServiceNodeMetadata.scala @@ -0,0 +1,76 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.service.graph.node.finder.model + +import java.util + +import com.expedia.www.haystack.service.graph.node.finder.config.NodeMetadataConfiguration +import org.apache.kafka.common.serialization.{Deserializer, Serde, Serdes, Serializer} +import org.apache.kafka.streams.state.{KeyValueStore, StoreBuilder, Stores} +import org.json4s.DefaultFormats +import org.json4s.jackson.Serialization + +case class ServiceNodeMetadata(useSharedSpan: Boolean) + +class ServiceNodeMetadataSerde extends Serde[ServiceNodeMetadata] { + implicit val formats = DefaultFormats + + override def deserializer(): Deserializer[ServiceNodeMetadata] = { + new Deserializer[ServiceNodeMetadata] { + override def configure(map: util.Map[String, _], b: Boolean): Unit = () + + override def close(): Unit = () + + override def deserialize(key: String, payload: Array[Byte]): ServiceNodeMetadata = { + if (payload == null) { + null + } else { + Serialization.read[ServiceNodeMetadata](new String(payload)) + } + } + } + } + + override def serializer(): Serializer[ServiceNodeMetadata] = { + new Serializer[ServiceNodeMetadata] { + override def configure(map: util.Map[String, _], b: Boolean): Unit = () + + override def serialize(key: String, data: ServiceNodeMetadata): Array[Byte] = { + Serialization.write(data).getBytes("utf-8") + } + + override def close(): Unit = () + } + } + + override def configure(map: util.Map[String, _], b: Boolean): Unit = () + + override def close(): Unit = () +} + +object MetadataStoreBuilder { + def storeBuilder(config: NodeMetadataConfiguration): StoreBuilder[KeyValueStore[String, ServiceNodeMetadata]] = { + Stores.keyValueStoreBuilder( + Stores.inMemoryKeyValueStore(config.topic), + Serdes.String(), + new ServiceNodeMetadataSerde()) + .withCachingEnabled() + .withLoggingDisabled() + } +} diff --git a/service-graph/node-finder/src/main/scala/com/expedia/www/haystack/service/graph/node/finder/model/SpanPair.scala b/service-graph/node-finder/src/main/scala/com/expedia/www/haystack/service/graph/node/finder/model/SpanPair.scala new file mode 100644 index 000000000..df1a05a74 --- /dev/null +++ b/service-graph/node-finder/src/main/scala/com/expedia/www/haystack/service/graph/node/finder/model/SpanPair.scala @@ -0,0 +1,162 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.service.graph.node.finder.model + +import com.expedia.metrics.{MetricData, MetricDefinition, TagCollection} +import com.expedia.www.haystack.commons.entities._ +import com.expedia.www.haystack.service.graph.node.finder.utils.SpanType +import org.apache.commons.lang3.StringUtils +import org.slf4j.LoggerFactory +import scala.collection.JavaConverters._ + +/** + * An instance of SpanPair can contain data from both server and client spans. + * SpanPair is considered "complete" if it has data fields from both server and client span of the same SpanId + */ +class SpanPair { + private val LOGGER = LoggerFactory.getLogger(classOf[SpanPair]) + + private var clientSpan: LightSpan = _ + private var serverSpan: LightSpan = _ + private var isSharedSpan: Boolean = false + + /** + * Returns true of the current instance has data for both server and client spans + * and their services are different + * + * @return true or false + */ + def isComplete: Boolean = { + clientSpan != null && + serverSpan != null && + clientSpan.serviceName != serverSpan.serviceName && + StringUtils.isNotEmpty(serverSpan.serviceName) && + StringUtils.isNotEmpty(clientSpan.serviceName) + } + + /** + * Returns the backing LightSpan objects + * + * @return list of LightSpan objects or an empty list + */ + def getBackingSpans: List[LightSpan] = { + List(clientSpan, serverSpan).filter(w => w != null) + } + + /** + * Merges the given spans into the current instance of the SpanPair using spanType. + * Also, merge them if parent-child relationship is there between given spans + * to produce {@link #getGraphEdge} and {@link #getLatency} data + * + * @param spanOne lightSpan to be merged with the current SpanPair + * @param spanTwo lightSpan to be merged with the current SpanPair + */ + def merge(spanOne: LightSpan, spanTwo: LightSpan): Unit = { + if (spanOne.spanId.equalsIgnoreCase(spanTwo.parentSpanId)) { + setSpans(LightSpanBuilder.updateSpanTypeIfAbsent(spanOne, SpanType.CLIENT), LightSpanBuilder.updateSpanTypeIfAbsent(spanTwo, SpanType.SERVER)) + isSharedSpan = false + } else if (spanOne.parentSpanId.equalsIgnoreCase(spanTwo.spanId)) { + setSpans(LightSpanBuilder.updateSpanTypeIfAbsent(spanOne, SpanType.SERVER), LightSpanBuilder.updateSpanTypeIfAbsent(spanTwo, SpanType.CLIENT)) + isSharedSpan = false + } else { + setSpans(spanOne, spanTwo) + isSharedSpan = true + } + + LOGGER.debug("created a span pair: client: {}, server: {}", List(clientSpan, serverSpan):_*) + } + + /** + * set clientSpan or serverSpan depending upon the value of spanType in LightSpan + * + * @param spanOne span which needs to be set to clientSpan or serverSpan + * @param spanTwo span which needs to be set to clientSpan or serverSpan + */ + private def setSpans(spanOne: LightSpan, spanTwo: LightSpan) = { + Seq(spanOne, spanTwo).foreach(span => + span.spanType match { + case SpanType.CLIENT => + this.clientSpan = span + case SpanType.SERVER => + this.serverSpan = span + case SpanType.OTHER => + } + ) + } + + /** + * Returns an instance of GraphEdge if the current SpanPair is complete. A GraphEdge + * contains the client span's ServiceName, it's OperationName and the corresponding server + * span's ServiceName. These three data points acts as the two nodes and edge of a graph relationship + * + * @return an instance of GraphEdge or None if the current SpanPair is inComplete + */ + def getGraphEdge: Option[GraphEdge] = { + if (isComplete) { + val clientVertex = GraphVertex(clientSpan.serviceName, clientSpan.tags) + val serverVertex = GraphVertex(serverSpan.serviceName, serverSpan.tags) + Some(GraphEdge(clientVertex, serverVertex, clientSpan.operationName, clientSpan.time)) + } else { + None + } + } + + /** + * Returns an instance of MetricPoint that measures the latency of the current Span. Latency of the current + * Span is computed as client span's duration minus it's corresponding server span's duration. MetricPoint instance + * returned will be of type Gauge tagged with the current (client span's) service name and operation name. + * + * @return an instance of MetricPoint or None if the current spanPair instance is incomplete + */ + def getLatency: Option[MetricData] = { + if (isComplete) { + val tags = new TagCollection(Map( + TagKeys.SERVICE_NAME_KEY -> clientSpan.serviceName, + TagKeys.OPERATION_NAME_KEY -> clientSpan.operationName, + MetricDefinition.UNIT -> "ms", + MetricDefinition.MTYPE -> "gauge" + ).asJava) + val metricDefinition = new MetricDefinition("latency", tags, TagCollection.EMPTY) + val metricData = new MetricData(metricDefinition, + (clientSpan.duration - serverSpan.duration)/1000, + clientSpan.time / 1000) + + Some(metricData) + } else { + None + } + } + + def getId: String = s"${clientSpan.spanId}" + def getServerSpan: LightSpan = serverSpan + def getClientSpan: LightSpan = clientSpan + def IsSharedSpan: Boolean = isSharedSpan + + override def toString = s"SpanPair($isComplete, $clientSpan, $serverSpan)" +} + +object SpanPairBuilder { + def createSpanPair(spanOne: LightSpan, spanTwo: LightSpan): SpanPair = { + require(spanOne != null) + require(spanTwo != null) + + val newSpanPair = new SpanPair + newSpanPair.merge(spanOne, spanTwo) + newSpanPair + } +} \ No newline at end of file diff --git a/service-graph/node-finder/src/main/scala/com/expedia/www/haystack/service/graph/node/finder/utils/SpanUtils.scala b/service-graph/node-finder/src/main/scala/com/expedia/www/haystack/service/graph/node/finder/utils/SpanUtils.scala new file mode 100644 index 000000000..76da43820 --- /dev/null +++ b/service-graph/node-finder/src/main/scala/com/expedia/www/haystack/service/graph/node/finder/utils/SpanUtils.scala @@ -0,0 +1,134 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.service.graph.node.finder.utils + +import com.expedia.open.tracing.Span +import com.expedia.www.haystack.service.graph.node.finder.utils.SpanType.SpanType +import org.apache.commons.lang3.StringUtils + +import scala.collection.JavaConverters._ + +/** + * Object with utility methods to process a Span + */ +object SpanUtils { + + val SERVER_SEND_EVENT = "ss" + val SERVER_RECV_EVENT = "sr" + val CLIENT_SEND_EVENT = "cs" + val CLIENT_RECV_EVENT = "cr" + val CLIENT = "client" + val SERVER = "server" + + private val ONE = 1 + private val TWO = 1 << 1 + private val FOUR = 1 << 2 + private val EIGHT = 1 << 3 + + private val THREE = ONE | TWO + private val TWELVE = FOUR | EIGHT + + private val SPAN_MARKERS = Map( + CLIENT_SEND_EVENT -> Flag(ONE), + CLIENT_RECV_EVENT -> Flag(TWO), + SERVER_SEND_EVENT -> Flag(FOUR), + SERVER_RECV_EVENT -> Flag(EIGHT)) + + private val SPAN_TYPE_MAP = Map(Flag(THREE) -> SpanType.CLIENT, Flag(TWELVE) -> SpanType.SERVER) + + /** + * Given a span check if it is eligible for accumulation and can be a light span + * @param span span to validate + * @return + */ + def isAccumulableSpan(span: Span): Boolean = + StringUtils.isNotBlank(span.getSpanId)&& + StringUtils.isNotBlank(span.getServiceName) && + StringUtils.isNotBlank(span.getOperationName) && + span.getStartTime > 0 + + /** + * Given a span, this method looks for ('cs', 'cr') and ('sr', 'ss') pairs in log fields with key as "event" + * to identify a span type. Presence of ('cs', 'cr') events will result in SpanType.CLIENT and presence of + * events ('sr', 'ss') events will result in SpanType.SERVER. All other spans will be identified as OTHER + * @param span Span to identify + * @return Some(SpanType) of the given span or None + */ + def getSpanType(span: Span): SpanType = { + var flag = Flag(0) + span.getLogsList.forEach(log => { + log.getFieldsList.asScala.foreach(tag => { + if (tag.getKey.equalsIgnoreCase("event") && StringUtils.isNotEmpty(tag.getVStr)) { + flag = flag | SPAN_MARKERS.getOrElse(tag.getVStr.toLowerCase, Flag(0)) + } + }) + }) + + // if event log tag is absent in the span object, decide the span type using `span.kind` tag key + // possible values for span.kind are `client` and `server` + // See opentracing conventions + SPAN_TYPE_MAP.getOrElse(flag, { + span.getTagsList.asScala.find(_.getKey == "span.kind").map(_.getVStr.toLowerCase) match { + case Some("client") => SpanType.CLIENT + case Some("server") => SpanType.SERVER + case _ => SpanType.OTHER + } + }) + } + + /** + * Finds the timestamp of the log entry in the given span that has a key named "event" with value that matches + * the given eventValue + * @param span Span from which event timestamp to be read + * @param eventValue value if the "event" field to match + * @return Some(Long) of the timestamp read or None + */ + def getEventTimestamp(span: Span, eventValue: String): Option[Long] = + span.getLogsList.asScala.find(log => { + log.getFieldsList.asScala.exists(tag => { + tag.getKey.equalsIgnoreCase("event") && StringUtils.isNotEmpty(tag.getVStr) && + tag.getVStr.equalsIgnoreCase(eventValue) + }) + }) match { + case Some(log) => Option(log.getTimestamp) + case _ => None + } +} + +/** + * Enum for different span types processed + * by the node finder application + */ +object SpanType extends Enumeration { + type SpanType = Value + val SERVER, CLIENT, OTHER = Value +} + +/** + * Simple case class representing a flag + * @param value : value of the flag + */ +case class Flag(value: Int) { + def | (that: Flag): Flag = Flag(this.value | that.value) + + override def equals(obj: scala.Any): Boolean = { + obj.asInstanceOf[Flag].value == value + } +} + + diff --git a/service-graph/node-finder/src/test/java/org/expedia/www/haystack/commons/scalatest/IntegrationSuite.java b/service-graph/node-finder/src/test/java/org/expedia/www/haystack/commons/scalatest/IntegrationSuite.java new file mode 100644 index 000000000..fe13e09e4 --- /dev/null +++ b/service-graph/node-finder/src/test/java/org/expedia/www/haystack/commons/scalatest/IntegrationSuite.java @@ -0,0 +1,29 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package org.expedia.www.haystack.commons.scalatest; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@org.scalatest.TagAnnotation +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD, ElementType.TYPE}) +public @interface IntegrationSuite { +} diff --git a/service-graph/node-finder/src/test/resources/integration/kafka-server.properties b/service-graph/node-finder/src/test/resources/integration/kafka-server.properties new file mode 100644 index 000000000..860ae817c --- /dev/null +++ b/service-graph/node-finder/src/test/resources/integration/kafka-server.properties @@ -0,0 +1,51 @@ +# The id of the broker. This must be set to a unique integer for each broker. +broker.id=0 + +# The port the socket server listens on +port=9092 + +# The number of threads handling network requests +num.network.threads=2 + +# The number of threads doing disk I/O +num.io.threads=8 + +# The send buffer (SO_SNDBUF) used by the socket server +socket.send.buffer.bytes=1048576 + +# The receive buffer (SO_RCVBUF) used by the socket server +socket.receive.buffer.bytes=1048576 + +# The maximum size of a request that the socket server will accept (protection against OOM) +socket.request.max.bytes=104857600 + +# A comma seperated list of directories under which to store log files +log.dirs=target/kafka-logs + +# The default number of log partitions per topic. More partitions allow greater +# parallelism for consumption, but this will also result in more files across +# the brokers. +num.partitions=1 + +# The minimum age of a log file to be eligible for deletion +log.retention.hours=168 + +# The maximum size of a log segment file. When this size is reached a new log segment will be created. +log.segment.bytes=536870912 + +# The interval at which log segments are checked to see if they can be deleted according +# to the retention policies +log.retention.check.interval.ms=60000 + +# By default the log cleaner is disabled and the log retention policy will default to just delete segments after their retention expires. +# If log.cleaner.enable=true is set the cleaner will be enabled and individual logs can then be marked for log compaction. +log.cleaner.enable=false + +# Timeout in ms for connecting to zookeeper +zookeeper.connection.timeout.ms=1000000 + +#auto create topics +auto.create.topics.enable=true + +default.replication.factor=1 +offsets.topic.replication.factor=1 \ No newline at end of file diff --git a/service-graph/node-finder/src/test/resources/integration/local.conf b/service-graph/node-finder/src/test/resources/integration/local.conf new file mode 100644 index 000000000..7e03852cc --- /dev/null +++ b/service-graph/node-finder/src/test/resources/integration/local.conf @@ -0,0 +1,45 @@ +health.status.path = "target/isHealthy" + +kafka { + close.timeout.ms = 30000 + + streams { + application.id = "haystack-service-graph-node-finder" + bootstrap.servers = "localhost:9092" + num.stream.threads = 1 + request.timeout.ms = 60000 + commit.interval.ms = 3000 + auto.offset.reset = earliest + #timestamp.extractor = "org.apache.kafka.streams.processor.WallclockTimestampExtractor" + } + + producer { + metrics { + topic = "metricpoints" + } + service.call { + topic = "graph-nodes" + } + } + + consumer { + topic = "proto-spans" + } + + accumulator { + interval = 1000 + } + + collectorTags = ["X-HAYSTACK-INFRASTRUCTURE-PROVIDER", "tier"] + + node.metadata { + topic { + autocreate = false + name = "haystack-node-finder-metadata" + partition.count = 1 + replication.factor = 1 + } + } +} + +haystack.graphite.host = "monitoring-influxdb-graphite.kube-system.svc" diff --git a/service-graph/node-finder/src/test/resources/integration/zookeeper.properties b/service-graph/node-finder/src/test/resources/integration/zookeeper.properties new file mode 100644 index 000000000..c3e355615 --- /dev/null +++ b/service-graph/node-finder/src/test/resources/integration/zookeeper.properties @@ -0,0 +1,6 @@ +# the directory where the snapshot is stored. +dataDir=target +# the port at which the clients will connect +clientPort=2181 +# disable the per-ip limit on the number of connections since this is a non-production config +maxClientCnxns=0 \ No newline at end of file diff --git a/service-graph/node-finder/src/test/resources/log4j.properties b/service-graph/node-finder/src/test/resources/log4j.properties new file mode 100644 index 000000000..fa7f75bf8 --- /dev/null +++ b/service-graph/node-finder/src/test/resources/log4j.properties @@ -0,0 +1,5 @@ +log4j.rootLogger=OFF, stdout + +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=[%d] %p %m (%c)%n \ No newline at end of file diff --git a/service-graph/node-finder/src/test/resources/logback-test.xml b/service-graph/node-finder/src/test/resources/logback-test.xml new file mode 100644 index 000000000..38a30a589 --- /dev/null +++ b/service-graph/node-finder/src/test/resources/logback-test.xml @@ -0,0 +1,24 @@ + + + + + + + true + + + + + + %d{yyyy-MM-dd HH:mm:ss:SSS} %thread, %level, %logger{70}, "%msg" %replace(%ex){'[\n]+', '\\n'}%nopex%n + + + + + + + + + + + diff --git a/service-graph/node-finder/src/test/resources/test/test.conf b/service-graph/node-finder/src/test/resources/test/test.conf new file mode 100644 index 000000000..e66482268 --- /dev/null +++ b/service-graph/node-finder/src/test/resources/test/test.conf @@ -0,0 +1,44 @@ +health.status.path = "/app/isHealthy" + +kafka { + close.timeout.ms = 30000 + + streams { + application.id = "haystack-service-graph-node-finder" + bootstrap.servers = "kafkasvc:9092" + num.stream.threads = 4 + request.timeout.ms = 60000 + commit.interval.ms = 3000 + auto.offset.reset = latest + #timestamp.extractor = "org.apache.kafka.streams.processor.WallclockTimestampExtractor" + } + + producer { + metrics { + topic = "metricpoints" + } + service.call { + topic = "graph-nodes" + } + } + + consumer { + topic = "proto-spans" + } + + accumulator { + interval = 60000 + } + collectorTags = ["X-HAYSTACK-INFRASTRUCTURE-PROVIDER", "tier"] + + node.metadata { + topic { + autocreate = false + name = "haystack-node-finder-metadata" + partition.count = 6 + replication.factor = 2 + } + } +} + +haystack.graphite.host = "monitoring-influxdb-graphite.kube-system.svc" diff --git a/service-graph/node-finder/src/test/resources/test/test_no_app_id.conf b/service-graph/node-finder/src/test/resources/test/test_no_app_id.conf new file mode 100644 index 000000000..2147acd1f --- /dev/null +++ b/service-graph/node-finder/src/test/resources/test/test_no_app_id.conf @@ -0,0 +1,33 @@ +health.status.path = "/app/isHealthy" + +kafka { + close.timeout.ms = 30000 + + streams { + bootstrap.servers = "kafkasvc:9092" + num.stream.threads = 4 + request.timeout.ms = 60000 + commit.interval.ms = 3000 + auto.offset.reset = latest + #timestamp.extractor = "org.apache.kafka.streams.processor.WallclockTimestampExtractor" + } + + producer { + metrics { + topic = "metricpoints" + } + service.call { + topic = "service-calls" + } + } + + consumer { + topic = "proto-spans" + } + + accumulator { + interval = 60000 + } +} + +haystack.graphite.host = "monitoring-influxdb-graphite.kube-system.svc" diff --git a/service-graph/node-finder/src/test/resources/test/test_no_bootstrap.conf b/service-graph/node-finder/src/test/resources/test/test_no_bootstrap.conf new file mode 100644 index 000000000..cbd5c15a7 --- /dev/null +++ b/service-graph/node-finder/src/test/resources/test/test_no_bootstrap.conf @@ -0,0 +1,33 @@ +health.status.path = "/app/isHealthy" + +kafka { + close.timeout.ms = 30000 + + streams { + application.id = "haystack-service-graph-node-finder" + num.stream.threads = 4 + request.timeout.ms = 60000 + commit.interval.ms = 3000 + auto.offset.reset = latest + #timestamp.extractor = "org.apache.kafka.streams.processor.WallclockTimestampExtractor" + } + + producer { + metrics { + topic = "metricpoints" + } + service.call { + topic = "service-calls" + } + } + + consumer { + topic = "proto-spans" + } + + accumulator { + interval = 60000 + } +} + +haystack.graphite.host = "monitoring-influxdb-graphite.kube-system.svc" diff --git a/service-graph/node-finder/src/test/resources/test/test_no_consumer.conf b/service-graph/node-finder/src/test/resources/test/test_no_consumer.conf new file mode 100644 index 000000000..0a360d21e --- /dev/null +++ b/service-graph/node-finder/src/test/resources/test/test_no_consumer.conf @@ -0,0 +1,30 @@ +health.status.path = "/app/isHealthy" + +kafka { + close.timeout.ms = 30000 + + streams { + application.id = "haystack-service-graph-node-finder" + bootstrap.servers = "kafkasvc:9092" + num.stream.threads = 4 + request.timeout.ms = 60000 + commit.interval.ms = 3000 + auto.offset.reset = latest + #timestamp.extractor = "org.apache.kafka.streams.processor.WallclockTimestampExtractor" + } + + producer { + metrics { + topic = "metricpoints" + } + service.call { + topic = "service-calls" + } + } + + accumulator { + interval = 60000 + } +} + +haystack.graphite.host = "monitoring-influxdb-graphite.kube-system.svc" diff --git a/service-graph/node-finder/src/test/resources/test/test_no_metrics_topic.conf b/service-graph/node-finder/src/test/resources/test/test_no_metrics_topic.conf new file mode 100644 index 000000000..3b6b93ea8 --- /dev/null +++ b/service-graph/node-finder/src/test/resources/test/test_no_metrics_topic.conf @@ -0,0 +1,31 @@ +health.status.path = "/app/isHealthy" + +kafka { + close.timeout.ms = 30000 + + streams { + application.id = "haystack-service-graph-node-finder" + bootstrap.servers = "kafkasvc:9092" + num.stream.threads = 4 + request.timeout.ms = 60000 + commit.interval.ms = 3000 + auto.offset.reset = latest + #timestamp.extractor = "org.apache.kafka.streams.processor.WallclockTimestampExtractor" + } + + producer { + service.call { + topic = "service-calls" + } + } + + consumer { + topic = "proto-spans" + } + + accumulator { + interval = 60000 + } +} + +haystack.graphite.host = "monitoring-influxdb-graphite.kube-system.svc" diff --git a/service-graph/node-finder/src/test/resources/test/test_no_producer.conf b/service-graph/node-finder/src/test/resources/test/test_no_producer.conf new file mode 100644 index 000000000..0ae6d8ec0 --- /dev/null +++ b/service-graph/node-finder/src/test/resources/test/test_no_producer.conf @@ -0,0 +1,25 @@ +health.status.path = "/app/isHealthy" + +kafka { + close.timeout.ms = 30000 + + streams { + application.id = "haystack-service-graph-node-finder" + bootstrap.servers = "kafkasvc:9092" + num.stream.threads = 4 + request.timeout.ms = 60000 + commit.interval.ms = 3000 + auto.offset.reset = latest + #timestamp.extractor = "org.apache.kafka.streams.processor.WallclockTimestampExtractor" + } + + consumer { + topic = "proto-spans" + } + + accumulator { + interval = 60000 + } +} + +haystack.graphite.host = "monitoring-influxdb-graphite.kube-system.svc" diff --git a/service-graph/node-finder/src/test/scala/com/expedia/www/haystack/TestSpec.scala b/service-graph/node-finder/src/test/scala/com/expedia/www/haystack/TestSpec.scala new file mode 100644 index 000000000..569d500f7 --- /dev/null +++ b/service-graph/node-finder/src/test/scala/com/expedia/www/haystack/TestSpec.scala @@ -0,0 +1,189 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack + +import java.util.UUID +import java.util.concurrent.TimeUnit + +import com.expedia.open.tracing.Tag.TagType +import com.expedia.open.tracing.{Log, Span, Tag} +import com.expedia.www.haystack.service.graph.node.finder.model.{LightSpan, SpanPair, SpanPairBuilder} +import com.expedia.www.haystack.service.graph.node.finder.utils.SpanType.SpanType +import com.expedia.www.haystack.service.graph.node.finder.utils.{SpanType, SpanUtils} +import org.scalatest.easymock.EasyMockSugar +import org.scalatest.{FunSpec, GivenWhenThen, Matchers} + +trait TestSpec extends FunSpec with GivenWhenThen with Matchers with EasyMockSugar { + private val DEFAULT_START_TIME = System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(5) + private val DEFAULT_DURATION_MICROS = TimeUnit.MILLISECONDS.toMicros(100) + + def newLightSpan(spanId: String, parentSpanId: String, serviceName: String, operName: String, spanType: SpanType): LightSpan = { + LightSpan(spanId, parentSpanId, System.currentTimeMillis(), serviceName, operName, 1000, spanType, Map()) + } + + def newLightSpan(spanId: String, parentSpanId: String, serviceName: String, operationName: String, startTimeInMillis: Long, + duration: Long, spanType: SpanType, tags: Map[String, String] = Map()): LightSpan = { + LightSpan(spanId, parentSpanId, startTimeInMillis, serviceName, operationName, duration, spanType, tags) + } + + def randomLightSpan(): LightSpan = { + LightSpan(UUID.randomUUID().toString, UUID.randomUUID().toString, System.currentTimeMillis(), "svc", "oper", 1000, SpanType.CLIENT, Map()) + } + + def newSpan(spanId: String, parentSpanId: String, serviceName: String): Span = { + newSpan(spanId, parentSpanId, serviceName, "oper", DEFAULT_DURATION_MICROS, client = false, server = false)._1 + } + + def newServerSpan(spanId: String, parentSpanId: String, serviceName: String): Span = { + newSpan(spanId, parentSpanId, serviceName, "oper", DEFAULT_DURATION_MICROS, client = false, server = true)._1 + } + + def newClientSpan(spanId: String, parentSpanId: String, serviceName: String): Span = { + newSpan(spanId, parentSpanId, serviceName, "oper", DEFAULT_DURATION_MICROS, client = true, server = false)._1 + } + + def newClientSpan(spanId: String, parentSpanId: String, serviceName: String, startTime: Long, duration: Long): Span = { + newSpan(spanId, parentSpanId, startTime, serviceName, "oper", duration, client=true, server=false)._1 + } + + def newServerSpan(spanId: String, parentSpanId: String, serviceName: String, startTime: Long, duration: Long): Span = { + newSpan(spanId, parentSpanId, startTime, serviceName, "oper", duration, client=false, server=true)._1 + } + + def newSpan(serviceName: String, operation: String, duration: Long, client: Boolean, server: Boolean): (Span, SpanType) = { + newSpan(UUID.randomUUID().toString, UUID.randomUUID().toString, serviceName, operation, duration, client, server) + } + + def newSpan(spanId: String, parentSpanId: String, serviceName: String, operation: String, duration: Long, client: Boolean, server: Boolean): (Span, SpanType) = { + newSpan(spanId, parentSpanId, DEFAULT_START_TIME, serviceName, operation, duration, client, server) + } + + def newSpan(spanId: String, parentSpanId: String, ts: Long, serviceName: String, operation: String, duration: Long, client: Boolean, + server: Boolean, tags: Map[String, String] = Map()): (Span, SpanType) = { + val spanBuilder = Span.newBuilder() + spanBuilder.setTraceId(UUID.randomUUID().toString) + spanBuilder.setSpanId(spanId) + spanBuilder.setParentSpanId(parentSpanId) + spanBuilder.setServiceName(serviceName) + spanBuilder.setOperationName(operation) + spanBuilder.setStartTime(ts * 1000) //microseconds + spanBuilder.setDuration(duration) + var spanType = SpanType.OTHER + + val logBuilder = Log.newBuilder() + if (client) { + logBuilder.setTimestamp(ts) + logBuilder.addFields(Tag.newBuilder().setKey("event").setVStr(SpanUtils.CLIENT_SEND_EVENT).build()) + spanBuilder.addLogs(logBuilder.build()) + logBuilder.clear() + logBuilder.setTimestamp(ts + duration) + logBuilder.addFields(Tag.newBuilder().setKey("event").setVStr(SpanUtils.CLIENT_RECV_EVENT).build()) + spanBuilder.addLogs(logBuilder.build()) + spanType = SpanType.CLIENT + spanBuilder.addTags(Tag.newBuilder().setKey("span.kind").setVStr("client")) + } + + if (server) { + logBuilder.setTimestamp(ts) + logBuilder.addFields(Tag.newBuilder().setKey("event").setVStr(SpanUtils.SERVER_RECV_EVENT).build()) + spanBuilder.addLogs(logBuilder.build()) + logBuilder.clear() + logBuilder.setTimestamp(ts + duration) + logBuilder.addFields(Tag.newBuilder().setKey("event").setVStr(SpanUtils.SERVER_SEND_EVENT).build()) + spanBuilder.addLogs(logBuilder.build()) + spanType = SpanType.SERVER + spanBuilder.addTags(Tag.newBuilder().setKey("span.kind").setVStr("server")) + } + + if (tags.nonEmpty) { + val tagBuilder = Tag.newBuilder() + tags.foreach(tag => { + tagBuilder.setKey(tag._1).setVStr(tag._2).setType(TagType.STRING) + spanBuilder.addTags(tagBuilder.build()) + tagBuilder.clear() + }) + } + + (spanBuilder.build(), spanType) + } + + def produceSimpleSpan(offset: Long, callback: (Span) => Unit): Unit = + callback(newSpan(UUID.randomUUID().toString, + UUID.randomUUID().toString, + System.currentTimeMillis() - offset, + "foo-service", "bar", 1500, client = false, server = false)._1) + + def produceClientSpan(offset: Long, callback: (Span) => Unit): Unit = + callback(newSpan(UUID.randomUUID().toString, + UUID.randomUUID().toString, + System.currentTimeMillis() - offset, + "foo-service", "bar", 1500, client = true, server = false)._1) + + def produceServerSpan(offset: Long, callback: (Span) => Unit): Unit = + callback(newSpan(UUID.randomUUID().toString, + UUID.randomUUID().toString, + System.currentTimeMillis() - offset, + "baz-service", "bar", 500, client = false, server = true)._1) + + def produceClientAndServerSpans(offset: Long, callback: (Span) => Unit): Unit = { + val clientSend = System.currentTimeMillis() - offset + val serverReceive = clientSend + 500 + val spanId = UUID.randomUUID().toString + val parentSpanId = UUID.randomUUID().toString + val source = "foo-service" + val op = "bar" + val dest = "baz-service" + val (clientSpan, _) = newSpan(spanId, parentSpanId, clientSend, source, op, 1500, client = true, server = false) + val (serverSpan, _) = newSpan(spanId, parentSpanId, serverReceive, dest, op, 500, client = false, server = true) + callback(clientSpan) + callback(serverSpan) + } + + def writeSpans(count: Int, + startOffset: Long, + producer: (Long, (Span) => Unit) => Unit, + consumer: (Span) => Unit): Unit = { + require(count >= 1) + var i = count + while (i >= 1) { + producer(i * startOffset, consumer) + i -= 1 + } + } + + def invalidSpanPair(): SpanPair = { + val spanId = UUID.randomUUID().toString + val parentSpanId = UUID.randomUUID().toString + val clientLightSpan = newLightSpan(spanId, parentSpanId, "foo-service", "bar", System.currentTimeMillis(), 1000, SpanType.CLIENT) + val anotherClientLightSpan = newLightSpan(spanId, parentSpanId, "foo-service", "bar", System.currentTimeMillis(), 1000, SpanType.CLIENT) + val spanPair = SpanPairBuilder.createSpanPair(clientLightSpan, anotherClientLightSpan) + spanPair + } + + def validSpanPair(tags: Map[String, String] = Map()): SpanPair = { + val clientSend = System.currentTimeMillis() + val serverReceive = clientSend + 500 + val spanId = UUID.randomUUID().toString + val parentSpanId = UUID.randomUUID().toString + val clientLightSpan = newLightSpan(spanId, parentSpanId, "foo-service", "bar", clientSend, 1500, SpanType.CLIENT, tags) + val serverLightSpan = newLightSpan(spanId, parentSpanId, "baz-service", "bar", serverReceive, 500, SpanType.SERVER, tags) + + val spanPair = SpanPairBuilder.createSpanPair(clientLightSpan, serverLightSpan) + spanPair + } +} diff --git a/service-graph/node-finder/src/test/scala/com/expedia/www/haystack/commons/kafka/KafkaController.scala b/service-graph/node-finder/src/test/scala/com/expedia/www/haystack/commons/kafka/KafkaController.scala new file mode 100644 index 000000000..a35c52849 --- /dev/null +++ b/service-graph/node-finder/src/test/scala/com/expedia/www/haystack/commons/kafka/KafkaController.scala @@ -0,0 +1,100 @@ +package com.expedia.www.haystack.commons.kafka + +import java.util.Properties +import java.util.concurrent.TimeUnit + +import kafka.server.RunningAsBroker +import org.apache.kafka.clients.CommonClientConfigs +import org.apache.kafka.clients.admin.{AdminClient, NewTopic} +import org.apache.kafka.clients.consumer.{ConsumerConfig, KafkaConsumer} +import org.apache.kafka.clients.producer.{KafkaProducer, ProducerConfig} +import org.apache.kafka.common.serialization.{Deserializer, Serializer} +import org.slf4j.LoggerFactory + +import scala.collection.JavaConverters._ +import scala.util.Try + +class KafkaController(kafkaProperties: Properties, zooKeeperProperties: Properties) { + require(kafkaProperties != null) + require(zooKeeperProperties != null) + + private val LOGGER = LoggerFactory.getLogger(classOf[KafkaController]) + + private val zkPort = zooKeeperProperties.getProperty("clientPort").toInt + private val kafkaPort = kafkaProperties.getProperty("port").toInt + + lazy val zkUrl: String = "localhost:" + zkPort + lazy val kafkaUrl: String = "localhost:" + kafkaPort + + private val kafkaPropertiesWithZk = new Properties + kafkaPropertiesWithZk.putAll(kafkaProperties) + kafkaPropertiesWithZk.put("zookeeper.connect", zkUrl) + private val kafkaServer = new KafkaLocal(kafkaPropertiesWithZk) + + def startService(): Unit = { + //start zk + val zookeeper = new ZooKeeperLocal(zooKeeperProperties) + new Thread(zookeeper).start() + + //start kafka + kafkaServer.start() + Thread.sleep(1000) + if (kafkaServer.state().currentState != RunningAsBroker.state) { + throw new IllegalStateException("Kafka server is not in a running state") + } + + //lifecycle message + LOGGER.info("Kafka started and listening : {}", kafkaUrl) + } + + def stopService(): Unit = { + //stop kafka + kafkaServer.stop() + + //lifecycle message + LOGGER.info("Kafka stopped") + } + + def createTopics(topics: List[String]): Unit = { + if (topics.nonEmpty) { + val adminClient = AdminClient.create(getBootstrapProperties) + try { + adminClient.createTopics(topics.map(topic => new NewTopic(topic, 1, 1)).asJava) + adminClient.listTopics().names().get().forEach(s => LOGGER.info("Available topic : {}", s)) + } + finally { + Try(adminClient.close(5, TimeUnit.SECONDS)) + } + } + } + + def createProducer[K, V] (topic: String, keySerializer: Class[_ <: Serializer[K]], + valueSerializer: Class[_ <: Serializer[V]]) : KafkaProducer[K, V] = { + val properties = getBootstrapProperties + properties.put(ProducerConfig.CLIENT_ID_CONFIG, topic + "Producer") + properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, keySerializer.getName) + properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, valueSerializer.getName) + new KafkaProducer[K, V](properties) + } + + def createConsumer[K, V] (topic: String, keySerializer: Class[_ <: Deserializer[K]], + valueSerializer: Class[_ <: Deserializer[V]]) : KafkaConsumer[K, V] = { + val properties = getBootstrapProperties + properties.put(ConsumerConfig.CLIENT_ID_CONFIG, topic + "Consumer") + properties.put(ConsumerConfig.GROUP_ID_CONFIG, topic + "ConsumerGroup") + properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, keySerializer.getName) + properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, valueSerializer.getName) + properties.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest") + val consumer = new KafkaConsumer[K, V](properties) + consumer.subscribe(List(topic).asJava) + consumer + } + + private def getBootstrapProperties: Properties = { + val properties = new Properties() + properties.put(CommonClientConfigs.BOOTSTRAP_SERVERS_CONFIG, List(kafkaUrl).asJava) + properties + } +} + +class InvalidStateException(message: String) extends RuntimeException(message) {} diff --git a/service-graph/node-finder/src/test/scala/com/expedia/www/haystack/commons/kafka/KafkaLocal.scala b/service-graph/node-finder/src/test/scala/com/expedia/www/haystack/commons/kafka/KafkaLocal.scala new file mode 100644 index 000000000..0dce343a2 --- /dev/null +++ b/service-graph/node-finder/src/test/scala/com/expedia/www/haystack/commons/kafka/KafkaLocal.scala @@ -0,0 +1,23 @@ +package com.expedia.www.haystack.commons.kafka + +import java.util.Properties + +import kafka.metrics.KafkaMetricsReporter +import kafka.server.{BrokerState, KafkaConfig, KafkaServer} + +class KafkaLocal(val kafkaProperties: Properties) { + val kafkaConfig: KafkaConfig = KafkaConfig.fromProps(kafkaProperties) + val kafka: KafkaServer = new KafkaServer(kafkaConfig, kafkaMetricsReporters = List[KafkaMetricsReporter]()) + + def start(): Unit = { + kafka.startup() + } + + def stop(): Unit = { + kafka.shutdown() + } + + def state(): BrokerState = { + kafka.brokerState + } +} diff --git a/service-graph/node-finder/src/test/scala/com/expedia/www/haystack/commons/kafka/ZooKeeperLocal.scala b/service-graph/node-finder/src/test/scala/com/expedia/www/haystack/commons/kafka/ZooKeeperLocal.scala new file mode 100644 index 000000000..11b0da66b --- /dev/null +++ b/service-graph/node-finder/src/test/scala/com/expedia/www/haystack/commons/kafka/ZooKeeperLocal.scala @@ -0,0 +1,31 @@ +package com.expedia.www.haystack.commons.kafka + +import java.io.IOException +import java.util.Properties + +import org.apache.zookeeper.server.quorum.QuorumPeerConfig +import org.apache.zookeeper.server.{ServerConfig, ZooKeeperServerMain} +import org.slf4j.LoggerFactory + + +object ZooKeeperLocal { + private val LOGGER = LoggerFactory.getLogger(classOf[ZooKeeperLocal]) +} + +class ZooKeeperLocal(val zkProperties: Properties) extends Runnable { + private val quorumConfiguration = new QuorumPeerConfig + quorumConfiguration.parseProperties(zkProperties) + private val configuration = new ServerConfig + configuration.readFrom(quorumConfiguration) + private val zooKeeperServer = new ZooKeeperServerMain + + override def run(): Unit = { + try { + zooKeeperServer.runFromConfig(configuration) + } + catch { + case e: IOException => + ZooKeeperLocal.LOGGER.error("Zookeeper startup failed.", e) + } + } +} diff --git a/service-graph/node-finder/src/test/scala/com/expedia/www/haystack/service/graph/node/finder/AppSpec.scala b/service-graph/node-finder/src/test/scala/com/expedia/www/haystack/service/graph/node/finder/AppSpec.scala new file mode 100644 index 000000000..1d37a3b6c --- /dev/null +++ b/service-graph/node-finder/src/test/scala/com/expedia/www/haystack/service/graph/node/finder/AppSpec.scala @@ -0,0 +1,142 @@ +package com.expedia.www.haystack.service.graph.node.finder +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +import java.util.Properties + +import com.expedia.open.tracing.Span +import com.expedia.www.haystack.TestSpec +import com.expedia.www.haystack.commons.health.HealthStatusController +import com.expedia.www.haystack.commons.kafka.{InvalidStateException, KafkaController} +import com.expedia.www.haystack.commons.kstreams.app.StateChangeListener +import com.expedia.www.haystack.commons.kstreams.serde.SpanSerializer +import com.expedia.www.haystack.commons.kstreams.serde.metricdata.MetricDataDeserializer +import com.expedia.www.haystack.service.graph.node.finder.config.AppConfiguration +import com.expedia.www.haystack.service.graph.node.finder.utils.SpanUtils +import org.apache.kafka.clients.producer.{KafkaProducer, ProducerRecord} +import org.apache.kafka.common.serialization.{StringDeserializer, StringSerializer} +import org.apache.kafka.streams.KafkaStreams +import org.expedia.www.haystack.commons.scalatest.IntegrationSuite +import org.scalatest.BeforeAndAfter +import org.slf4j.LoggerFactory + +@IntegrationSuite +class AppSpec extends TestSpec with BeforeAndAfter { + + private val LOGGER = LoggerFactory.getLogger(classOf[AppSpec]) + + private val appConfig = new AppConfiguration("integration/local.conf") + private val stateChangeListener = new ExtendedStateChangeListener(new HealthStatusController) + private val streamsRunner = App.createStreamsRunner(appConfig, stateChangeListener) + + val kafkaController: KafkaController = createKafkaController() + + before { + //start kafka and zk + kafkaController.startService() + + //ensure test topics are present + kafkaController.createTopics(List(appConfig.kafkaConfig.protoSpanTopic, + appConfig.kafkaConfig.serviceCallTopic, appConfig.kafkaConfig.metricsTopic, appConfig.kafkaConfig.metadataConfig.topic)) + + //start topology + streamsRunner.start() + + //time for kstreams to initialize completely· + waitForStreams() + } + + describe("node finder application") { + it("should process spans from kafka and produce latency metrics and graph edges") { + //send test data to source topic + val producer = kafkaController.createProducer(appConfig.kafkaConfig.protoSpanTopic, + classOf[StringSerializer], classOf[SpanSerializer]) + + //send sample data + sendRecords(producer, 5) + + //read data from output topics + LOGGER.info(s"Consuming topics ${appConfig.kafkaConfig.metricsTopic} and ${appConfig.kafkaConfig.serviceCallTopic}") + val metricsConsumer = kafkaController.createConsumer(appConfig.kafkaConfig.metricsTopic, + classOf[StringDeserializer], classOf[MetricDataDeserializer]) + val metricRecords = metricsConsumer.poll(5000) + + val graphConsumer = kafkaController.createConsumer(appConfig.kafkaConfig.serviceCallTopic, + classOf[StringDeserializer], classOf[StringDeserializer]) + val graphRecords = graphConsumer.poll(5000) + + //check if they are as expected + metricRecords.count() should be(5) + graphRecords.count() should be(5) + } + } + + after { + //stop topology + streamsRunner.close() + + //stop kafka and zk + kafkaController.stopService() + } + + private def createKafkaController() : KafkaController = { + val zkProperties = new Properties + zkProperties.load(classOf[AppSpec].getClassLoader.getResourceAsStream("integration/zookeeper.properties")) + + val kafkaProperties = new Properties + kafkaProperties.load(classOf[AppSpec].getClassLoader.getResourceAsStream("integration/kafka-server.properties")) + + new KafkaController(kafkaProperties, zkProperties) + } + + private def waitForStreams(): Unit = { + while (!stateChangeListener.currentState.isRunning && + (stateChangeListener.currentState == KafkaStreams.State.CREATED)) Thread.sleep(100) + + if (!stateChangeListener.currentState.isRunning) { + throw new InvalidStateException(stateChangeListener.currentState + " is not expected after startup") + } + } + + private def sendRecords(producer: KafkaProducer[String, Span], count: Int) : Unit = { + val writer: (Span) => Unit = span => { + producer.send(new ProducerRecord[String, Span](appConfig.kafkaConfig.protoSpanTopic, span.getSpanId, span)) + LOGGER.info("sent {} span {} : {}", SpanUtils.getSpanType(span).toString, span.getSpanId, span.getStartTime.toString) + } + + //send 5 simple spans, 5 client spans, 5 server span and 5 client-server span combinations + for (_ <- 1 to count) produceClientSpan(10000, writer) + for (_ <- 1 to count) produceServerSpan(9000, writer) + for (_ <- 1 to count) produceClientAndServerSpans(8000, writer) + for (_ <- 1 to count) produceSimpleSpan(5000, writer) + producer.flush() + + //sleep for 30 seconds for streams to process. This is probably too much for local - + //but depending on the compute in build servers this time varies + Thread.sleep(30000) + } + + class ExtendedStateChangeListener(healthStatusController: HealthStatusController) + extends StateChangeListener(healthStatusController) { + var currentState: KafkaStreams.State = _ + + override def onChange(newState: KafkaStreams.State, oldState: KafkaStreams.State): Unit = { + super.onChange(newState, oldState) + currentState = newState + } + } +} diff --git a/service-graph/node-finder/src/test/scala/com/expedia/www/haystack/service/graph/node/finder/app/GraphNodeProducerSpec.scala b/service-graph/node-finder/src/test/scala/com/expedia/www/haystack/service/graph/node/finder/app/GraphNodeProducerSpec.scala new file mode 100644 index 000000000..280793bb8 --- /dev/null +++ b/service-graph/node-finder/src/test/scala/com/expedia/www/haystack/service/graph/node/finder/app/GraphNodeProducerSpec.scala @@ -0,0 +1,76 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.service.graph.node.finder.app + +import com.expedia.www.haystack.TestSpec +import com.expedia.www.haystack.commons.entities.GraphEdge +import org.apache.kafka.streams.processor.ProcessorContext +import org.easymock.EasyMock._ + +class GraphNodeProducerSpec extends TestSpec { + describe("producing graph nodes") { + it("should emit a valid graph node for a give complete SpanPair") { + Given("a valid SpanPair instance") + val spanPair = validSpanPair(Map("testtag" -> "true")) + val context = mock[ProcessorContext] + val graphNodeProducer = new GraphNodeProducer + val captured = newCapture[GraphEdge]() + When("process is called on GraphNodeProducer with it") + expecting { + context.forward(anyString(), capture[GraphEdge](captured)).once() + context.commit().once() + } + replay(context) + graphNodeProducer.init(context) + graphNodeProducer.process(spanPair.getId, spanPair) + val edge = captured.getValue + Then("it should produce a valid GraphNode object") + verify(context) + edge.source.name should be("foo-service") + edge.destination.name should be("baz-service") + edge.operation should be("bar") + edge.source.tags.get("testtag") shouldBe Some("true") + edge.destination.tags.get("testtag") shouldBe Some("true") + } + it("should emit no graph nodes for invalid light spans") { + Given("an incomplete SpanPair instance") + val spanPair = invalidSpanPair() + val context = mock[ProcessorContext] + val graphNodeProducer = new GraphNodeProducer + When("process is called on GraphNodeProducer with it") + expecting { + context.commit().once() + } + replay(context) + graphNodeProducer.init(context) + graphNodeProducer.process(spanPair.getId, spanPair) + Then("it should produce no graph node in the context") + verify(context) + } + } + describe("graph node producer supplier") { + it("should supply a valid producer") { + Given("a supplier instance") + val supplier = new GraphNodeProducerSupplier + When("a producer is request") + val producer = supplier.get() + Then("should yield a valid producer") + producer should not be null + } + } +} diff --git a/service-graph/node-finder/src/test/scala/com/expedia/www/haystack/service/graph/node/finder/app/LatencyProducerSpec.scala b/service-graph/node-finder/src/test/scala/com/expedia/www/haystack/service/graph/node/finder/app/LatencyProducerSpec.scala new file mode 100644 index 000000000..1537a596d --- /dev/null +++ b/service-graph/node-finder/src/test/scala/com/expedia/www/haystack/service/graph/node/finder/app/LatencyProducerSpec.scala @@ -0,0 +1,69 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.service.graph.node.finder.app + +import com.expedia.metrics.MetricData +import com.expedia.www.haystack.TestSpec +import org.apache.kafka.streams.processor.ProcessorContext +import org.easymock.EasyMock._ + +class LatencyProducerSpec extends TestSpec { + describe("latency producer") { + it("should produce latency metric for complete SpanPair") { + Given("a valid SpanPair instance") + val spanPair = validSpanPair() + val context = mock[ProcessorContext] + val latencyProducer = new LatencyProducer + When("process is invoked with a complete SpanPair") + expecting { + context.forward(anyString(), isA(classOf[MetricData])).once() + context.commit().once() + } + replay(context) + latencyProducer.init(context) + latencyProducer.process(spanPair.getId, spanPair) + Then("it should produce a metric point in the context") + verify(context) + } + it("should produce no metrics for invalid SpanPair") { + Given("an incomplete SpanPair instance") + val spanPair = invalidSpanPair() + val context = mock[ProcessorContext] + val latencyProducer = new LatencyProducer + When("process is invoked with a complete SpanPair") + expecting { + context.commit().once() + } + replay(context) + latencyProducer.init(context) + latencyProducer.process(spanPair.getId, spanPair) + Then("it should produce no metric points in the context") + verify(context) + } + } + describe("latency producer supplier") { + it("should supply a valid producer") { + Given("a supplier instance") + val supplier = new LatencyProducerSupplier + When("a producer is request") + val producer = supplier.get() + Then("should yield a valid producer") + producer should not be null + } + } +} diff --git a/service-graph/node-finder/src/test/scala/com/expedia/www/haystack/service/graph/node/finder/app/SpanAccumulatorSpec.scala b/service-graph/node-finder/src/test/scala/com/expedia/www/haystack/service/graph/node/finder/app/SpanAccumulatorSpec.scala new file mode 100644 index 000000000..a19222a0f --- /dev/null +++ b/service-graph/node-finder/src/test/scala/com/expedia/www/haystack/service/graph/node/finder/app/SpanAccumulatorSpec.scala @@ -0,0 +1,456 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.service.graph.node.finder.app + +import java.util.concurrent.TimeUnit + +import com.expedia.open.tracing.Span +import com.expedia.www.haystack.TestSpec +import com.expedia.www.haystack.commons.graph.GraphEdgeTagCollector +import com.expedia.www.haystack.service.graph.node.finder.model.{ServiceNodeMetadata, SpanPair} +import org.apache.kafka.streams.processor._ +import org.apache.kafka.streams.state.{KeyValueStore, Stores} +import org.easymock.EasyMock +import org.easymock.EasyMock._ + +import scala.collection.mutable + +class SpanAccumulatorSpec extends TestSpec { + private val storeName = "my-store" + private val DEFAULT_ACCUMULATE_INTERVAL_MILLIS = TimeUnit.SECONDS.toMillis(2) + + describe("a span accumulator") { + it("should schedule Punctuator on init") { + Given("a processor context") + val (context, _, _, _) = mockContext(0) + When("accumulator is initialized") + createAccumulator(context) + Then("it should schedule punctuation") + verify(context) + } + + it("should collect all Client or Server Spans provided for processing") { + Given("an accumulator") + val accumulator = new SpanAccumulator(storeName, 1000, new GraphEdgeTagCollector()) + When("10 server, 10 client and 10 other spans are processed") + val producers = List[(Long, (Span) => Unit) => Unit](produceSimpleSpan, + produceServerSpan, produceClientSpan) + producers.foreach(producer => writeSpans(10, 200, producer, (span) => accumulator.process(span.getSpanId, span))) + Then("accumulator should hold only the 10 client and 10 server spans") + accumulator.spanCount should be(30) + } + + it("should emit SpanPair instances only for pairs of server and client spans") { + Given("an accumulator and initialized with a processor context") + val (context, _, _, _) = mockContext(10) + val accumulator = createAccumulator(context) + And("50 spans are written to it, with 10 client, 10 server, 10 other and 10 pairs of server and client") + val producers = List[(Long, (Span) => Unit) => Unit](produceSimpleSpan, + produceServerSpan, produceClientSpan, produceClientAndServerSpans) + producers.foreach(producer => writeSpans(10, 2500, producer, (span) => accumulator.process(span.getSpanId, span))) + When("punctuate is called") + accumulator.getPunctuator(context).punctuate(System.currentTimeMillis()) + + Then("it should produce 10 SpanPair instances as expected") + verify(context) + And("the accumulator's collection should be empty") + accumulator.spanCount should be(0) + } + } + + describe("create span pair using ids") { + it("should emit SpanPair instances for parent-child relation using ids") { + Given("an accumulator and initialized with a processor context") + val (context, kvStore, _, _) = mockContext(4) + val accumulator = createAccumulator(context) + + And("spans from 5 services") + val spanList = List( + newSpan("I1", "I2", "svc1"), + newSpan("I3", "I1", "svc1"), + newSpan("I4", "I3", "svc2"), + newSpan("I5", "I4", "svc2"), + newSpan("I6", "I5", "svc3"), + newSpan("I7", "I6", "svc3"), + newSpan("I8", "I7", "svc4"), + newSpan("I9", "I8", "svc4"), + newSpan("I10", "I9", "svc5"), + newSpan("I11", "I10", "svc5") + ) + spanList.foreach(span => accumulator.process(span.getSpanId, span)) + + When("punctuate is called") + accumulator.getPunctuator(context).punctuate(System.currentTimeMillis()) + + Then("it should produce 10 SpanPair instances as expected") + verify(context) + And("the accumulator's collection should be empty") + accumulator.spanCount should be(0) + + 1 until 6 foreach (id => { + kvStore.get(s"svc$id") shouldBe null + }) + } + + it("should emit SpanPair instances for parent-child relation using ids with server spans") { + Given("an accumulator and initialized with a processor context") + val (context, kvStore, _, _) = mockContext(2) + val accumulator = createAccumulator(context) + + And("spans from 5 services") + val spanList = List( + newServerSpan("I1", "I2", "svc1"), + newServerSpan("I4", "I1", "svc2"), + newClientSpan("I5", "I4", "svc2"), + newServerSpan("I6", "I5", "svc3"), + newServerSpan("I8", "I6", "svc4"), + newClientSpan("I9", "I8", "svc4"), + newClientSpan("I10", "I9", "svc5"), + newServerSpan("I11", "I10", "svc6") + ) + spanList.foreach(span => accumulator.process(span.getSpanId, span)) + + When("punctuate is called") + accumulator.getPunctuator(context).punctuate(System.currentTimeMillis()) + + Then("it should produce 10 SpanPair instances as expected") + verify(context) + And("the accumulator's collection should be empty") + accumulator.spanCount should be(0) + 1 until 7 foreach { id => + kvStore.get(s"svc$id") shouldBe null + } + } + + it("should emit SpanPair instances for parent-child relation using ids with (I5, I4) and (I6, I5) in reverse order") { + Given("an accumulator and initialized with a processor context") + val (context, kvStore, _, _) = mockContext(4) + val accumulator = createAccumulator(context) + And("spans from 5 services") + val spanList = List( + newSpan("I1", "I2", "svc1"), + newSpan("I3", "I1", "svc1"), + newSpan("I4", "I3", "svc2"), + newSpan("I6", "I5", "svc3"), // child comes first + newSpan("I5", "I4", "svc2"), // then comes the parent + newSpan("I7", "I6", "svc3"), + newSpan("I8", "I7", "svc4"), + newSpan("I9", "I8", "svc4"), + newSpan("I10", "I9", "svc5"), + newSpan("I11", "I10", "svc5") + ) + spanList.foreach(span => accumulator.process(span.getSpanId, span)) + + When("punctuate is called") + accumulator.getPunctuator(context).punctuate(System.currentTimeMillis()) + + Then("it should produce 10 SpanPair instances as expected") + verify(context) + And("the accumulator's collection should be empty") + accumulator.spanCount should be(0) + 1 until 6 foreach { id => + kvStore.get(s"svc$id") shouldBe null + } + } + + it("should emit SpanPair instances for fork relation using ids for svc4 -> svc5 & svc4 -> svc6") { + Given("an accumulator and initialized with a processor context") + val (context, _, _, _) = mockContext(5) + val accumulator = createAccumulator(context) + And("spans from 6 services") + val spanList = List( + newSpan("I1", "I2", "svc1"), + newSpan("I3", "I1", "svc1"), + newSpan("I4", "I3", "svc2"), + newSpan("I6", "I5", "svc3"), + newSpan("I5", "I4", "svc2"), + newSpan("I7", "I6", "svc3"), + + newSpan("I8", "I7", "svc4"), + newSpan("I9", "I8", "svc4"), + newSpan("I10", "I8", "svc4"), + + //downstream of svc4 + newSpan("I11", "I9", "svc5"), + newSpan("I12", "I11", "svc5"), + + //downstream of svc4 + newSpan("I13", "I10", "svc6"), + newSpan("I14", "I13", "svc6") + ) + spanList.foreach(span => accumulator.process(span.getSpanId, span)) + + When("punctuate is called") + accumulator.getPunctuator(context).punctuate(System.currentTimeMillis()) + + Then("it should produce 10 SpanPair instances as expected") + verify(context) + And("the accumulator's collection should be empty") + accumulator.spanCount should be(0) + } + + it("should emit valid SpanPair instances for parent-child relation ignoring duplicate spans") { + Given("an accumulator and initialized with a processor context") + val (context, _, _, _) = mockContext(1) + val accumulator = createAccumulator(context) + And("spans from 5 services") + val spanList = List( + newSpan("I1", "I2", "svc1"), + newSpan("I1", "I2", "svc1"), //duplicate server span + newSpan("I3", "I1", "svc2"), + newSpan("I4", "I3", "svc2") + ) + spanList.foreach(span => accumulator.process(span.getSpanId, span)) + + When("punctuate is called") + accumulator.getPunctuator(context).punctuate(System.currentTimeMillis()) + + Then("it should produce 1 SpanPair instances as expected") + verify(context) + And("the accumulator's collection should be empty") + accumulator.spanCount should be(0) + } + } + + it("should emit valid SpanPair instances in mixed merge mode where we receive spans in Singular(sharable) and Dual(non-sharable) style") { + Given("an accumulator and initialized with a processor context") + val (context, kvStore, forwardedKeys, forwardedSpanPairs) = mockContext(3) + val accumulator = createAccumulator(context) + And("spans from 4 services") + val spanList = List( + // sharable client-server span + newClientSpan("I1", "I2", "svc1"), + newServerSpan("I1", "I2", "svc2"), + + // non-sharable client-server span + newClientSpan("I2", "I1", "svc2"), + newServerSpan("I3", "I2", "svc3"), + + // sharable client-server span + newClientSpan("I4", "I3", "svc3"), + newServerSpan("I4", "I3", "svc4") + ) + spanList.foreach(span => accumulator.process(span.getSpanId, span)) + + When("punctuate is called") + accumulator.getPunctuator(context).punctuate(System.currentTimeMillis()) + + Then("it should produce 3 SpanPair instances as expected") + verify(context) + And("the accumulator's collection should be empty") + accumulator.spanCount should be(0) + kvStore.get("svc1") shouldBe null + kvStore.get("svc2").useSharedSpan shouldBe true + kvStore.get("svc3") + kvStore.get("svc4").useSharedSpan shouldBe true + extractClientServerSvcNames(forwardedSpanPairs) should contain allOf("svc1->svc2", "svc2->svc3", "svc3->svc4") + forwardedKeys.toSet should contain allOf("I1", "I2", "I4") + } + + it("should respect the singular(sharable) span merge style once set even later if it receives dual(non-sharable) span mode") { + Given("an accumulator and initialized with a processor context") + val (context, kvStore, forwardedKeys, forwardedSpanPairs) = mockContext(3) + val accumulator = createAccumulator(context) + And("spans from 3 services") + val spanList = List( + // sharable client-server span + newClientSpan("I1", "I2", "svc1"), + newServerSpan("I1", "I2", "svc2"), + + // sharable client-server span + newClientSpan("I3", "I1", "svc2"), + newServerSpan("I3", "I1", "svc3"), + + // one non-sharable client-server span between svc1 and svc3 + // one sharable client-server span between svc2 and svc3 + newClientSpan("T1", "T2", "svc1"), + newServerSpan("T3", "T1", "svc3"), + newClientSpan("T3", "T1", "svc2") + ) + spanList.foreach(span => accumulator.process(span.getSpanId, span)) + + When("punctuate is called") + accumulator.getPunctuator(context).punctuate(System.currentTimeMillis()) + + Then("it should produce 3 SpanPair instances as expected") + verify(context) + And("the accumulator's collection should be empty") + accumulator.spanCount should be(0) + + kvStore.get("svc1") shouldBe null + kvStore.get("svc2").useSharedSpan shouldBe true + kvStore.get("svc3").useSharedSpan shouldBe true + extractClientServerSvcNames(forwardedSpanPairs) should contain allOf("svc1->svc2", "svc2->svc3") + forwardedKeys.toSet should contain allOf("I1", "I3", "T3") + } + + it("should auto-correct from dual to Singular merge style mode and never go back") { + Given("an accumulator and initialized with a processor context") + val (context, kvStore, forwardedKeys, forwardedSpanPairs) = mockContext(3) + val accumulator = createAccumulator(context) + And("spans from 5 services") + val spanList = List( + // non-sharable client-server span between svc1 and svc3 + newClientSpan("I1", "I2", "svc1"), + newServerSpan("I3", "I1", "svc3"), + + newServerSpan("I1", "I2", "svc2"), + newClientSpan("I3", "I1", "svc2"), + + newClientSpan("T1", "T2", "svc1"), + newServerSpan("T3", "T1", "svc3") + ) + spanList.foreach(span => accumulator.process(span.getSpanId, span)) + + When("punctuate is called") + accumulator.getPunctuator(context).punctuate(System.currentTimeMillis()) + + Then("it should produce 3 SpanPair instances as expected") + verify(context) + And("the accumulator's collection should be empty") + accumulator.spanCount should be(0) + kvStore.get("svc1") shouldBe null + kvStore.get("svc2").useSharedSpan shouldBe true + kvStore.get("svc3").useSharedSpan shouldBe true + extractClientServerSvcNames(forwardedSpanPairs) should contain allOf("svc1->svc2", "svc1->svc3", "svc2->svc3") + forwardedKeys.toSet should contain allOf("I1", "I3") + } + + it("should emit valid SpanPair instances for only singular(sharable) styled spans") { + Given("an accumulator and initialized with a processor context") + val (context, kvStore, _, _) = mockContext(3) + val accumulator = createAccumulator(context) + And("spans from 4 services") + val spanList = List( + newServerSpan("I1", "I2", "svc2"), + newServerSpan("I2", "I1", "svc3"), + newServerSpan("I3", "I2", "svc4"), + newClientSpan("I1", "I2", "svc1"), + newClientSpan("I2", "I1", "svc2"), + newClientSpan("I3", "I2", "svc3") + ) + spanList.foreach(span => accumulator.process(span.getSpanId, span)) + + When("punctuate is called") + accumulator.getPunctuator(context).punctuate(System.currentTimeMillis()) + + Then("it should produce 3 SpanPair instances as expected") + verify(context) + And("the accumulator's collection should be empty") + accumulator.spanCount should be(0) + kvStore.get("svc1") shouldBe null + 2 until 5 foreach { id => + kvStore.get(s"svc$id").useSharedSpan shouldBe true + } + } + + it("should apply eviction logic using the end time (start time + duration) of the incoming spans and not rely on their start time") { + Given("an accumulator and initialized with a processor context") + val (context, kvStore, forwardedKeys, forwardedSpanPairs) = mockContext(2, 2) + val accumulator = createAccumulator(context) + + And("spans from 2 services") + val currentTime = System.currentTimeMillis() + val oldStartTime = currentTime - TimeUnit.SECONDS.toMillis(10) + val longDurationSpans = List( + // sharable client-server span + newClientSpan("I1", "I2", "svc1", oldStartTime, TimeUnit.SECONDS.toMicros(9)), + newClientSpan("I3", "I4", "svc1", oldStartTime, TimeUnit.SECONDS.toMicros(5)), + newServerSpan("I3", "I4", "svc2", oldStartTime, TimeUnit.SECONDS.toMicros(5)) + ) + longDurationSpans.foreach(span => accumulator.process(span.getSpanId, span)) + + When("punctuate is called") + accumulator.getPunctuator(context).punctuate(currentTime) + + Then("it should produce 2 SpanPair instances as expected") + And("the accumulator's collection not be empty, it should hold span with spanId I1") + accumulator.spanCount should be(1) + Set("I1") should contain allElementsOf accumulator.internalSpanMap.keySet + + And("when finally, server span with spanId I1 is observed, it should process and forward") + List( + newServerSpan("I1", "I2", "svc2", oldStartTime, TimeUnit.SECONDS.toMicros(9)) + ).foreach(span => accumulator.process(span.getSpanId, span)) + + // once the stream time moves ahead, it should evict the older spans + accumulator.getPunctuator(context).punctuate(currentTime + TimeUnit.SECONDS.toMillis(5)) + accumulator.spanCount should be(0) + + verify(context) + + kvStore.get("svc1") shouldBe null + kvStore.get("svc2").useSharedSpan shouldBe true + extractClientServerSvcNames(forwardedSpanPairs) should contain allElementsOf Seq("svc1->svc2") + forwardedKeys.toSet should contain allElementsOf Seq("I1", "I3") + } + + describe("span accumulator supplier") { + it("should supply a valid accumulator") { + Given("a supplier instance") + val supplier = new SpanAccumulatorSupplier(storeName, 1000, new GraphEdgeTagCollector()) + When("an accumulator instance is request") + val producer = supplier.get() + Then("should yield a valid producer") + producer should not be null + } + } + + private def mockContext(expectedForwardCalls: Int, expectedCommits: Int = 1): (ProcessorContext, KeyValueStore[String, ServiceNodeMetadata], mutable.ListBuffer[String], mutable.ListBuffer[SpanPair]) = { + val context = mock[ProcessorContext] + val stateStore = Stores.inMemoryKeyValueStore(storeName).get() + + val forwardedKeys = mutable.ListBuffer[String]() + val forwardedSpanPairs = mutable.ListBuffer[SpanPair]() + + expecting { + context.schedule(anyLong(), isA(classOf[PunctuationType]), isA(classOf[Punctuator])) + .andReturn(mock[Cancellable]).once() + + if (expectedForwardCalls > 0) { + val captureForwardedKey = EasyMock.newCapture[String]() + val captureForwardedSpanPair = EasyMock.newCapture[SpanPair]() + context + .forward(EasyMock.capture(captureForwardedKey), EasyMock.capture(captureForwardedSpanPair)) + .andAnswer(() => { + val spanPair = captureForwardedSpanPair.getValue + if (spanPair.IsSharedSpan) { + stateStore.asInstanceOf[KeyValueStore[String, ServiceNodeMetadata]].put(spanPair.getServerSpan.serviceName, ServiceNodeMetadata(true)) + } + forwardedKeys += captureForwardedKey.getValue + forwardedSpanPairs += spanPair + }).times(expectedForwardCalls) + + context.commit().times(expectedCommits) + } + context.getStateStore(storeName).andReturn(stateStore) + } + replay(context) + (context, stateStore.asInstanceOf[KeyValueStore[String, ServiceNodeMetadata]], forwardedKeys, forwardedSpanPairs) + } + + private def createAccumulator(context: ProcessorContext): SpanAccumulator = { + val accumulator = new SpanAccumulator(storeName, DEFAULT_ACCUMULATE_INTERVAL_MILLIS.toInt, new GraphEdgeTagCollector()) + accumulator.init(context) + accumulator + } + + private def extractClientServerSvcNames(spanPairs: mutable.ListBuffer[SpanPair]): Set[String] = { + spanPairs.map(p => p.getClientSpan.serviceName + "->" + p.getServerSpan.serviceName).toSet + } +} diff --git a/service-graph/node-finder/src/test/scala/com/expedia/www/haystack/service/graph/node/finder/app/StreamsSpec.scala b/service-graph/node-finder/src/test/scala/com/expedia/www/haystack/service/graph/node/finder/app/StreamsSpec.scala new file mode 100644 index 000000000..aaea1dac5 --- /dev/null +++ b/service-graph/node-finder/src/test/scala/com/expedia/www/haystack/service/graph/node/finder/app/StreamsSpec.scala @@ -0,0 +1,79 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.service.graph.node.finder.app + +import com.expedia.www.haystack.TestSpec +import com.expedia.www.haystack.commons.entities.GraphEdge +import com.expedia.www.haystack.commons.kstreams.SpanTimestampExtractor +import com.expedia.www.haystack.commons.kstreams.serde.SpanDeserializer +import com.expedia.www.haystack.commons.kstreams.serde.metricdata.MetricDataSerializer +import com.expedia.www.haystack.service.graph.node.finder.app.metadata.{MetadataProducerSupplier, MetadataStoreUpdateProcessorSupplier} +import com.expedia.www.haystack.service.graph.node.finder.config.{KafkaConfiguration, NodeMetadataConfiguration} +import com.expedia.www.haystack.service.graph.node.finder.model.ServiceNodeMetadata +import org.apache.kafka.common.serialization._ +import org.apache.kafka.streams.state.{KeyValueStore, StoreBuilder} +import org.apache.kafka.streams.{StreamsConfig, Topology} +import org.easymock.EasyMock._ + +class StreamsSpec extends TestSpec { + describe("configuring a topology should") { + it("should add a source, three processors and two sinks with expected arguments") { + Given("a configuration object of type KafkaConfiguration") + val streamsConfig = mock[StreamsConfig] + val kafkaConfig = KafkaConfiguration(streamsConfig, + "metrics", "service-call", + "proto-spans", Topology.AutoOffsetReset.LATEST, + new SpanTimestampExtractor, 10000, 10000, NodeMetadataConfiguration(false, "mystore", 1, 1), List("tier")) + val streams = new Streams(kafkaConfig) + val topology = mock[Topology] + When("initialize is invoked with a topology") + expecting { + topology.addSource(isA(classOf[Topology.AutoOffsetReset]), anyString(), + isA(classOf[SpanTimestampExtractor]), isA(classOf[StringDeserializer]), + isA(classOf[SpanDeserializer]), anyString()).andReturn(topology).once() + topology.addProcessor(anyString(), isA(classOf[SpanAccumulatorSupplier]), + anyString()).andReturn(topology).once() + topology.addProcessor(anyString(), isA(classOf[GraphNodeProducerSupplier]), + anyString()).andReturn(topology).once() + topology.addProcessor(anyString(), isA(classOf[LatencyProducerSupplier]), + anyString()).andReturn(topology).once() + topology.addSink(anyString(), anyString(), isA(classOf[StringSerializer]), + isA(classOf[MetricDataSerializer]), anyString()).andReturn(topology).once() + topology.addSink(anyString(), anyString(), isA(classOf[Serializer[GraphEdge]]), + isA(classOf[Serializer[GraphEdge]]), anyString()).andReturn(topology).once() + + topology.addProcessor(anyString(), isA(classOf[MetadataProducerSupplier]), + anyString()).andReturn(topology).once() + topology.addSink(anyString(), anyString(), isA(classOf[Serializer[String]]), + isA(classOf[Serializer[ServiceNodeMetadata]]), anyString()).andReturn(topology).once() + + topology.addGlobalStore(isA(classOf[StoreBuilder[KeyValueStore[String, ServiceNodeMetadata]]]), + anyString(), + isA(classOf[Deserializer[String]]), + isA(classOf[Deserializer[ServiceNodeMetadata]]), + anyString(), + anyString(), + isA(classOf[MetadataStoreUpdateProcessorSupplier])).andReturn(topology).once() + } + replay(topology) + streams.initialize(topology) + Then("it is configured as expected") + verify(topology) + } + } +} diff --git a/service-graph/node-finder/src/test/scala/com/expedia/www/haystack/service/graph/node/finder/config/AppConfigurationSpec.scala b/service-graph/node-finder/src/test/scala/com/expedia/www/haystack/service/graph/node/finder/config/AppConfigurationSpec.scala new file mode 100644 index 000000000..664f6f76d --- /dev/null +++ b/service-graph/node-finder/src/test/scala/com/expedia/www/haystack/service/graph/node/finder/config/AppConfigurationSpec.scala @@ -0,0 +1,85 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.service.graph.node.finder.config + +import com.expedia.www.haystack.TestSpec +import com.expedia.www.haystack.commons.kstreams.SpanTimestampExtractor +import com.typesafe.config.ConfigException + +class AppConfigurationSpec extends TestSpec { + describe("loading application configuration") { + it("should fail creating KafkaConfiguration if no application id is specified") { + Given("a test configuration file") + val file = "test/test_no_app_id.conf" + When("Application configuration is loaded") + Then("it should throw an exception") + intercept[IllegalArgumentException] { + new AppConfiguration(file).kafkaConfig + } + } + it("should fail creating KafkaConfiguration if no bootstrap is specified") { + Given("a test configuration file") + val file = "test/test_no_bootstrap.conf" + When("Application configuration is loaded") + Then("it should throw an exception") + intercept[IllegalArgumentException] { + new AppConfiguration(file).kafkaConfig + } + } + it("should fail creating KafkaConfiguration if no metrics topic is specified") { + Given("a test configuration file") + val file = "test/test_no_metrics_topic.conf" + When("Application configuration is loaded") + Then("it should throw an exception") + intercept[ConfigException] { + new AppConfiguration(file).kafkaConfig + } + } + it("should fail creating KafkaConfiguration if no consumer is specified") { + Given("a test configuration file") + val file = "test/test_no_consumer.conf" + When("Application configuration is loaded") + Then("it should throw an exception") + intercept[ConfigException] { + new AppConfiguration(file).kafkaConfig + } + } + it("should fail creating KafkaConfiguration if no producer is specified") { + Given("a test configuration file") + val file = "test/test_no_producer.conf" + When("Application configuration is loaded") + Then("it should throw an exception") + intercept[ConfigException] { + new AppConfiguration(file).kafkaConfig + } + } + it("should create KafkaConfiguration as specified") { + Given("a test configuration file") + val file = "test/test.conf" + When("Application configuration is loaded and KafkaConfiguration is obtained") + val config = new AppConfiguration(file).kafkaConfig + Then("it should load as expected") + config.streamsConfig.defaultTimestampExtractor() shouldBe a [SpanTimestampExtractor] + config.serviceCallTopic should be ("graph-nodes") + config.accumulatorInterval should be (60000) + config.metadataConfig.topic should be ("haystack-node-finder-metadata") + config.metadataConfig.partitionCount should be (6) + config.metadataConfig.replicationFactor should be (2) + } + } +} diff --git a/service-graph/node-finder/src/test/scala/com/expedia/www/haystack/service/graph/node/finder/model/SpanPairSpec.scala b/service-graph/node-finder/src/test/scala/com/expedia/www/haystack/service/graph/node/finder/model/SpanPairSpec.scala new file mode 100644 index 000000000..9d67ac108 --- /dev/null +++ b/service-graph/node-finder/src/test/scala/com/expedia/www/haystack/service/graph/node/finder/model/SpanPairSpec.scala @@ -0,0 +1,95 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.service.graph.node.finder.model + +import java.util.UUID + +import com.expedia.metrics.{MetricDefinition, TagCollection} +import com.expedia.www.haystack.TestSpec +import com.expedia.www.haystack.commons.entities._ +import com.expedia.www.haystack.service.graph.node.finder.utils.SpanType + +import scala.collection.JavaConverters._ + +class SpanPairSpec extends TestSpec { + describe("a complete span") { + it("should return a valid graphEdge for non-open tracing compliant spans") { + Given("a complete spanlite") + val spanId = UUID.randomUUID().toString + val parentSpanId = UUID.randomUUID().toString + val clientTime = System.currentTimeMillis() + + val clientSpan = newLightSpan(spanId, parentSpanId, "foo-service", "bar", clientTime, 1000, SpanType.CLIENT) + val serverSpan = newLightSpan(spanId, parentSpanId, "baz-service", "bar", SpanType.SERVER) + val spanPair = SpanPairBuilder.createSpanPair(clientSpan, serverSpan) + + When("get graphEdge is called") + val graphEdge = spanPair.getGraphEdge + + Then("it should return a valid graphEdge") + spanPair.isComplete should be(true) + graphEdge.get should be(GraphEdge(GraphVertex("foo-service"), GraphVertex("baz-service"), "bar", clientTime)) + } + + it("should return a valid graphEdge for open tracing compliant spans") { + Given("a complete spanlite") + val spanId = UUID.randomUUID().toString + val clientTime = System.currentTimeMillis() + + val clientSpan = newLightSpan(spanId, UUID.randomUUID().toString, "foo-service", "bar", clientTime, 1000, SpanType.OTHER) + val serverSpan = newLightSpan(UUID.randomUUID().toString, spanId, "baz-service", "bar", SpanType.OTHER) + val spanPair = SpanPairBuilder.createSpanPair(clientSpan, serverSpan) + + When("get graphEdge is called") + val graphEdge = spanPair.getGraphEdge + + Then("it should return a valid graphEdge") + spanPair.isComplete should be(true) + graphEdge.get should be(GraphEdge(GraphVertex("foo-service"), GraphVertex("baz-service"), "bar", clientTime)) + } + it("should return valid metricPoints") { + Given("a complete spanlite") + val clientSend = System.currentTimeMillis() + val serverReceive = clientSend + 500 + val spanId = UUID.randomUUID().toString + val clientSpan = newLightSpan(spanId, UUID.randomUUID().toString, "baz-service", "bar", + serverReceive, 500, SpanType.SERVER, Map()) + val serverSpan = newLightSpan(spanId, UUID.randomUUID().toString, "foo-service", "bar", + clientSend, 1500, SpanType.CLIENT, Map()) + val spanPair = SpanPairBuilder.createSpanPair(clientSpan, serverSpan) + + When("get Latency is called") + val metricPoint = spanPair.getLatency.get + + Then("it should return a valid latency pairs") + val tags = new TagCollection(Map( + TagKeys.SERVICE_NAME_KEY -> "foo-service", + TagKeys.OPERATION_NAME_KEY -> "bar", + MetricDefinition.UNIT -> "ms", + MetricDefinition.MTYPE -> "gauge" + ).asJava) + + spanPair.isComplete should be(true) + metricPoint.getMetricDefinition.getKey should be ("latency") + metricPoint.getValue should be (1) + metricPoint.getTimestamp should be (clientSend / 1000) + metricPoint.getMetricDefinition.getTags should equal (tags) + } + } + +} diff --git a/service-graph/node-finder/src/test/scala/com/expedia/www/haystack/service/graph/node/finder/utils/SpanUtilsSpec.scala b/service-graph/node-finder/src/test/scala/com/expedia/www/haystack/service/graph/node/finder/utils/SpanUtilsSpec.scala new file mode 100644 index 000000000..949e00878 --- /dev/null +++ b/service-graph/node-finder/src/test/scala/com/expedia/www/haystack/service/graph/node/finder/utils/SpanUtilsSpec.scala @@ -0,0 +1,120 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.service.graph.node.finder.utils + +import com.expedia.open.tracing.Tag +import com.expedia.www.haystack.TestSpec + +class SpanUtilsSpec extends TestSpec { + describe("discovering a span type") { + it("should return CLIENT when both 'cr' and 'cs' is present") { + Given("a span with 'cr' and 'cs' event logs") + val (span, _) = newSpan("foo-service", "bar", 6000, client = true, server = false) + When("getSpanType is called") + val spanType = SpanUtils.getSpanType(span) + Then("it is marked as CLIENT") + spanType should be (SpanType.CLIENT) + } + + it("should return CLIENT when more when 'cr', 'cs' and 'sr' is present but span.kind is set correctly") { + Given("a span with 'cr','cs', 'sr' and 'ss' event logs") + val (span, _) = newSpan("foo-service", "bar", 6000, client = true, server = true) + When("getSpanType is called") + val spanType = SpanUtils.getSpanType(span) + Then("it is marked as OTHER") + spanType should be (SpanType.CLIENT) + } + + it("should return SERVER when just 'sr' and 'ss' are present") { + Given("a span with 'sr' and 'ss' event logs") + val (span, _) = newSpan("foo-service", "bar", 6000, client = false, server = true) + When("getSpanType is called") + val spanType = SpanUtils.getSpanType(span) + Then("it is marked as SERVER") + spanType should be (SpanType.SERVER) + } + + it("should return client when 'cr', 'cs', 'sr' and 'ss' are present but span.kind tag is present") { + Given("a span with no 'cr', cs', 'sr' and 'ss' event logs") + var (span, _) = newSpan("foo-service", "bar", 6000, client = false, server = false) + span = span.toBuilder.addTags(Tag.newBuilder().setKey("span.kind").setVStr("client")).build() + When("getSpanType is called") + val spanType = SpanUtils.getSpanType(span) + Then("it is marked as CLIENT") + spanType should be (SpanType.CLIENT) + } + + it("should return server when 'cr', 'cs', 'sr' and 'ss' are present but span.kind tag is present") { + Given("a span with no 'cr', 'cs', 'sr' and 'ss' event logs") + var (span, _) = newSpan("foo-service", "bar", 6000, client = false, server = false) + span = span.toBuilder.addTags(Tag.newBuilder().setKey("span.kind").setVStr("server")).build() + When("getSpanType is called") + val spanType = SpanUtils.getSpanType(span) + Then("it is marked as SERVER") + spanType should be (SpanType.SERVER) + } + + it("should return server when 'sr' and 'ss' are present and span.kind tag is also present") { + Given("a span with 'sr' and 'ss' event logs and span.kind tag as 'server'") + var (span, _) = newSpan("foo-service", "bar", 6000, client = false, server = true) + span = span.toBuilder.addTags(Tag.newBuilder().setKey("span.kind").setVStr("server")).build() + When("getSpanType is called") + val spanType = SpanUtils.getSpanType(span) + Then("it is marked as SERVER") + spanType should be (SpanType.SERVER) + } + + it("should return client when 'cr' and 'cs' are present and span.kind tag is also present") { + Given("a span with 'cr' and 'cs' event logs and span.kind tag as 'client'") + var (span, _) = newSpan("foo-service", "bar", 6000, client = true, server = false) + span = span.toBuilder.addTags(Tag.newBuilder().setKey("span.kind").setVStr("client")).build() + When("getSpanType is called") + val spanType = SpanUtils.getSpanType(span) + Then("it is marked as CLIENT") + spanType should be (SpanType.CLIENT) + } + } + + describe("finding an event time") { + it("should return None with the spanType is OTHER") { + Given("a span with no event logs") + val (span, _) = newSpan("foo-service", "bar", 6000, client = false, server = false) + When("getEventTime is called") + val eventTime = SpanUtils.getEventTimestamp(span, SpanUtils.SERVER_SEND_EVENT) + Then("it is marked as OTHER") + eventTime should be (None) + } + it("should return None with the spanType is SERVER and we look for CLIENT_SEND") { + Given("a span with no event logs") + val (span, _) = newSpan("foo-service", "bar", 6000, client = false, server = true) + When("getEventTime is called") + val eventTime = SpanUtils.getEventTimestamp(span, SpanUtils.CLIENT_SEND_EVENT) + Then("it is marked as OTHER") + eventTime should be (None) + } + it("should return timeStamp with the spanType is SERVER and we look for SERVER_SEND") { + Given("a span with no event logs") + val (span, _) = newSpan("foo-service", "bar", 6000, client = false, server = true) + When("getEventTime is called") + val eventTime = SpanUtils.getEventTimestamp(span, SpanUtils.SERVER_SEND_EVENT) + Then("it is marked as OTHER") + (eventTime.get > 0) should be (true) + } + } + +} diff --git a/service-graph/pom.xml b/service-graph/pom.xml new file mode 100644 index 000000000..66ef8c6a7 --- /dev/null +++ b/service-graph/pom.xml @@ -0,0 +1,538 @@ + + + + 4.0.0 + com.expedia.www + haystack-service-graph + 1.0.15-SNAPSHOT + pom + + + node-finder + graph-builder + snapshot-store + snapshotter + + + + scm:git:git://github.com/ExpediaDotCom/haystack-service-graph.git + scm:git:ssh://github.com/ExpediaDotCom/haystack-service-graph.git + http://github.com/ExpediaDotCom/haystack-service-graph + + + ${project.groupId}:${project.artifactId} + Code to generate the service graph and network latency using spans from the proto span stream + https://github.com/ExpediaDotCom/haystack-service-graph/tree/master + + + + + Apache License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + + haystack + Haystack Team + haystack@expedia.com + https://github.com/ExpediaDotCom/haystack + + + + + 4.2.5 + 1.11.447 + 3.4 + 1.7.1 + 0.5.0 + 1.0.61 + 0.1.12 + ${version} + 4.5.3 + 9.4.19.v20190610 + 3.5.3 + 3.6.0 + 1.2.3 + 1.6 + 3.2.1 + 3.0.1 + 1.9.5 + 0.8.13 + 4.1.45.Final + 1.6.8 + 1.6.0 + 1.8 + 3.4.0 + ${scala.major.version}.${scala.minor.version}.${scala.tiny.version} + ${scala.major.version}.${scala.minor.version} + 2 + 12 + 5 + 3.0.3 + 2.4.1 + 1.3.0 + 1.7.25 + 6.8 + 1.3.1 + + true + + + + + + + com.google.protobuf + protobuf-java + ${protobuf.version} + + + io.grpc + grpc-protobuf + ${grpc.version} + + + io.grpc + grpc-stub + ${grpc.version} + + + io.grpc + grpc-netty + ${grpc.version} + + + io.netty + netty-handler + ${netty.handler.version} + + + + + org.scala-lang + scala-library + ${scala-library.version} + + + org.scala-lang + scala-reflect + ${scala-library.version} + + + + + com.typesafe + config + ${typesafe-config.version} + + + + + ch.qos.logback + logback-classic + ${logback.version} + + + ch.qos.logback + logback-core + ${logback.version} + + + org.slf4j + slf4j-api + ${slf4j-api.version} + + + + com.amazonaws + aws-java-sdk-s3 + ${aws-java-sdk.version} + + + + org.json4s + json4s-jackson_${scala.major.minor.version} + ${json4s.version} + + + + org.json4s + json4s-ext_${scala.major.minor.version} + ${json4s.version} + + + + com.nrinaudo + kantan.csv_${scala.major.minor.version} + ${kantan_csv.version} + + + + com.expedia.www + haystack-logback-metrics-appender + ${haystack.logback.metrics.appender.version} + + + + org.apache.commons + commons-lang3 + ${commons-lang.version} + + + + org.apache.httpcomponents + httpclient + ${httpclient.version} + + + + + com.expedia.www + haystack-commons + ${haystack-commons.version} + + + com.expedia.www + haystack-service-graph-snapshot-store + ${haystack-service-graph-snapshot-store.version} + + + + + org.msgpack + msgpack-core + ${msgpack.version} + + + + + org.eclipse.jetty + jetty-server + ${jetty.version} + + + org.eclipse.jetty + jetty-servlet + ${jetty.version} + + + org.apache.httpcomponents + fluent-hc + ${apache.httpcomponents.version} + + + + org.mockito + mockito-all + ${mockito.version} + test + + + + org.scalaj + scalaj-http_${scala.major.minor.version} + ${scalaj-http.version} + + + + + + + + com.expedia.www + haystack-commons + + + + org.json4s + json4s-jackson_${scala.major.minor.version} + + + + com.typesafe + config + + + + com.google.protobuf + protobuf-java + + + + io.grpc + grpc-protobuf + + + + io.grpc + grpc-stub + + + + org.scala-lang + scala-library + + + + org.scala-lang + scala-reflect + + + + ch.qos.logback + logback-classic + + + + ch.qos.logback + logback-core + + + + org.slf4j + slf4j-api + + + + + org.scalatest + scalatest_${scala.major.minor.version} + ${scalatest.version} + test + + + org.pegdown + pegdown + ${pegdown.version} + test + + + junit + junit + 4.12 + test + + + org.easymock + easymock + 3.4 + test + + + org.apache.kafka + kafka-streams + 1.1.0 + provided + + + com.codahale.metrics + metrics-core + 3.0.2 + + + + + + ${basedir}/src/main/scala + + + ${basedir}/src/main/resources + true + + + + + + org.scalatest + scalatest-maven-plugin + 1.0 + + + test + + test + + + + + + + org.scoverage + scoverage-maven-plugin + ${scoverage.plugin.version} + + + 80 + true + true + ${scala-library.version} + true + + + + + org.apache.maven.plugins + maven-shade-plugin + 1.6 + + + + org.scalastyle + scalastyle-maven-plugin + 0.8.0 + + true + false + ${basedir}/../checkstyles/scalastyle_config.xml + ${basedir}/src/main/scala + ${basedir}/src/test/scala + ${project.build.directory}/scalastyle-output.xml + UTF-8 + + + + compile-scalastyle + + check + + compile + + + + + net.alchim31.maven + scala-maven-plugin + 3.2.1 + + + scala-compile-first + process-resources + + add-source + compile + + + + scala-test-compile + process-test-resources + + testCompile + + + + attach-javadocs + + doc-jar + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.6.1 + + ${project.jdk.version} + ${project.jdk.version} + + + + org.apache.maven.plugins + maven-source-plugin + ${maven-source-plugin.version} + + + attach-sources + + jar-no-fork + + + + + + org.apache.maven.plugins + maven-gpg-plugin + ${maven-gpg-plugin.version} + + + org.sonatype.plugins + nexus-staging-maven-plugin + ${nexus-staging-maven-plugin.version} + + + + + + org.apache.maven.plugins + maven-source-plugin + + + attach-sources + + jar-no-fork + + + + + + org.apache.maven.plugins + maven-gpg-plugin + + ${skipGpg} + + + + sign-artifacts + verify + + sign + + + + + + org.sonatype.plugins + nexus-staging-maven-plugin + true + + ossrh + https://oss.sonatype.org/ + true + + + + + + + + ossrh + https://oss.sonatype.org/content/repositories/snapshots + + + ossrh + http://oss.sonatype.org/service/local/staging/deploy/maven2/ + + + diff --git a/service-graph/snapshot-store/README.md b/service-graph/snapshot-store/README.md new file mode 100644 index 000000000..c8429565b --- /dev/null +++ b/service-graph/snapshot-store/README.md @@ -0,0 +1,25 @@ +#Haystack : snapshot-store + +The "snapshot" feature of the service graph starts with a call (made at regular intervals) to the service-graph service, +asking for a current copy of the service graph; the service graph that it receives is then persisted. Currently two +types of persistent storage are supported: +1. The local file system, for development work and small installations +2. Amazon [AWS S3](https://aws.amazon.com/s3/) + +The persistent copies can then be queried to observe the service graph at a point of time in the past. + +This snapshot-store package contains code to manage both types of durable locations. + +The persistent copies of the service-graph will be purged after a suitable amount of time. Since S3 has an +[object expiration feature](https://aws.amazon.com/blogs/aws/amazon-s3-object-expiration/), there is no need +for snapshot-store code to purge data from S3. The code for the local file system does purge expired data. + +Snapshots are stored in two CSV files: one for edges and one for nodes. These files can be consumed by +[Spark DataFrames](https://spark.apache.org/docs/2.3.0/sql-programming-guide.html#datasets-and-dataframes) +and were chosen as the storage format to facilitate using Spark or similar tools to analyze historical service graphs. + +## Building + +``` +mvn clean package +``` \ No newline at end of file diff --git a/service-graph/snapshot-store/pom.xml b/service-graph/snapshot-store/pom.xml new file mode 100644 index 000000000..9eff98831 --- /dev/null +++ b/service-graph/snapshot-store/pom.xml @@ -0,0 +1,104 @@ + + + + 4.0.0 + + + haystack-service-graph + com.expedia.www + 1.0.15-SNAPSHOT + + + haystack-service-graph-snapshot-store + + + + + Apache License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + + com.expedia.www + haystack-logback-metrics-appender + + + com.amazonaws + aws-java-sdk-s3 + + + org.mockito + mockito-all + + + com.nrinaudo + kantan.csv_${scala.major.minor.version} + + + + + + + org.scalatest + scalatest-maven-plugin + + + test + + test + + + org.expedia.www.haystack.commons.scalatest.IntegrationSuite + + + + + + + org.scoverage + scoverage-maven-plugin + + true + 100 + + + + + net.alchim31.maven + scala-maven-plugin + + + + org.scalastyle + scalastyle-maven-plugin + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + diff --git a/service-graph/snapshot-store/src/main/scala/com/expedia/www/haystack/service/graph/snapshot/store/Constants.scala b/service-graph/snapshot-store/src/main/scala/com/expedia/www/haystack/service/graph/snapshot/store/Constants.scala new file mode 100644 index 000000000..5583a6572 --- /dev/null +++ b/service-graph/snapshot-store/src/main/scala/com/expedia/www/haystack/service/graph/snapshot/store/Constants.scala @@ -0,0 +1,46 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.service.graph.snapshot.store + +object Constants { + private val DotJson = ".json" + + val DotCsv = ".csv" + val SlashNodes = "/nodes" + val SlashEdges = "/edges" + val _Nodes = "_nodes" + val _Edges = "_edges" + val SourceKey: String = "source" + val EdgesKey: String = "edges" + val DestinationKey: String = "destination" + val StatsKey: String = "stats" + val TagsKey: String = "tags" + val CountKey: String = "count" + val LastSeenKey: String = "lastSeen" + val ErrorCountKey: String = "errorCount" + val EffectiveFromKey: String = "effectiveFrom" + val EffectiveToKey: String = "effectiveTo" + val IdKey: String = "id" + val NameKey: String = "name" + val InfrastructureProviderKey: String = "X-HAYSTACK-INFRASTRUCTURE-PROVIDER" + val TierKey: String = "tier" + val ServiceGraph: String = "serviceGraph" + val JsonFileNameWithExtension: String = ServiceGraph + DotJson + val NodesCsvFileNameWithExtension: String = ServiceGraph + _Nodes + DotCsv + val EdgesCsvFileNameWithExtension: String = ServiceGraph + _Edges + DotCsv +} diff --git a/service-graph/snapshot-store/src/main/scala/com/expedia/www/haystack/service/graph/snapshot/store/DataFramesIntoJsonTransformer.scala b/service-graph/snapshot-store/src/main/scala/com/expedia/www/haystack/service/graph/snapshot/store/DataFramesIntoJsonTransformer.scala new file mode 100644 index 000000000..95a4f4919 --- /dev/null +++ b/service-graph/snapshot-store/src/main/scala/com/expedia/www/haystack/service/graph/snapshot/store/DataFramesIntoJsonTransformer.scala @@ -0,0 +1,81 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.service.graph.snapshot.store + +import com.expedia.www.haystack.service.graph.snapshot.store.Constants.EdgesKey +import com.expedia.www.haystack.service.graph.snapshot.store.DataFramesIntoJsonTransformer.{AddToMapError, WriteError} +import kantan.csv._ +import kantan.csv.ops._ +import org.slf4j.{Logger, LoggerFactory} + +import scala.collection.mutable + +object DataFramesIntoJsonTransformer { + val WriteError: String = "Problem reading JSON in write()" + val AddToMapError: String = "Problem reading JSON in addToMap()" +} + +class DataFramesIntoJsonTransformer(logger: Logger) { + def this() { + this(LoggerFactory.getLogger("com.expedia.www.haystack.service.graph.snapshot.store.DataFramesIntoJsonTransformer")) + } + + private def addToMap(map: mutable.Map[Long, Node], + either: Either[ReadError, NodeWithId]): Unit = { + either match { + case Left(readError) => logger.error(AddToMapError, readError) + case Right(nodeWithId) => map.put(nodeWithId.id, NodeWithId.nodeMapper(nodeWithId)) + } + } + + private var prependComma = false + + private def write(stringBuilder: StringBuilder, + nodeIdVsNode: mutable.Map[Long, Node], + either: Either[ReadError, EdgeWithIds]): Unit = { + either match { + case Left(readError) => logger.error(WriteError, readError) + case Right(edgeWithId) => + val edge = Edge.mapper(nodeIdVsNode, edgeWithId) + stringBuilder.append(edge.toJson(prependComma)) + prependComma = true + } + } + + private implicit val nodeDecoder: RowDecoder[NodeWithId] = + RowDecoder.decoder(0, 1, 2, 3)(NodeWithId.apply) + + private implicit val edgeDecoder: RowDecoder[EdgeWithIds] = + RowDecoder.decoder(0, 1, 2, 3, 4, 5, 6, 7)(EdgeWithIds.apply) + + def parseDataFrames(nodesRawData: String, + edgesRawData: String): String = { + val nodeIdVsNode = saveNodesToMap(nodesRawData) + val stringBuilder = new StringBuilder + stringBuilder.append("{\n \"").append(EdgesKey).append("\": [\n") + edgesRawData.asCsvReader[EdgeWithIds](rfc.withHeader).foreach(write(stringBuilder, nodeIdVsNode, _)) + stringBuilder.append("\n ]\n}\n") + stringBuilder.toString() + } + + private def saveNodesToMap(nodesRawData: String): mutable.Map[Long, Node] = { + val nodeIdVsNode = mutable.Map[Long, Node]() + nodesRawData.asCsvReader[NodeWithId](rfc.withHeader).foreach(addToMap(nodeIdVsNode, _)) + nodeIdVsNode + } +} diff --git a/service-graph/snapshot-store/src/main/scala/com/expedia/www/haystack/service/graph/snapshot/store/Edge.scala b/service-graph/snapshot-store/src/main/scala/com/expedia/www/haystack/service/graph/snapshot/store/Edge.scala new file mode 100644 index 000000000..f49ad8f44 --- /dev/null +++ b/service-graph/snapshot-store/src/main/scala/com/expedia/www/haystack/service/graph/snapshot/store/Edge.scala @@ -0,0 +1,96 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.service.graph.snapshot.store + +import Constants._ + +import scala.collection.mutable + +case class Edge(source: Node, + destination: Node, + statsCount: Long, + statsLastSeen: Long, + statsErrorCount: Long, + effectiveFrom: Long, + effectiveTo: Long) { + private val Newline = "\n" + private val CommaNewline = "," + Newline + private val QuoteColonSpace = "\": " + + def toJson(prependComma: Boolean): String = { + val stringBuilder = new StringBuilder + if(prependComma) { + stringBuilder.append(CommaNewline) + } + stringBuilder.append(" {") + appendNode(stringBuilder, source, SourceKey) + appendNode(stringBuilder, destination, DestinationKey) + appendStats(stringBuilder) + stringBuilder.append(" \"").append(EffectiveFromKey).append(QuoteColonSpace).append(effectiveFrom).append(CommaNewline) + stringBuilder.append(" \"").append(EffectiveToKey).append(QuoteColonSpace).append(effectiveTo).append(Newline) + stringBuilder.append(" }") + stringBuilder.toString() + } + + private def appendStats(stringBuilder: StringBuilder) = { + stringBuilder.append("\n \"").append(StatsKey).append("\": {\n") + stringBuilder.append(" \"").append(CountKey).append(QuoteColonSpace).append(statsCount).append(CommaNewline) + stringBuilder.append(" \"").append(LastSeenKey).append(QuoteColonSpace).append(statsLastSeen).append(CommaNewline) + stringBuilder.append(" \"").append(ErrorCountKey).append(QuoteColonSpace).append(statsErrorCount).append(Newline) + stringBuilder.append(" },\n") + } + + private def appendNode(stringBuilder: StringBuilder, node: Node, key: String) = { + stringBuilder.append("\n \"").append(key).append("\": {\n") + stringBuilder.append(" \"").append(NameKey).append("\": \"").append(node.name).append("\"") + stringBuilder.append(CommaNewline).append(" \"").append(TagsKey).append("\": {") + if (areAnyTagsDefined) { + stringBuilder.append(Newline) + if (node.tier.isDefined) { + stringBuilder.append(" \"").append(TierKey).append("\": \"").append(node.tier.get).append("\"") + if (node.infrastructureProvider.isDefined) { + stringBuilder.append(CommaNewline) + } + } + if (node.infrastructureProvider.isDefined) { + stringBuilder.append(" \"").append(InfrastructureProviderKey).append("\": \"") + .append(node.infrastructureProvider.get).append("\"") + } + stringBuilder.append("\n ") + } + stringBuilder.append("}\n") + stringBuilder.append(" },") + + def areAnyTagsDefined = { + node.infrastructureProvider.isDefined || node.tier.isDefined + } + } +} + +object Edge { + def mapper(nodeIdVsNode: mutable.Map[Long, Node], + edgeWithIds: EdgeWithIds): Edge = { + Edge(nodeIdVsNode(edgeWithIds.sourceId), + nodeIdVsNode(edgeWithIds.destinationId), + edgeWithIds.statsCount, + edgeWithIds.statsLastSeen, + edgeWithIds.statsErrorCount, + edgeWithIds.effectiveFrom, + edgeWithIds.effectiveTo) + } +} diff --git a/service-graph/snapshot-store/src/main/scala/com/expedia/www/haystack/service/graph/snapshot/store/EdgeWithIds.scala b/service-graph/snapshot-store/src/main/scala/com/expedia/www/haystack/service/graph/snapshot/store/EdgeWithIds.scala new file mode 100644 index 000000000..fa2c87251 --- /dev/null +++ b/service-graph/snapshot-store/src/main/scala/com/expedia/www/haystack/service/graph/snapshot/store/EdgeWithIds.scala @@ -0,0 +1,27 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.service.graph.snapshot.store + +case class EdgeWithIds(id: Long, + sourceId: Long, + destinationId: Long, + statsCount: Long, + statsLastSeen: Long, + statsErrorCount: Long, + effectiveFrom: Long, + effectiveTo: Long) \ No newline at end of file diff --git a/service-graph/snapshot-store/src/main/scala/com/expedia/www/haystack/service/graph/snapshot/store/FileSnapshotStore.scala b/service-graph/snapshot-store/src/main/scala/com/expedia/www/haystack/service/graph/snapshot/store/FileSnapshotStore.scala new file mode 100644 index 000000000..562a3fd87 --- /dev/null +++ b/service-graph/snapshot-store/src/main/scala/com/expedia/www/haystack/service/graph/snapshot/store/FileSnapshotStore.scala @@ -0,0 +1,116 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.service.graph.snapshot.store + +import java.nio.charset.StandardCharsets +import java.nio.file.{Files, Path, Paths} +import java.time.Instant +import java.util.{Comparator, Optional} + +class FileSnapshotStore(val directoryName: String) extends SnapshotStore { + private val directory = Paths.get(directoryName) + + def this() = { + this("/") + } + + /** + * Returns a FileSnapshotStore using the directory name specified + * + * @param constructorArguments constructorArguments[0] must specify the directory to which snapshots will be stored + * @return the concrete FileSnapshotStore to use + */ + override def build(constructorArguments: Array[String]): SnapshotStore = { + new FileSnapshotStore(constructorArguments(0)) + } + + /** + * Writes a string to the persistent store + * + * @param instant date/time of the write, used to create the name, which will later be used in read() and purge() + * @param content String to write + * @return a tuple of the paths of the two CSV files (one for nodes, one for edges, in that order) to which the nodes + * and edges were written; @see java.nio.file.Path + */ + override def write(instant: Instant, + content: String): (Path, Path) = { + if (!Files.exists(directory)) { + Files.createDirectories(directory) + } + val nodesAndEdges = transformJsonToNodesAndEdges(content) + val nodePath = write(instant, Constants._Nodes, nodesAndEdges.nodes) + val edgesPath = write(instant, Constants._Edges, nodesAndEdges.edges) + (nodePath, edgesPath) + } + + private def write(instant: Instant, suffix: String, content: String) = { + val path = directory.resolve(createIso8601FileName(instant) + suffix) + Files.write(path, content.getBytes(StandardCharsets.UTF_8)) + } + + private val pathNameComparator: Comparator[Path] = (o1: Path, o2: Path) => o1.toString.compareTo(o2.toString) + /** + * Reads content from the persistent store + * + * @param instant date/time of the read + * @return the content, transformed to JSON, of the youngest _nodes and _edges files whose ISO-8601-based name is + * earlier or equal to instant + */ + override def read(instant: Instant): Option[String] = { + var optionString: Option[String] = None + val fileNameForInstant = createIso8601FileName(instant) + val fileToUse: Optional[Path] = Files + .walk(directory, 1) + .filter(_.toFile.getName.endsWith(Constants._Nodes)) + .filter(_.toFile.getName.substring(0, fileNameForInstant.length) <= fileNameForInstant) + .max(pathNameComparator) + if (fileToUse.isPresent) { + val nodesRawData = Files.readAllLines(fileToUse.get).toArray.mkString("\n") + val edgesPath = Paths.get(fileToUse.get().toAbsolutePath.toString.replace(Constants._Nodes, Constants._Edges)) + val edgesRawData = Files.readAllLines(edgesPath).toArray.mkString("\n") + optionString = Some(transformNodesAndEdgesToJson(nodesRawData, edgesRawData)) + } + optionString + } + + /** + * Purges items from the persistent store + * + * @param instant date/time of items to be purged; items whose ISO-8601-based name is earlier than or equal to + * instant will be purged + * @return the number of items purged + */ + override def purge(instant: Instant): Integer = { + val fileNameForInstant = createIso8601FileName(instant) + val pathsToPurge: Array[AnyRef] = Files + .walk(directory, 1) + .filter(isNodesOrEdgesFile(_)) + .filter(_.toFile.getName.substring(0, fileNameForInstant.length) <= fileNameForInstant) + .toArray + for (anyRef <- pathsToPurge) { + Files.delete(anyRef.asInstanceOf[Path]) + } + pathsToPurge.length + } + + private def isNodesOrEdgesFile(path: Path): Boolean = { + val name = path.toFile.getName + name.endsWith(Constants._Nodes) || name.endsWith(Constants._Edges) + } + +} diff --git a/service-graph/snapshot-store/src/main/scala/com/expedia/www/haystack/service/graph/snapshot/store/JsonIntoDataFramesTransformer.scala b/service-graph/snapshot-store/src/main/scala/com/expedia/www/haystack/service/graph/snapshot/store/JsonIntoDataFramesTransformer.scala new file mode 100644 index 000000000..d1d2fe85f --- /dev/null +++ b/service-graph/snapshot-store/src/main/scala/com/expedia/www/haystack/service/graph/snapshot/store/JsonIntoDataFramesTransformer.scala @@ -0,0 +1,143 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.service.graph.snapshot.store + +import com.expedia.www.haystack.service.graph.snapshot.store.Constants._ +import com.expedia.www.haystack.service.graph.snapshot.store.JsonIntoDataFramesTransformer._ +import org.json4s._ +import org.json4s.jackson.JsonMethods._ + +import scala.collection.mutable +import scala.util.Try + +object JsonIntoDataFramesTransformer { + private val SourceId = SourceKey + IdKey.capitalize + private val DestinationId = DestinationKey + IdKey.capitalize + private val StatsCount = StatsKey + CountKey.capitalize + private val StatsLastSeen = StatsKey + LastSeenKey.capitalize + private val StatsErrorCount = StatsKey + ErrorCountKey.capitalize + private val NodesFormatString = "%s,%s,%s,%s\n" + private val EdgesFormatString = "%s,%s,%s,%s,%s,%s,%s,%s\n" + + val NodesHeader: String = NodesFormatString.format( + IdKey, NameKey, InfrastructureProviderKey, TierKey) + val EdgesHeader: String = EdgesFormatString.format( + IdKey, SourceId, DestinationId, StatsCount, StatsLastSeen, StatsErrorCount, EffectiveFromKey, EffectiveToKey) +} + +class JsonIntoDataFramesTransformer { + + def parseJson(jsonInput: String): NodesAndEdges = { + val jValue = parse(jsonInput, useBigDecimalForDouble = false, useBigIntForLong = true) + implicit val formats: DefaultFormats.type = DefaultFormats + val map = jValue.extract[Map[String, Any]] + val edgesList = map.getOrElse(EdgesKey, List[Any]()).asInstanceOf[List[Any]] + val nodeToIdMap = getNodeToIdMap(edgesList) + val nodes = createNodesCsvList(nodeToIdMap) + val edges = createEdgesCsvList(edgesList, nodeToIdMap) + NodesAndEdges(nodes, edges) + } + + private def createEdgesCsvList(edgesList: List[Any], nodeToIdMap: mutable.Map[Node, Long]): String = { + val stringBuilder = new mutable.StringBuilder(EdgesHeader) + var id = 1 + for { + edge <- edgesList + } yield { + val edgeAsMap = edge.asInstanceOf[Map[String, Any]] + val statsAsMap = edgeAsMap.getOrElse(StatsKey, Map[String, Any]()).asInstanceOf[Map[String, Any]] + val maybeSourceNode = findNode(SourceKey, edgeAsMap) + val maybeDestinationNode = findNode(DestinationKey, edgeAsMap) + val str = EdgesFormatString.format(id, + if (maybeSourceNode.isDefined) nodeToIdMap(maybeSourceNode.get).toString else "", + if (maybeDestinationNode.isDefined) nodeToIdMap(maybeDestinationNode.get).toString else "", + Try(statsAsMap(CountKey).asInstanceOf[BigInt].toString()).getOrElse(""), + Try(statsAsMap(LastSeenKey).asInstanceOf[BigInt].toString()).getOrElse(""), + Try(statsAsMap(ErrorCountKey).asInstanceOf[BigInt].toString()).getOrElse(""), + Try(edgeAsMap(EffectiveFromKey).asInstanceOf[BigInt].toString()).getOrElse(""), + Try(edgeAsMap(EffectiveToKey).asInstanceOf[BigInt].toString()).getOrElse("")) + stringBuilder.append(str) + id = id + 1 + } + stringBuilder.toString + } + + private def createNodesCsvList(nodeToIdMap: mutable.Map[Node, Long]): String = { + val stringBuilder = new mutable.StringBuilder(NodesHeader) + nodeToIdMap.foreach { + case (node, id) => + val name = surroundWithQuotesIfNecessary(node.name) + val infrastructureProvider = surroundWithQuotesIfNecessary(node.infrastructureProvider.getOrElse("")) + val tier = surroundWithQuotesIfNecessary(node.tier.getOrElse("")) + val str = NodesFormatString.format(id, name, infrastructureProvider, tier) + stringBuilder.append(str) + } + stringBuilder.toString + } + + // See http://www.creativyst.com/Doc/Articles/CSV/CSV01.htm#FileFormat + private def surroundWithQuotesIfNecessary(string: String): String = { + val stringWithEscapedQuotes = string.replaceAll("\"", "\"\"") + var stringToReturn = stringWithEscapedQuotes + if (string.startsWith(" ") || string.endsWith(" ") || string.contains(",")) { + stringToReturn = "\"" + string + "\"" + } + stringToReturn + } + + private def getNodeToIdMap(edgesList: List[Any]): mutable.Map[Node, Long] = { + val sourceNodes = findNodesOfType(edgesList, SourceKey) + val destinationNodes = findNodesOfType(edgesList, DestinationKey) + val nodes = (sourceNodes ::: destinationNodes).distinct + val nodeToIdMap = mutable.Map[Node, Long]() + var nodeId: Long = 1L + for (node <- nodes) { + nodeToIdMap(node) = nodeId + nodeId = nodeId + 1 + } + nodeToIdMap + } + + private def findNodesOfType(edgesList: List[Any], nodeType: String) = { + val nodes = for { + edge <- edgesList + } yield { + val edgeMap = edge.asInstanceOf[Map[String, Any]] + val sourceNode: Option[Node] = findNode(nodeType, edgeMap) + sourceNode + } + nodes.flatten + } + + private def findNode(nodeType: String, edgeMap: Map[String, Any]): Option[Node] = { + var optionNode: Option[Node] = None + val nodeOptionAny = edgeMap.get(nodeType) + if (nodeOptionAny.isDefined) { + val map = nodeOptionAny.get.asInstanceOf[Map[String, Any]] + val nameOptionAny = map.get(NameKey) + if (nameOptionAny.isDefined) { + val name = nameOptionAny.get.asInstanceOf[String] + val tags = map.getOrElse(TagsKey, Map.empty[String, String]).asInstanceOf[Map[String, String]] + val xHaystackInfrastructureProvider = Option(tags.getOrElse(InfrastructureProviderKey, null)) + val tier = Option(tags.getOrElse(TierKey, null)) + optionNode = Some(Node(name, xHaystackInfrastructureProvider, tier)) + } + } + optionNode + } +} diff --git a/service-graph/snapshot-store/src/main/scala/com/expedia/www/haystack/service/graph/snapshot/store/Node.scala b/service-graph/snapshot-store/src/main/scala/com/expedia/www/haystack/service/graph/snapshot/store/Node.scala new file mode 100644 index 000000000..69e35747c --- /dev/null +++ b/service-graph/snapshot-store/src/main/scala/com/expedia/www/haystack/service/graph/snapshot/store/Node.scala @@ -0,0 +1,22 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.service.graph.snapshot.store + +case class Node(name: String, + infrastructureProvider: Option[String], + tier: Option[String]) \ No newline at end of file diff --git a/service-graph/snapshot-store/src/main/scala/com/expedia/www/haystack/service/graph/snapshot/store/NodeWithId.scala b/service-graph/snapshot-store/src/main/scala/com/expedia/www/haystack/service/graph/snapshot/store/NodeWithId.scala new file mode 100644 index 000000000..13f661d5c --- /dev/null +++ b/service-graph/snapshot-store/src/main/scala/com/expedia/www/haystack/service/graph/snapshot/store/NodeWithId.scala @@ -0,0 +1,30 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.service.graph.snapshot.store + +case class NodeWithId(id: Long, + name: String, + xHaystackInfrastructureProvider: Option[String], + tier: Option[String]) + +object NodeWithId { + def nodeMapper: NodeWithId => Node = (nodeWithId: NodeWithId) => + Node(nodeWithId.name, + nodeWithId.xHaystackInfrastructureProvider, + nodeWithId.tier) +} \ No newline at end of file diff --git a/service-graph/snapshot-store/src/main/scala/com/expedia/www/haystack/service/graph/snapshot/store/NodesAndEdges.scala b/service-graph/snapshot-store/src/main/scala/com/expedia/www/haystack/service/graph/snapshot/store/NodesAndEdges.scala new file mode 100644 index 000000000..5c1cca6c0 --- /dev/null +++ b/service-graph/snapshot-store/src/main/scala/com/expedia/www/haystack/service/graph/snapshot/store/NodesAndEdges.scala @@ -0,0 +1,20 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.service.graph.snapshot.store + +case class NodesAndEdges(nodes: String, edges: String) diff --git a/service-graph/snapshot-store/src/main/scala/com/expedia/www/haystack/service/graph/snapshot/store/S3SnapshotStore.scala b/service-graph/snapshot-store/src/main/scala/com/expedia/www/haystack/service/graph/snapshot/store/S3SnapshotStore.scala new file mode 100644 index 000000000..a1af57d6a --- /dev/null +++ b/service-graph/snapshot-store/src/main/scala/com/expedia/www/haystack/service/graph/snapshot/store/S3SnapshotStore.scala @@ -0,0 +1,163 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.service.graph.snapshot.store + +import java.time.Instant + +import com.amazonaws.regions.Regions +import com.amazonaws.services.s3.{AmazonS3, AmazonS3ClientBuilder} +import com.amazonaws.services.s3.model.{ListObjectsV2Request, ListObjectsV2Result} +import com.expedia.www.haystack.service.graph.snapshot.store.Constants.{DotCsv, SlashEdges, SlashNodes} +import com.expedia.www.haystack.service.graph.snapshot.store.S3SnapshotStore.createItemName + +import scala.collection.JavaConverters._ +import scala.math.Ordering.String.max + +/** + * Companion object, with public AmazonS3 that can be set to a mock for unit tests + */ +object S3SnapshotStore { + var amazonS3: AmazonS3 = AmazonS3ClientBuilder.standard.withRegion(Regions.US_WEST_2).build + + def createItemName(folderName: String, fileName: String): String = { + s"$folderName/$fileName" + } + + +} + +/** + * Object that stores snapshots in S3 + * + * @param s3Client client with which to communicate with S3 + * @param bucketName name of the bucket + * @param folderName name of the "folder" in the bucket (becomes the prefix of the S3 item name) + * @param listObjectsBatchSize number of results to return with each listObjectsV2 request to S3; smaller values + * use less memory at the cost of more calls to S3. The best value would be the maximum + * number of snapshots that will exist in S3 before being purged; for example, with a + * one hour snapshot interval and a snapshot TTL of 1 year, 366 * 24 = 8784 would be a good + * value (perhaps rounded to 10,000). Using a "good" value for listObjectsBatchSize + * improves the performance of calls to read from the S3SnapshotStore. + */ +class S3SnapshotStore(val s3Client: AmazonS3, + val bucketName: String, + val folderName: String, + val listObjectsBatchSize: Int) extends SnapshotStore { + private val itemNamePrefix = folderName + "/" + + def this() = { + this(S3SnapshotStore.amazonS3, "", "", 0) + } + + /** + * Builds an S3SnapshotStore implementation given arguments to pass to the constructor + * + * @param constructorArguments + * - '''constructorArguments[0]''' is unused by this method but will be the fully qualified name of the + * S3SnapshotStore class, i.e. "com.expedia.www.haystack.service.graph.snapshot.store.S3SnapshotStore" + * - '''constructorArguments[1]''' must be a String that specifies the bucket + * - '''constructorArguments[2]''' must be a String that specifies the folder in the bucket + * - '''constructorArguments[3]''' must be a String that specifies the batch count when listing items in the bucket + * @return the S3SnapshotStore to use + */ + override def build(constructorArguments: Array[String]): SnapshotStore = { + val bucketName = constructorArguments(1) + val folderName = constructorArguments(2) + val listObjectsBatchSize = if (constructorArguments.length > 3) constructorArguments(3).toInt else 0 + new S3SnapshotStore(s3Client, bucketName, folderName, listObjectsBatchSize) + } + + /** + * Writes a string to the persistent store + * + * @param instant date/time of the write, used to create the name, which will later be used in read() and purge() + * @param content String to write + * @return the item names of the two objects written to S3 (does not include the bucket name): the first item name + * returned will end in "/nodes" and the other will end in "/edges" + */ + override def write(instant: Instant, + content: String): (String, String) = { + if (!s3Client.doesBucketExistV2(bucketName)) { + s3Client.createBucket(bucketName) + } + val nodesAndEdges = transformJsonToNodesAndEdges(content) + write(bucketName, instant, SlashNodes + DotCsv, nodesAndEdges.nodes) + write(bucketName, instant, SlashEdges + DotCsv, nodesAndEdges.edges) + val itemNameBase = createIso8601FileName(instant) + (createItemName(folderName, itemNameBase + SlashNodes), createItemName(folderName, itemNameBase + SlashEdges)) + } + + private def write(bucketName: String, instant: Instant, suffix: String, content: String) = { + val itemNameBase = createItemName(folderName, createIso8601FileName(instant)) + val itemName = itemNameBase + suffix + s3Client.putObject(bucketName, itemName, content) + } + + /** + * Reads content from the persistent store + * + * @param instant date/time of the read + * @return the content of the youngest item whose ISO-8601-based name is earlier or equal to instant + * @throws IllegalArgumentException if listObjectsBatchSize <= 0 + */ + override def read(instant: Instant): Option[String] = { + var optionString: Option[String] = None + val itemName = getItemNameOfYoungestNodesItemBeforeInstant(instant) + if (itemName.isDefined) { + val nodesItemName = itemName.get + val nodesRawData = s3Client.getObjectAsString(bucketName, nodesItemName) + val edgesItemName = nodesItemName.replace(SlashNodes, SlashEdges) + val edgesRawData = s3Client.getObjectAsString(bucketName, edgesItemName) + optionString = Some(transformNodesAndEdgesToJson(nodesRawData, edgesRawData)) + } + optionString + } + + private def getItemNameOfYoungestNodesItemBeforeInstant(instant: Instant): Option[String] = { + var optionString: Option[String] = None + if (listObjectsBatchSize > 0) { + val listObjectsV2Request = new ListObjectsV2Request().withBucketName(bucketName).withMaxKeys(listObjectsBatchSize) + val instantAsItemName = createItemName(folderName, createIso8601FileName(instant)) + var listObjectsV2Result: ListObjectsV2Result = null + do { + listObjectsV2Result = s3Client.listObjectsV2(bucketName) + val objectSummaries = listObjectsV2Result.getObjectSummaries.asScala + .filter(_.getKey.startsWith(itemNamePrefix)) + .filter(_.getKey.endsWith(SlashNodes)) + .filter(_.getKey.substring(0, instantAsItemName.length) <= instantAsItemName) + val potentialMax = if (objectSummaries.nonEmpty) Some(objectSummaries.maxBy(_.getKey).getKey) else None + (optionString, potentialMax) match { + case (None, None) => + optionString = None + case (None, Some(_)) => + optionString = potentialMax + case (Some(_), None) => + // optionString stays unchanged + case (Some(optionStringItemName), Some(potentialMaxItemName)) => + optionString = Some(max(optionStringItemName, potentialMaxItemName)) + } + listObjectsV2Request.setContinuationToken(listObjectsV2Result.getNextContinuationToken) + } while (listObjectsV2Result.isTruncated) + } else { + throw new IllegalArgumentException("S3SnapshotStore objects that read from S3 must be created with a positive " + + s"value of listObjectsBatchSize, not the [$listObjectsBatchSize] value that was provided") + } + optionString + } + +} diff --git a/service-graph/snapshot-store/src/main/scala/com/expedia/www/haystack/service/graph/snapshot/store/SnapshotStore.scala b/service-graph/snapshot-store/src/main/scala/com/expedia/www/haystack/service/graph/snapshot/store/SnapshotStore.scala new file mode 100644 index 000000000..1b82233d6 --- /dev/null +++ b/service-graph/snapshot-store/src/main/scala/com/expedia/www/haystack/service/graph/snapshot/store/SnapshotStore.scala @@ -0,0 +1,83 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.service.graph.snapshot.store + +import java.time.Instant +import java.time.format.DateTimeFormatterBuilder + +trait SnapshotStore { + /** + * Builds a SnapshotStore implementation given arguments to pass to the constructor + * + * @param constructorArguments arguments to pass to the constructor + * @return the concrete SnapshotStore to use + */ + def build(constructorArguments: Array[String]): SnapshotStore + + /** + * Writes a string to the persistent store + * + * @param instant date/time of the write, used to create the name, which will later be used in read() and purge() + * @param content String to write + * @return implementation-dependent value; see implementation documentation for details + */ + def write(instant: Instant, + content: String): AnyRef + + /** + * Reads content from the persistent store + * + * @param instant date/time of the read + * @return the content of the youngest item whose ISO-8601-based name is earlier or equal to instant + */ + def read(instant: Instant): Option[String] + + /** + * Purges items from the persistent store (optional operation; the S3 implementation of SnapshotStore will use an S3 + * lifecycle rule to purge items, but the file implementation must purge old files) + * + * @param instant date/time of items to be purged; items whose ISO-8601-based name is earlier than or equal to + * instant will be purged + * @return the number of items purged + */ + def purge(instant: Instant): Integer = { + 0 + // Override if purge code is needed by the particular SnapshotStore implementation + } + + private val formatter = new DateTimeFormatterBuilder().appendInstant(3).toFormatter + + def createIso8601FileName(instant: Instant): String = { + formatter.format(instant) + } + + // Not stateful, so only one object is needed + private val jsonIntoDataFramesTransformer = new JsonIntoDataFramesTransformer + + def transformJsonToNodesAndEdges(json: String): NodesAndEdges = { + jsonIntoDataFramesTransformer.parseJson(json) + } + + def transformNodesAndEdgesToJson(nodesRawData: String, + edgesRawData: String): String = { + // Stateful because of instance variable DataFramesIntoJsonTransformer.prependComma, so each parse needs one + val dataFramesIntoJsonTransformer = new DataFramesIntoJsonTransformer + + dataFramesIntoJsonTransformer.parseDataFrames(nodesRawData, edgesRawData) + } +} diff --git a/service-graph/snapshot-store/src/test/resources/serviceGraph.json b/service-graph/snapshot-store/src/test/resources/serviceGraph.json new file mode 100644 index 000000000..7f03f0708 --- /dev/null +++ b/service-graph/snapshot-store/src/test/resources/serviceGraph.json @@ -0,0 +1,2547 @@ +{ + "edges": [ + { + "source": { + "name": "detail-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws,dc" + } + }, + "destination": { + "name": "provideradapter-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 202931, + "lastSeen": 1544575410111, + "errorCount": 0 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587937 + }, + { + "source": { + "name": "daily-data-update-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "dc" + } + }, + "destination": { + "name": "async-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 5136140, + "lastSeen": 1544575571142, + "errorCount": 0 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587796 + }, + { + "source": { + "name": "search-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "dc" + } + }, + "destination": { + "name": "provideradapter-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 134125, + "lastSeen": 1544575498167, + "errorCount": 21882 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587999 + }, + { + "source": { + "name": "internet-proxy", + "tags": {} + }, + "destination": { + "name": "front-door-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws,dc" + } + }, + "stats": { + "count": 17367200, + "lastSeen": 1544575567920, + "errorCount": 103264 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575588026 + }, + { + "source": { + "name": "westeros-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "stark-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 157201, + "lastSeen": 1544575421793, + "errorCount": 36 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575588024 + }, + { + "source": { + "name": "internet-proxy", + "tags": {} + }, + "destination": { + "name": "rails-web", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 130, + "lastSeen": 1544573988361, + "errorCount": 0 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544574600000 + }, + { + "source": { + "name": "shopping-pricing", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "new-shopping-pricing", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 3531583, + "lastSeen": 1544575569804, + "errorCount": 0 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587705 + }, + { + "source": { + "name": "authentication-service", + "tags": {} + }, + "destination": { + "name": "userinteraction-web", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 76788, + "lastSeen": 1544575553640, + "errorCount": 0 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587815 + }, + { + "source": { + "name": "stark-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "context-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 180327, + "lastSeen": 1544575421874, + "errorCount": 2088 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587693 + }, + { + "source": { + "name": "stark-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "template-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 169216, + "lastSeen": 1544575421889, + "errorCount": 1 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575588068 + }, + { + "source": { + "name": "shopping-search-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "westeros-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 22890079, + "lastSeen": 1544575572251, + "errorCount": 0 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587672 + }, + { + "source": { + "name": "api-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "shopping-content-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 2, + "lastSeen": 1544554416805, + "errorCount": 0 + }, + "effectiveFrom": 1544553000000, + "effectiveTo": 1544554800000 + }, + { + "source": { + "name": "detail-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "dc" + } + }, + "destination": { + "name": "chargeback-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 60, + "lastSeen": 1544574583227, + "errorCount": 0 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544574600000 + }, + { + "source": { + "name": "detail-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws,dc" + } + }, + "destination": { + "name": "adapter-aws", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 303022, + "lastSeen": 1544575592096, + "errorCount": 1786 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587954 + }, + { + "source": { + "name": "new-shopping-pricing", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "multishop", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 3893347, + "lastSeen": 1544575569844, + "errorCount": 2 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587734 + }, + { + "source": { + "name": "westeros-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "guide-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 1442998, + "lastSeen": 1544575571677, + "errorCount": 61 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587753 + }, + { + "source": { + "name": "search-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws,dc" + } + }, + "destination": { + "name": "help-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 2826475, + "lastSeen": 1544575555819, + "errorCount": 1624211 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587681 + }, + { + "source": { + "name": "authentication-service", + "tags": {} + }, + "destination": { + "name": "front-door-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "dc" + } + }, + "stats": { + "count": 99413, + "lastSeen": 1544575499415, + "errorCount": 0 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587706 + }, + { + "source": { + "name": "boss-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "dc" + } + }, + "destination": { + "name": "provideradapter-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 3258, + "lastSeen": 1544575229219, + "errorCount": 800 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587979 + }, + { + "source": { + "name": "payment-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "fx", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 233615, + "lastSeen": 1544575277983, + "errorCount": 16 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587689 + }, + { + "source": { + "name": "api-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "location-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 196315, + "lastSeen": 1544575522183, + "errorCount": 4 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587866 + }, + { + "source": { + "name": "boss-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "booking-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 3, + "lastSeen": 1544559964099, + "errorCount": 1 + }, + "effectiveFrom": 1544558400000, + "effectiveTo": 1544560200000 + }, + { + "source": { + "name": "boss-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "dc" + } + }, + "destination": { + "name": "chargeback-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 169, + "lastSeen": 1544574617075, + "errorCount": 38 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575588010 + }, + { + "source": { + "name": "front-door-service", + "tags": { + "tier": "1,2", + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws,dc" + } + }, + "destination": { + "name": "checkout-payment-domain-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "dc" + } + }, + "stats": { + "count": 342981, + "lastSeen": 1544575571750, + "errorCount": 126 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587681 + }, + { + "source": { + "name": "forge-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "context-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 5042343, + "lastSeen": 1544575571591, + "errorCount": 405 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587826 + }, + { + "source": { + "name": "front-door-service", + "tags": { + "tier": "3,1,2", + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws,dc" + } + }, + "destination": { + "name": "location-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws,dc" + } + }, + "stats": { + "count": 21474934, + "lastSeen": 1544575571886, + "errorCount": 26012 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575588099 + }, + { + "source": { + "name": "internet-proxy", + "tags": {} + }, + "destination": { + "name": "his-web", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 698168, + "lastSeen": 1544575554236, + "errorCount": 87771 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587677 + }, + { + "source": { + "name": "internet-proxy", + "tags": {} + }, + "destination": { + "name": "hers-web", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 2907715, + "lastSeen": 1544575570922, + "errorCount": 33645 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587793 + }, + { + "source": { + "name": "front-door-service", + "tags": { + "tier": "1", + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "new-shopping-pricing", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 116, + "lastSeen": 1544574157710, + "errorCount": 0 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544574600000 + }, + { + "source": { + "name": "internet-proxy", + "tags": {} + }, + "destination": { + "name": "lpt-web", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 1821807, + "lastSeen": 1544575567232, + "errorCount": 6746 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575588011 + }, + { + "source": { + "name": "satellite", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "endurance-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 12111, + "lastSeen": 1544575506385, + "errorCount": 140 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575588024 + }, + { + "source": { + "name": "front-door-service", + "tags": { + "tier": "1", + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws,dc" + } + }, + "destination": { + "name": "detail-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws,dc" + } + }, + "stats": { + "count": 570805, + "lastSeen": 1544575504575, + "errorCount": 32 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587811 + }, + { + "source": { + "name": "internet-proxy", + "tags": {} + }, + "destination": { + "name": "search-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "dc" + } + }, + "stats": { + "count": 74, + "lastSeen": 1544573552180, + "errorCount": 0 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544574600000 + }, + { + "source": { + "name": "loom-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "lists-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 18356, + "lastSeen": 1544575525038, + "errorCount": 0 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575588011 + }, + { + "source": { + "name": "front-door-service", + "tags": { + "tier": "2", + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "dc" + } + }, + "destination": { + "name": "mars", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 73614, + "lastSeen": 1544575526411, + "errorCount": 0 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587848 + }, + { + "source": { + "name": "mormont-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "seo-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 596144, + "lastSeen": 1544575525706, + "errorCount": 3 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587937 + }, + { + "source": { + "name": "info-site-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "shopping-cart", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 6, + "lastSeen": 1544572410049, + "errorCount": 0 + }, + "effectiveFrom": 1544500800000, + "effectiveTo": 1544572800000 + }, + { + "source": { + "name": "shopping-detail-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "shopping-pricing", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 2838890, + "lastSeen": 1544575569418, + "errorCount": 0 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587682 + }, + { + "source": { + "name": "loom-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "api-customer", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 8583, + "lastSeen": 1544575208652, + "errorCount": 0 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587796 + }, + { + "source": { + "name": "multishop", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "pricing-engine", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 15560819, + "lastSeen": 1544575569839, + "errorCount": 35 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587689 + }, + { + "source": { + "name": "westeros-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "forge-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 3161466, + "lastSeen": 1544575571588, + "errorCount": 4817 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587639 + }, + { + "source": { + "name": "front-door-service", + "tags": { + "tier": "2", + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws,dc" + } + }, + "destination": { + "name": "third-party-provider-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 9735, + "lastSeen": 1544575279207, + "errorCount": 3 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587827 + }, + { + "source": { + "name": "internet-proxy", + "tags": {} + }, + "destination": { + "name": "insurance-shopping-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 410, + "lastSeen": 1544574748096, + "errorCount": 0 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587918 + }, + { + "source": { + "name": "shopping-detail-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "shopping-user-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 1214950, + "lastSeen": 1544575528611, + "errorCount": 0 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587723 + }, + { + "source": { + "name": "ticket-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "boss-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "dc" + } + }, + "stats": { + "count": 20845, + "lastSeen": 1544575478589, + "errorCount": 68 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587644 + }, + { + "source": { + "name": "internet-proxy", + "tags": {} + }, + "destination": { + "name": "third-party-api-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 5000, + "lastSeen": 1544574936681, + "errorCount": 0 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587722 + }, + { + "source": { + "name": "internet-proxy", + "tags": {} + }, + "destination": { + "name": "progressive-webapp-api", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 687220, + "lastSeen": 1544575524183, + "errorCount": 20 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587866 + }, + { + "source": { + "name": "internet-proxy", + "tags": {} + }, + "destination": { + "name": "compositor-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 243059, + "lastSeen": 1544575396324, + "errorCount": 22 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575588026 + }, + { + "source": { + "name": "westeros-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "mormont-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 652939, + "lastSeen": 1544575525612, + "errorCount": 68 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587815 + }, + { + "source": { + "name": "boss-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "dc" + } + }, + "destination": { + "name": "ticket-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 27317, + "lastSeen": 1544575505207, + "errorCount": 0 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575588010 + }, + { + "source": { + "name": "internet-proxy", + "tags": {} + }, + "destination": { + "name": "userinteraction-web", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 2907462, + "lastSeen": 1544575570897, + "errorCount": 10681 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587808 + }, + { + "source": { + "name": "internet-proxy", + "tags": {} + }, + "destination": { + "name": "info-site-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 84704, + "lastSeen": 1544575480474, + "errorCount": 0 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587827 + }, + { + "source": { + "name": "boss-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "dc" + } + }, + "destination": { + "name": "melisandre-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 125064, + "lastSeen": 1544575501716, + "errorCount": 2985 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575588087 + }, + { + "source": { + "name": "progressive-webapp-api", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "shopping-detail-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 2469688, + "lastSeen": 1544575569284, + "errorCount": 0 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587909 + }, + { + "source": { + "name": "guide-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "template-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 806, + "lastSeen": 1544573623862, + "errorCount": 0 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544574600000 + }, + { + "source": { + "name": "api-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "shopping-detail-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 8, + "lastSeen": 1544569762201, + "errorCount": 0 + }, + "effectiveFrom": 1544491800000, + "effectiveTo": 1544571000000 + }, + { + "source": { + "name": "search-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws,dc" + } + }, + "destination": { + "name": "controller-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 20783217, + "lastSeen": 1544575572498, + "errorCount": 246 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587641 + }, + { + "source": { + "name": "progressive-webapp-api", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "shopping-search-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 973286, + "lastSeen": 1544575570804, + "errorCount": 0 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575588061 + }, + { + "source": { + "name": "endurance-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "location-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 18729, + "lastSeen": 1544575266335, + "errorCount": 16 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587723 + }, + { + "source": { + "name": "help-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "adapter-aws", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 379955, + "lastSeen": 1544575554493, + "errorCount": 0 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587808 + }, + { + "source": { + "name": "internet-proxy", + "tags": {} + }, + "destination": { + "name": "targaryen-web", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 8861, + "lastSeen": 1544575485661, + "errorCount": 324 + }, + "effectiveFrom": 1544500800000, + "effectiveTo": 1544575587634 + }, + { + "source": { + "name": "guide-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "seo-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 654, + "lastSeen": 1544573560757, + "errorCount": 0 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544574600000 + }, + { + "source": { + "name": "internet-proxy", + "tags": {} + }, + "destination": { + "name": "drogo-api", + "tags": {} + }, + "stats": { + "count": 11154, + "lastSeen": 1544575207980, + "errorCount": 11 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587669 + }, + { + "source": { + "name": "payment-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "lannister-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 13974992, + "lastSeen": 1544575556069, + "errorCount": 9 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587798 + }, + { + "source": { + "name": "info-site-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "detail-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws,dc" + } + }, + "stats": { + "count": 234369, + "lastSeen": 1544575570813, + "errorCount": 6024 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587952 + }, + { + "source": { + "name": "shopping-search-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "shopping-content-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 19029006, + "lastSeen": 1544575571645, + "errorCount": 0 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587677 + }, + { + "source": { + "name": "new-shopping-pricing", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "fx", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 304458, + "lastSeen": 1544575397477, + "errorCount": 5 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587674 + }, + { + "source": { + "name": "internet-proxy", + "tags": {} + }, + "destination": { + "name": "loom-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 90053, + "lastSeen": 1544575481890, + "errorCount": 7 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587788 + }, + { + "source": { + "name": "new-shopping-pricing", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "payment-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 20680712, + "lastSeen": 1544575556069, + "errorCount": 32 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587782 + }, + { + "source": { + "name": "api-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "shopping-search-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 56878, + "lastSeen": 1544575522176, + "errorCount": 0 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587882 + }, + { + "source": { + "name": "search-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws,dc" + } + }, + "destination": { + "name": "margaery-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 14646996, + "lastSeen": 1544575571980, + "errorCount": 145 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587689 + }, + { + "source": { + "name": "loom-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "api-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 985553, + "lastSeen": 1544575571604, + "errorCount": 0 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575588060 + }, + { + "source": { + "name": "tyrion-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "chargeback-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 176, + "lastSeen": 1544573823589, + "errorCount": 0 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544574600000 + }, + { + "source": { + "name": "help-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "rules-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 237982, + "lastSeen": 1544575571933, + "errorCount": 0 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587954 + }, + { + "source": { + "name": "shopping-search-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "shopping-pricing", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 33794745, + "lastSeen": 1544575571645, + "errorCount": 0 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587875 + }, + { + "source": { + "name": "front-door-service", + "tags": { + "tier": "2", + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "dc" + } + }, + "destination": { + "name": "bolton-service", + "tags": {} + }, + "stats": { + "count": 319715, + "lastSeen": 1544575500355, + "errorCount": 1004 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587658 + }, + { + "source": { + "name": "ticket-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "melisandre-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 13072, + "lastSeen": 1544575568510, + "errorCount": 59 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587716 + }, + { + "source": { + "name": "help-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "provideradapter-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 1403232, + "lastSeen": 1544575555851, + "errorCount": 0 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587827 + }, + { + "source": { + "name": "tyrion-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "provideradapter-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 26, + "lastSeen": 1544571310584, + "errorCount": 0 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544572800000 + }, + { + "source": { + "name": "internet-proxy", + "tags": {} + }, + "destination": { + "name": "location-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 32518, + "lastSeen": 1544575275982, + "errorCount": 75 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587870 + }, + { + "source": { + "name": "forge-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "template-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 3548918, + "lastSeen": 1544575571596, + "errorCount": 30 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587738 + }, + { + "source": { + "name": "tips-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "location-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 764, + "lastSeen": 1544574548576, + "errorCount": 0 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544574600000 + }, + { + "source": { + "name": "shopping-search-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "baratheon-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 578577, + "lastSeen": 1544575571747, + "errorCount": 0 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587662 + }, + { + "source": { + "name": "westeros-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "hodor-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 6512590, + "lastSeen": 1544575570949, + "errorCount": 1296924 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587762 + }, + { + "source": { + "name": "front-door-service", + "tags": { + "tier": "1", + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "dc" + } + }, + "destination": { + "name": "ecommerce-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 73, + "lastSeen": 1544572581996, + "errorCount": 0 + }, + "effectiveFrom": 1544493600000, + "effectiveTo": 1544572800000 + }, + { + "source": { + "name": "detail-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws,dc" + } + }, + "destination": { + "name": "provideradapter-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 9094, + "lastSeen": 1544575240906, + "errorCount": 0 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575588090 + }, + { + "source": { + "name": "forge-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "seo-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 3369864, + "lastSeen": 1544575571631, + "errorCount": 6 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587848 + }, + { + "source": { + "name": "internet-proxy", + "tags": {} + }, + "destination": { + "name": "varys-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 68388, + "lastSeen": 1544575520781, + "errorCount": 80 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587934 + }, + { + "source": { + "name": "bronn-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "location-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 99784, + "lastSeen": 1544575571751, + "errorCount": 0 + }, + "effectiveFrom": 1544563800000, + "effectiveTo": 1544575587662 + }, + { + "source": { + "name": "stark-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "seo-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 167207, + "lastSeen": 1544575421914, + "errorCount": 1 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587725 + }, + { + "source": { + "name": "tyrion-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "adapter-aws", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 8112, + "lastSeen": 1544575271597, + "errorCount": 0 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575588026 + }, + { + "source": { + "name": "guide-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "context-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 1984413, + "lastSeen": 1544575571681, + "errorCount": 1983415 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587723 + }, + { + "source": { + "name": "boss-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "dc" + } + }, + "destination": { + "name": "booking-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 37252, + "lastSeen": 1544575265116, + "errorCount": 1136 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575588025 + }, + { + "source": { + "name": "shopping-search-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "location-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 1473126, + "lastSeen": 1544575571747, + "errorCount": 0 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587672 + }, + { + "source": { + "name": "shopping-search-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "bronn-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 1, + "lastSeen": 1544565534137, + "errorCount": 0 + }, + "effectiveFrom": 1544563800000, + "effectiveTo": 1544565600000 + }, + { + "source": { + "name": "baratheon-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "bronn-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 100667, + "lastSeen": 1544575571750, + "errorCount": 1 + }, + "effectiveFrom": 1544563800000, + "effectiveTo": 1544575587848 + }, + { + "source": { + "name": "varys-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "location-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 3, + "lastSeen": 1544552474213, + "errorCount": 0 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544553000000 + }, + { + "source": { + "name": "endurance-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "authentication-service", + "tags": {} + }, + "stats": { + "count": 8719, + "lastSeen": 1544575265175, + "errorCount": 77 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587674 + }, + { + "source": { + "name": "brienne-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "geo-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 38627, + "lastSeen": 1544557965110, + "errorCount": 0 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544558400000 + }, + { + "source": { + "name": "front-door-service", + "tags": { + "tier": "1", + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws,dc" + } + }, + "destination": { + "name": "search-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws,dc" + } + }, + "stats": { + "count": 587165, + "lastSeen": 1544575520505, + "errorCount": 1 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587935 + }, + { + "source": { + "name": "front-door-service", + "tags": { + "tier": "1", + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "dc" + } + }, + "destination": { + "name": "cache-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 72562, + "lastSeen": 1544575498524, + "errorCount": 206 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587934 + }, + { + "source": { + "name": "hodor-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "location-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 10500513, + "lastSeen": 1544575570958, + "errorCount": 76483 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575588061 + }, + { + "source": { + "name": "margaery-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "airpricingservice", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 16824540, + "lastSeen": 1544575571994, + "errorCount": 0 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587641 + }, + { + "source": { + "name": "shopping-content-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "location-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 30148726, + "lastSeen": 1544575571973, + "errorCount": 0 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575588099 + }, + { + "source": { + "name": "front-door-service", + "tags": { + "tier": "1", + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "dc" + } + }, + "destination": { + "name": "domain-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 46, + "lastSeen": 1544569236656, + "errorCount": 0 + }, + "effectiveFrom": 1544497200000, + "effectiveTo": 1544571000000 + }, + { + "source": { + "name": "progressive-webapp-api", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "shopping-content-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 5277998, + "lastSeen": 1544575572135, + "errorCount": 0 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587909 + }, + { + "source": { + "name": "progressive-webapp-api", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "location-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 3891685, + "lastSeen": 1544575571930, + "errorCount": 0 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575588087 + }, + { + "source": { + "name": "front-door-service", + "tags": { + "tier": "2", + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws,dc" + } + }, + "destination": { + "name": "authentication-service", + "tags": {} + }, + "stats": { + "count": 170461, + "lastSeen": 1544575499257, + "errorCount": 11 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587699 + }, + { + "source": { + "name": "internet-proxy", + "tags": {} + }, + "destination": { + "name": "session-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 611347, + "lastSeen": 1544575568839, + "errorCount": 1 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575588024 + }, + { + "source": { + "name": "ticket-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "booking-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 13140, + "lastSeen": 1544575218424, + "errorCount": 23 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587914 + }, + { + "source": { + "name": "shopping-detail-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "shopping-content-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 5779855, + "lastSeen": 1544575569418, + "errorCount": 0 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587937 + }, + { + "source": { + "name": "front-door-service", + "tags": { + "tier": "2", + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "dc" + } + }, + "destination": { + "name": "greyjoy-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 3984995, + "lastSeen": 1544575570003, + "errorCount": 106528 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587704 + }, + { + "source": { + "name": "boss-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "ticket-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 2, + "lastSeen": 1544559967407, + "errorCount": 0 + }, + "effectiveFrom": 1544558400000, + "effectiveTo": 1544560200000 + }, + { + "source": { + "name": "detail-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws,dc" + } + }, + "destination": { + "name": "margaery-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 2674151, + "lastSeen": 1544575594133, + "errorCount": 49 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587953 + }, + { + "source": { + "name": "shopping-search-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "shopping-user-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 18737197, + "lastSeen": 1544575571645, + "errorCount": 0 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587979 + }, + { + "source": { + "name": "internet-proxy", + "tags": {} + }, + "destination": { + "name": "satellite", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 217364, + "lastSeen": 1544575482983, + "errorCount": 290 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587723 + }, + { + "source": { + "name": "internet-proxy", + "tags": {} + }, + "destination": { + "name": "shopping-cart", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 10386, + "lastSeen": 1544575280832, + "errorCount": 204 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587797 + }, + { + "source": { + "name": "mormont-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "context-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 844962, + "lastSeen": 1544575554067, + "errorCount": 28241 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587815 + }, + { + "source": { + "name": "internet-proxy", + "tags": {} + }, + "destination": { + "name": "user-profile-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 151657, + "lastSeen": 1544575488509, + "errorCount": 12 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575588060 + }, + { + "source": { + "name": "front-door-service", + "tags": { + "tier": "1", + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "dc" + } + }, + "destination": { + "name": "shae-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "dc" + } + }, + "stats": { + "count": 7126, + "lastSeen": 1544575152362, + "errorCount": 0 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587866 + }, + { + "source": { + "name": "mormont-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "template-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 832484, + "lastSeen": 1544575554075, + "errorCount": 5 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587723 + }, + { + "source": { + "name": "detail-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "dc" + } + }, + "destination": { + "name": "rules-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 586462, + "lastSeen": 1544575592045, + "errorCount": 0 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587763 + }, + { + "source": { + "name": "detail-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "dc" + } + }, + "destination": { + "name": "controller-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 1297487, + "lastSeen": 1544575592045, + "errorCount": 0 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587693 + } + ] +} \ No newline at end of file diff --git a/service-graph/snapshot-store/src/test/resources/serviceGraph_edges.csv b/service-graph/snapshot-store/src/test/resources/serviceGraph_edges.csv new file mode 100644 index 000000000..3d4ebfa64 --- /dev/null +++ b/service-graph/snapshot-store/src/test/resources/serviceGraph_edges.csv @@ -0,0 +1,124 @@ +id,sourceId,destinationId,statsCount,statsLastSeen,statsErrorCount,effectiveFrom,effectiveTo +1,1,45,202931,1544575410111,0,1544488200000,1544575587937 +2,2,46,5136140,1544575571142,0,1544488200000,1544575587796 +3,3,45,134125,1544575498167,21882,1544488200000,1544575587999 +4,4,47,17367200,1544575567920,103264,1544488200000,1544575588026 +5,5,8,157201,1544575421793,36,1544488200000,1544575588024 +6,4,48,130,1544573988361,0,1544488200000,1544574600000 +7,6,12,3531583,1544575569804,0,1544488200000,1544575587705 +8,7,49,76788,1544575553640,0,1544488200000,1544575587815 +9,8,50,180327,1544575421874,2088,1544488200000,1544575587693 +10,8,51,169216,1544575421889,1,1544488200000,1544575588068 +11,9,5,22890079,1544575572251,0,1544488200000,1544575587672 +12,10,44,2,1544554416805,0,1544553000000,1544554800000 +13,11,52,60,1544574583227,0,1544488200000,1544574600000 +14,1,53,303022,1544575592096,1786,1544488200000,1544575587954 +15,12,28,3893347,1544575569844,2,1544488200000,1544575587734 +16,5,32,1442998,1544575571677,61,1544488200000,1544575587753 +17,13,34,2826475,1544575555819,1624211,1544488200000,1544575587681 +18,7,54,99413,1544575499415,0,1544488200000,1544575587706 +19,14,45,3258,1544575229219,800,1544488200000,1544575587979 +20,15,55,233615,1544575277983,16,1544488200000,1544575587689 +21,10,56,196315,1544575522183,4,1544488200000,1544575587866 +22,16,57,3,1544559964099,1,1544558400000,1544560200000 +23,14,52,169,1544574617075,38,1544488200000,1544575588010 +24,17,58,342981,1544575571750,126,1544488200000,1544575587681 +25,18,50,5042343,1544575571591,405,1544488200000,1544575587826 +26,19,59,21474934,1544575571886,26012,1544488200000,1544575588099 +27,4,60,698168,1544575554236,87771,1544488200000,1544575587677 +28,4,61,2907715,1544575570922,33645,1544488200000,1544575587793 +29,20,12,116,1544574157710,0,1544488200000,1544574600000 +30,4,62,1821807,1544575567232,6746,1544488200000,1544575588011 +31,21,33,12111,1544575506385,140,1544488200000,1544575588024 +32,22,1,570805,1544575504575,32,1544488200000,1544575587811 +33,4,3,74,1544573552180,0,1544488200000,1544574600000 +34,23,63,18356,1544575525038,0,1544488200000,1544575588011 +35,24,64,73614,1544575526411,0,1544488200000,1544575587848 +36,25,65,596144,1544575525706,3,1544488200000,1544575587937 +37,26,66,6,1544572410049,0,1544500800000,1544572800000 +38,27,6,2838890,1544575569418,0,1544488200000,1544575587682 +39,23,67,8583,1544575208652,0,1544488200000,1544575587796 +40,28,68,15560819,1544575569839,35,1544488200000,1544575587689 +41,5,18,3161466,1544575571588,4817,1544488200000,1544575587639 +42,29,69,9735,1544575279207,3,1544488200000,1544575587827 +43,4,70,410,1544574748096,0,1544488200000,1544575587918 +44,27,71,1214950,1544575528611,0,1544488200000,1544575587723 +45,30,14,20845,1544575478589,68,1544488200000,1544575587644 +46,4,72,5000,1544574936681,0,1544488200000,1544575587722 +47,4,31,687220,1544575524183,20,1544488200000,1544575587866 +48,4,73,243059,1544575396324,22,1544488200000,1544575588026 +49,5,25,652939,1544575525612,68,1544488200000,1544575587815 +50,14,30,27317,1544575505207,0,1544488200000,1544575588010 +51,4,49,2907462,1544575570897,10681,1544488200000,1544575587808 +52,4,26,84704,1544575480474,0,1544488200000,1544575587827 +53,14,74,125064,1544575501716,2985,1544488200000,1544575588087 +54,31,27,2469688,1544575569284,0,1544488200000,1544575587909 +55,32,51,806,1544573623862,0,1544488200000,1544574600000 +56,10,27,8,1544569762201,0,1544491800000,1544571000000 +57,13,75,20783217,1544575572498,246,1544488200000,1544575587641 +58,31,9,973286,1544575570804,0,1544488200000,1544575588061 +59,33,56,18729,1544575266335,16,1544488200000,1544575587723 +60,34,53,379955,1544575554493,0,1544488200000,1544575587808 +61,4,76,8861,1544575485661,324,1544500800000,1544575587634 +62,32,65,654,1544573560757,0,1544488200000,1544574600000 +63,4,77,11154,1544575207980,11,1544488200000,1544575587669 +64,15,78,13974992,1544575556069,9,1544488200000,1544575587798 +65,26,1,234369,1544575570813,6024,1544488200000,1544575587952 +66,9,44,19029006,1544575571645,0,1544488200000,1544575587677 +67,12,55,304458,1544575397477,5,1544488200000,1544575587674 +68,4,23,90053,1544575481890,7,1544488200000,1544575587788 +69,12,15,20680712,1544575556069,32,1544488200000,1544575587782 +70,10,9,56878,1544575522176,0,1544488200000,1544575587882 +71,13,43,14646996,1544575571980,145,1544488200000,1544575587689 +72,23,10,985553,1544575571604,0,1544488200000,1544575588060 +73,35,52,176,1544573823589,0,1544488200000,1544574600000 +74,34,79,237982,1544575571933,0,1544488200000,1544575587954 +75,9,6,33794745,1544575571645,0,1544488200000,1544575587875 +76,24,80,319715,1544575500355,1004,1544488200000,1544575587658 +77,30,74,13072,1544575568510,59,1544488200000,1544575587716 +78,34,45,1403232,1544575555851,0,1544488200000,1544575587827 +79,35,45,26,1544571310584,0,1544488200000,1544572800000 +80,4,56,32518,1544575275982,75,1544488200000,1544575587870 +81,18,51,3548918,1544575571596,30,1544488200000,1544575587738 +82,36,56,764,1544574548576,0,1544488200000,1544574600000 +83,9,39,578577,1544575571747,0,1544488200000,1544575587662 +84,5,42,6512590,1544575570949,1296924,1544488200000,1544575587762 +85,37,81,73,1544572581996,0,1544493600000,1544572800000 +86,1,45,9094,1544575240906,0,1544488200000,1544575588090 +87,18,65,3369864,1544575571631,6,1544488200000,1544575587848 +88,4,40,68388,1544575520781,80,1544488200000,1544575587934 +89,38,56,99784,1544575571751,0,1544563800000,1544575587662 +90,8,65,167207,1544575421914,1,1544488200000,1544575587725 +91,35,53,8112,1544575271597,0,1544488200000,1544575588026 +92,32,50,1984413,1544575571681,1983415,1544488200000,1544575587723 +93,14,57,37252,1544575265116,1136,1544488200000,1544575588025 +94,9,56,1473126,1544575571747,0,1544488200000,1544575587672 +95,9,38,1,1544565534137,0,1544563800000,1544565600000 +96,39,38,100667,1544575571750,1,1544563800000,1544575587848 +97,40,56,3,1544552474213,0,1544488200000,1544553000000 +98,33,7,8719,1544575265175,77,1544488200000,1544575587674 +99,41,82,38627,1544557965110,0,1544488200000,1544558400000 +100,22,13,587165,1544575520505,1,1544488200000,1544575587935 +101,37,83,72562,1544575498524,206,1544488200000,1544575587934 +102,42,56,10500513,1544575570958,76483,1544488200000,1544575588061 +103,43,84,16824540,1544575571994,0,1544488200000,1544575587641 +104,44,56,30148726,1544575571973,0,1544488200000,1544575588099 +105,37,85,46,1544569236656,0,1544497200000,1544571000000 +106,31,44,5277998,1544575572135,0,1544488200000,1544575587909 +107,31,56,3891685,1544575571930,0,1544488200000,1544575588087 +108,29,7,170461,1544575499257,11,1544488200000,1544575587699 +109,4,86,611347,1544575568839,1,1544488200000,1544575588024 +110,30,57,13140,1544575218424,23,1544488200000,1544575587914 +111,27,44,5779855,1544575569418,0,1544488200000,1544575587937 +112,24,87,3984995,1544575570003,106528,1544488200000,1544575587704 +113,16,30,2,1544559967407,0,1544558400000,1544560200000 +114,1,43,2674151,1544575594133,49,1544488200000,1544575587953 +115,9,71,18737197,1544575571645,0,1544488200000,1544575587979 +116,4,21,217364,1544575482983,290,1544488200000,1544575587723 +117,4,66,10386,1544575280832,204,1544488200000,1544575587797 +118,25,50,844962,1544575554067,28241,1544488200000,1544575587815 +119,4,88,151657,1544575488509,12,1544488200000,1544575588060 +120,37,89,7126,1544575152362,0,1544488200000,1544575587866 +121,25,51,832484,1544575554075,5,1544488200000,1544575587723 +122,11,79,586462,1544575592045,0,1544488200000,1544575587763 +123,11,75,1297487,1544575592045,0,1544488200000,1544575587693 diff --git a/service-graph/snapshot-store/src/test/resources/serviceGraph_nodes.csv b/service-graph/snapshot-store/src/test/resources/serviceGraph_nodes.csv new file mode 100644 index 000000000..6f38ed66c --- /dev/null +++ b/service-graph/snapshot-store/src/test/resources/serviceGraph_nodes.csv @@ -0,0 +1,90 @@ +id,name,X-HAYSTACK-INFRASTRUCTURE-PROVIDER,tier +38,bronn-service,aws, +19,front-door-service,"aws,dc","3,1,2" +68,pricing-engine,aws, +54,front-door-service,dc, +77,drogo-api,, +30,ticket-service,aws, +71,shopping-user-service,aws, +10,api-service,aws, +15,payment-service,aws, +88,user-profile-service,aws, +63,lists-service,aws, +20,front-door-service,aws,1 +66,shopping-cart,aws, +80,bolton-service,, +81,ecommerce-service,aws, +14,boss-service,dc, +6,shopping-pricing,aws, +70,insurance-shopping-service,aws, +65,seo-service,aws, +41,brienne-service,aws, +45,provideradapter-service,aws, +55,fx,aws, +2,daily-data-update-service,dc, +34,help-service,aws, +11,detail-service,dc, +46,async-service,aws, +26,info-site-service,aws, +22,front-door-service,"aws,dc",1 +39,baratheon-service,aws, +89,shae-service,dc, +53,adapter-aws,aws, +31,progressive-webapp-api,aws, +37,front-door-service,dc,1 +85,domain-service,aws, +62,lpt-web,aws, +69,third-party-provider-service,aws, +40,varys-service,aws, +78,lannister-service,aws, +7,authentication-service,, +76,targaryen-web,aws, +61,hers-web,aws, +25,mormont-service,aws, +82,geo-service,aws, +1,detail-service,"aws,dc", +4,internet-proxy,, +86,session-service,aws, +24,front-door-service,dc,2 +33,endurance-service,aws, +72,third-party-api-service,aws, +49,userinteraction-web,aws, +9,shopping-search-service,aws, +17,front-door-service,"aws,dc","1,2" +13,search-service,"aws,dc", +64,mars,aws, +12,new-shopping-pricing,aws, +67,api-customer,aws, +8,stark-service,aws, +27,shopping-detail-service,aws, +36,tips-service,aws, +44,shopping-content-service,aws, +83,cache-service,aws, +58,checkout-payment-domain-service,dc, +16,boss-service,aws, +23,loom-service,aws, +79,rules-service,aws, +48,rails-web,aws, +5,westeros-service,aws, +35,tyrion-service,aws, +75,controller-service,aws, +51,template-service,aws, +60,his-web,aws, +28,multishop,aws, +87,greyjoy-service,aws, +84,airpricingservice,aws, +32,guide-service,aws, +18,forge-service,aws, +47,front-door-service,"aws,dc", +73,compositor-service,aws, +42,hodor-service,aws, +43,margaery-service,aws, +52,chargeback-service,aws, +59,location-service,"aws,dc", +56,location-service,aws, +29,front-door-service,"aws,dc",2 +21,satellite,aws, +3,search-service,dc, +74,melisandre-service,aws, +50,context-service,aws, +57,booking-service,aws, diff --git a/service-graph/snapshot-store/src/test/scala/com/expedia/www/haystack/service/graph/snapshot/store/DataFramesIntoJsonTransformerSpec.scala b/service-graph/snapshot-store/src/test/scala/com/expedia/www/haystack/service/graph/snapshot/store/DataFramesIntoJsonTransformerSpec.scala new file mode 100644 index 000000000..bd9fd3ec3 --- /dev/null +++ b/service-graph/snapshot-store/src/test/scala/com/expedia/www/haystack/service/graph/snapshot/store/DataFramesIntoJsonTransformerSpec.scala @@ -0,0 +1,66 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.service.graph.snapshot.store + +import com.expedia.www.haystack.service.graph.snapshot.store.Constants._ +import com.expedia.www.haystack.service.graph.snapshot.store.DataFramesIntoJsonTransformer.{AddToMapError, WriteError} +import kantan.csv.ReadError +import org.mockito.Mockito +import org.scalatest.mockito.MockitoSugar +import org.scalatest.{FunSpec, Matchers, PrivateMethodTester} +import org.slf4j.Logger + +import scala.collection.mutable + +class DataFramesIntoJsonTransformerSpec extends FunSpec with Matchers with MockitoSugar with PrivateMethodTester { + private val stringSnapshotStoreSpecBase = new SnapshotStoreSpecBase + private val mockLogger = mock[Logger] + private val mockReadError = mock[ReadError] + private val emptyMap = mutable.Map.empty[Long, Node] + + describe("DataFramesIntoJsonTransformerSpec.parseDataFrames()") { + val dataFramesIntoJsonTransformer = new DataFramesIntoJsonTransformer(mockLogger) + it("should parse service graph nodes and edges into JSON") { + val nodesRawData = stringSnapshotStoreSpecBase.readFile(NodesCsvFileNameWithExtension) + val edgesRawData = stringSnapshotStoreSpecBase.readFile(EdgesCsvFileNameWithExtension) + val json = dataFramesIntoJsonTransformer.parseDataFrames(nodesRawData, edgesRawData) + json shouldEqual stringSnapshotStoreSpecBase.readFile(JsonFileNameWithExtension) + Mockito.verifyNoMoreInteractions(mockLogger, mockReadError) + } + } + + describe("DataFramesIntoJsonTransformerSpec.write()") { + val dataFramesIntoJsonTransformer = new DataFramesIntoJsonTransformer(mockLogger) + it("should log an error when it sees a ReadError") { + val write = PrivateMethod[Unit]('write) + dataFramesIntoJsonTransformer invokePrivate write(new StringBuilder, emptyMap, Left(mockReadError)) + Mockito.verify(mockLogger).error(WriteError, mockReadError) + Mockito.verifyNoMoreInteractions(mockLogger, mockReadError) + } + } + + describe("DataFramesIntoJsonTransformerSpec.addToMap()") { + val dataFramesIntoJsonTransformer = new DataFramesIntoJsonTransformer(mockLogger) + it("should log an error when it sees a ReadError") { + val addToMap = PrivateMethod[Unit]('addToMap) + dataFramesIntoJsonTransformer invokePrivate addToMap(emptyMap, Left(mockReadError)) + Mockito.verify(mockLogger).error(AddToMapError, mockReadError) + Mockito.verifyNoMoreInteractions(mockLogger, mockReadError) + } + } +} diff --git a/service-graph/snapshot-store/src/test/scala/com/expedia/www/haystack/service/graph/snapshot/store/FileSnapshotStoreSpec.scala b/service-graph/snapshot-store/src/test/scala/com/expedia/www/haystack/service/graph/snapshot/store/FileSnapshotStoreSpec.scala new file mode 100644 index 000000000..e163f43f5 --- /dev/null +++ b/service-graph/snapshot-store/src/test/scala/com/expedia/www/haystack/service/graph/snapshot/store/FileSnapshotStoreSpec.scala @@ -0,0 +1,74 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.service.graph.snapshot.store + +import java.io.File +import java.nio.file.{Files, Path, Paths} + +import com.expedia.www.haystack.service.graph.snapshot.store.Constants.JsonFileNameWithExtension + +class FileSnapshotStoreSpec extends SnapshotStoreSpecBase { + private val directory = Files.createTempDirectory("FileSnapshotStoreSpec") + directory.toFile.deleteOnExit() + + private val directoryName = directory.toFile.getCanonicalPath + private val serviceGraphJson = readFile(JsonFileNameWithExtension) + + describe("FileSnapshotStore") { + { + val defaultFaultSnapshotStore = new FileSnapshotStore + val fileSnapshotStore = defaultFaultSnapshotStore.build(Array(directoryName)) + it("should use an existing directory without trying to create it when writing") { + val pathsFromWrite = fileSnapshotStore.write(now, serviceGraphJson).asInstanceOf[(Path, Path)] + assert(pathsFromWrite._1.toFile.getCanonicalPath.startsWith(directoryName)) + assert(pathsFromWrite._2.toFile.getCanonicalPath.startsWith(directoryName)) + val iso8601FileName = fileSnapshotStore.createIso8601FileName(now) + assert(pathsFromWrite._1.toFile.getCanonicalPath.endsWith(iso8601FileName + Constants._Nodes)) + assert(pathsFromWrite._2.toFile.getCanonicalPath.endsWith(iso8601FileName + Constants._Edges)) + fileSnapshotStore.write(oneMillisecondBeforeNow, serviceGraphJson) + fileSnapshotStore.write(twoMillisecondsAfterNow, serviceGraphJson) + } + it("should return None when read() is called with a time that is too early") { + val fileContent = fileSnapshotStore.read(twoMillisecondsBeforeNow) + assert(fileContent === None) + } + it("should read the correct file when read() is called with a later time") { + val fileContent = fileSnapshotStore.read(oneMillisecondAfterNow) + assert(fileContent.get == serviceGraphJson) + } + it("should purge a single file when calling purge() with the timestamp of the oldest file") { + val numberOfFilesPurged = fileSnapshotStore.purge(oneMillisecondBeforeNow) + numberOfFilesPurged shouldEqual 2 + } + it("should purge the two remaining files when calling purge() with the youngest timestamp") { + val numberOfFilesPurged = fileSnapshotStore.purge(twoMillisecondsAfterNow) + numberOfFilesPurged shouldEqual 4 + } + } + it("should create the directory when the directory does not exist") { + val suffix = File.separator + "DirectoryToCreate" + val fileStore = new FileSnapshotStore(directoryName + suffix) + Paths.get(directoryName + suffix).toFile.deleteOnExit() + val pathFromWrite = fileStore.write(now, serviceGraphJson) + assert(pathFromWrite._1.toFile.getCanonicalPath.startsWith(directoryName + suffix)) + assert(pathFromWrite._2.toFile.getCanonicalPath.startsWith(directoryName + suffix)) + val numberOfFilesPurged = fileStore.purge(now) + numberOfFilesPurged shouldEqual 2 + } + } +} diff --git a/service-graph/snapshot-store/src/test/scala/com/expedia/www/haystack/service/graph/snapshot/store/JsonIntoDataFramesTransformerSpec.scala b/service-graph/snapshot-store/src/test/scala/com/expedia/www/haystack/service/graph/snapshot/store/JsonIntoDataFramesTransformerSpec.scala new file mode 100644 index 000000000..c3be16f99 --- /dev/null +++ b/service-graph/snapshot-store/src/test/scala/com/expedia/www/haystack/service/graph/snapshot/store/JsonIntoDataFramesTransformerSpec.scala @@ -0,0 +1,66 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.service.graph.snapshot.store + +import com.expedia.www.haystack.service.graph.snapshot.store.Constants._ +import com.expedia.www.haystack.service.graph.snapshot.store.JsonIntoDataFramesTransformer._ +import org.scalatest.{FunSpec, Matchers} + +class JsonIntoDataFramesTransformerSpec extends FunSpec with Matchers { + private val stringSnapshotStoreSpecBase = new SnapshotStoreSpecBase + + val jsonIntoDataFramesTransformer = new JsonIntoDataFramesTransformer + describe("JsonIntoDataFramesTransformer.parseJson()") { + it("should parse service graph JSON into nodes and edges") { + val serviceGraphJson = stringSnapshotStoreSpecBase.readFile(JsonFileNameWithExtension) + val nodesAndEdges = jsonIntoDataFramesTransformer.parseJson(serviceGraphJson) + nodesAndEdges.nodes shouldEqual stringSnapshotStoreSpecBase.readFile(NodesCsvFileNameWithExtension) + nodesAndEdges.edges shouldEqual stringSnapshotStoreSpecBase.readFile(EdgesCsvFileNameWithExtension) + } + it("should return empty nodes and edges when passed empty JSON") { + val nodesAndEdges = jsonIntoDataFramesTransformer.parseJson( + "{}") + nodesAndEdges.nodes shouldEqual NodesHeader + nodesAndEdges.edges shouldEqual EdgesHeader + } + it("should return empty nodes and edges when passed JSON with an empty list of edges") { + val nodesAndEdges = jsonIntoDataFramesTransformer.parseJson( + "{\"edges\":[]}") + nodesAndEdges.nodes shouldEqual NodesHeader + nodesAndEdges.edges shouldEqual EdgesHeader + } + it("should return and edge with no nodes when passed JSON with unnamed source and destination") { + val nodesAndEdges = jsonIntoDataFramesTransformer.parseJson( + "{\"edges\":[{\"source\":{},\"destination\":{}}]}") + nodesAndEdges.nodes shouldEqual NodesHeader + nodesAndEdges.edges shouldEqual EdgesHeader + "1,,,,,,,\n" + } + it("should gracefully handle JSON with an edge that has only a bare bones source") { + val nodesAndEdges = jsonIntoDataFramesTransformer.parseJson( + "{\"edges\":[{\"source\":{\"name\":\"Name\"}}]}") + nodesAndEdges.nodes shouldEqual NodesHeader + "1,Name,,\n" + nodesAndEdges.edges shouldEqual EdgesHeader + "1,1,,,,,,\n" + } + it("should gracefully handle JSON with an edge that has only a bare bones destination") { + val nodesAndEdges = jsonIntoDataFramesTransformer.parseJson( + "{\"edges\":[{\"destination\":{\"name\":\"Name\"}}]}") + nodesAndEdges.nodes shouldEqual NodesHeader + "1,Name,,\n" + nodesAndEdges.edges shouldEqual EdgesHeader + "1,,1,,,,,\n" + } + } +} diff --git a/service-graph/snapshot-store/src/test/scala/com/expedia/www/haystack/service/graph/snapshot/store/S3SnapshotStoreSpec.scala b/service-graph/snapshot-store/src/test/scala/com/expedia/www/haystack/service/graph/snapshot/store/S3SnapshotStoreSpec.scala new file mode 100644 index 000000000..7062624f0 --- /dev/null +++ b/service-graph/snapshot-store/src/test/scala/com/expedia/www/haystack/service/graph/snapshot/store/S3SnapshotStoreSpec.scala @@ -0,0 +1,247 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.service.graph.snapshot.store + +import java.time.Instant +import java.time.format.DateTimeFormatter.ISO_INSTANT +import java.util + +import com.amazonaws.regions.Regions +import com.amazonaws.services.s3.AmazonS3 +import com.amazonaws.services.s3.AmazonS3ClientBuilder.standard +import com.amazonaws.services.s3.model.{ListObjectsV2Result, S3ObjectSummary} +import com.expedia.www.haystack.service.graph.snapshot.store.Constants.{DotCsv, SlashEdges, SlashNodes} +import com.expedia.www.haystack.service.graph.snapshot.store.S3SnapshotStoreSpec.itemNamesWrittenToS3 +import org.mockito.Matchers._ +import org.mockito.Mockito +import org.mockito.Mockito.{times, verify, verifyNoMoreInteractions, when} +import org.scalatest.{BeforeAndAfterAll, _} +import org.scalatest.mockito.MockitoSugar + +import scala.collection.JavaConverters._ +import scala.collection.{immutable, mutable} + +object S3SnapshotStoreSpec { + private val itemNamesWrittenToS3 = mutable.SortedSet[(String, String)]() +} + +class S3SnapshotStoreSpec extends SnapshotStoreSpecBase with BeforeAndAfterAll with MockitoSugar with Matchers { + // Set to true to run these test in an integration-type way, talking to a real S3. + // You must have valid keys on your machine to do so, typically in ~/.aws/credentials. + private val useRealS3 = false + + private val bucketName = "haystack-service-graph-snapshots" + private val folderName = "unit-test-snapshots" + private val nextContinuationToken = "nextContinuationToken" + private val listObjectsV2Result = mock[ListObjectsV2Result] + private val s3Client = if (useRealS3) standard.withRegion(Regions.US_WEST_2).build else mock[AmazonS3] + private val serviceGraphJson = readFile(Constants.JsonFileNameWithExtension) + private val nodesCsv = readFile(Constants.NodesCsvFileNameWithExtension) + private val edgesCsv = readFile(Constants.EdgesCsvFileNameWithExtension) + + override def afterAll() { + if (useRealS3) { + itemNamesWrittenToS3.foreach(itemName => s3Client.deleteObject(bucketName, itemName._1)) + itemNamesWrittenToS3.foreach(itemName => s3Client.deleteObject(bucketName, itemName._2)) + s3Client.deleteBucket(bucketName) + } + else { + verifyNoMoreInteractionsForAllMocksThenReset() + } + } + + describe("S3SnapshotStore.build()") { + val store = new S3SnapshotStore() + var s3Store = store.build(Array(store.getClass.getCanonicalName, bucketName, folderName, "42")) + .asInstanceOf[S3SnapshotStore] + it("should use the arguments in the default constructor and the array") { + val s3Client: AmazonS3 = s3Store.s3Client + s3Client.getRegion.toString shouldEqual Regions.US_WEST_2.getName + s3Store.bucketName shouldEqual bucketName + s3Store.folderName shouldEqual folderName + s3Store.listObjectsBatchSize shouldEqual 42 + } + it("should use 0 for listObjectsBatchSize if no listObjectsBatchSize is specified in the args array") { + s3Store = store.build(Array(store.getClass.getCanonicalName, bucketName, folderName)) + .asInstanceOf[S3SnapshotStore] + s3Store.listObjectsBatchSize shouldEqual 0 + } + } + + describe("S3SnapshotStore") { + var s3Store = new S3SnapshotStore(s3Client, bucketName, folderName, 3) + it("should create the bucket when the bucket does not exist") { + if (!useRealS3) { + whensForWrite(false) + } + itemNamesWrittenToS3 += s3Store.write(oneMillisecondBeforeNow, serviceGraphJson) + if (!useRealS3) { + verify(s3Client).doesBucketExistV2(bucketName) + verify(s3Client).createBucket(bucketName) + verify(s3Client).putObject(bucketName, createItemName(oneMillisecondBeforeNow) + SlashNodes + DotCsv, nodesCsv) + verify(s3Client).putObject(bucketName, createItemName(oneMillisecondBeforeNow) + SlashEdges + DotCsv, edgesCsv) + verifyNoMoreInteractionsForAllMocksThenReset() + } + } + it("should not create the bucket when the bucket already exists") { + if (!useRealS3) { + whensForWrite(true) + } + itemNamesWrittenToS3 += s3Store.write(oneMillisecondAfterNow, serviceGraphJson) + itemNamesWrittenToS3 += s3Store.write(twoMillisecondsAfterNow, serviceGraphJson) + if (!useRealS3) { + verify(s3Client, times(2)).doesBucketExistV2(bucketName) + verify(s3Client).putObject(bucketName, createItemName(oneMillisecondAfterNow) + SlashNodes + DotCsv, nodesCsv) + verify(s3Client).putObject(bucketName, createItemName(oneMillisecondAfterNow) + SlashEdges + DotCsv, edgesCsv) + verify(s3Client).putObject(bucketName, createItemName(twoMillisecondsAfterNow) + SlashNodes + DotCsv, nodesCsv) + verify(s3Client).putObject(bucketName, createItemName(twoMillisecondsAfterNow) + SlashEdges + DotCsv, edgesCsv) + verifyNoMoreInteractionsForAllMocksThenReset() + } + } + it("should return None when read() is called with a time that is too early") { + if (!useRealS3) { + whensForRead + when(listObjectsV2Result.isTruncated).thenReturn(false) + when(listObjectsV2Result.getObjectSummaries).thenReturn(convertStringToS3ObjectSummary) + } + assert(s3Store.read(twoMillisecondsBeforeNow).isEmpty) + if (!useRealS3) { + verifiesForRead(1) + verifyNoMoreInteractionsForAllMocksThenReset() + } + } + it("should return the correct object when read() is called with a time that is not an exact match but is not too early") { + if (!useRealS3) { + whensForRead + when(s3Client.getObjectAsString(anyString(), anyString())).thenReturn(nodesCsv, edgesCsv) + when(listObjectsV2Result.isTruncated).thenReturn(false) + when(listObjectsV2Result.getObjectSummaries).thenReturn(convertStringToS3ObjectSummary) + } + assert(s3Store.read(now).get == serviceGraphJson) + if (!useRealS3) { + verifiesForRead(1) + verify(s3Client).getObjectAsString(anyString(), + org.mockito.Matchers.eq(createItemName(oneMillisecondBeforeNow) + SlashNodes)) + verify(s3Client).getObjectAsString(anyString(), + org.mockito.Matchers.eq(createItemName(oneMillisecondBeforeNow) + SlashEdges)) + verifyNoMoreInteractionsForAllMocksThenReset() + } + } + it("should return the correct object when read() is called with a time that is an exact match") { + if (!useRealS3) { + whensForRead + when(s3Client.getObjectAsString(anyString(), anyString())).thenReturn(nodesCsv, edgesCsv) + when(listObjectsV2Result.isTruncated).thenReturn(false) + when(listObjectsV2Result.getObjectSummaries).thenReturn(convertStringToS3ObjectSummary) + } + val actual = s3Store.read(twoMillisecondsAfterNow).get + val expected = serviceGraphJson + assert(actual == expected) + if (!useRealS3) { + verifiesForRead(1) + verify(s3Client).getObjectAsString(anyString(), + org.mockito.Matchers.eq(createItemName(twoMillisecondsAfterNow) + SlashNodes)) + verify(s3Client).getObjectAsString(anyString(), + org.mockito.Matchers.eq(createItemName(twoMillisecondsAfterNow) + SlashEdges)) + verifyNoMoreInteractionsForAllMocksThenReset() + } + } + it("should return the correct object for small batches") { + s3Store = new S3SnapshotStore(s3Client, bucketName, folderName, 1) + if (!useRealS3) { + whensForRead + when(s3Client.getObjectAsString(anyString(), anyString())).thenReturn(nodesCsv, edgesCsv) + when(listObjectsV2Result.isTruncated).thenReturn(true, true, false) + val it = itemNamesWrittenToS3.iterator + when(listObjectsV2Result.getObjectSummaries) + .thenReturn( + convertTupleToObjectSummary(it.next()).asJava, + convertTupleToObjectSummary(it.next()).asJava, + convertTupleToObjectSummary(it.next()).asJava) + } + assert(s3Store.read(twoMillisecondsAfterNow).get == serviceGraphJson) + if (!useRealS3) { + verifiesForRead(3) + verify(s3Client).getObjectAsString(anyString(), + org.mockito.Matchers.eq(createItemName(twoMillisecondsAfterNow) + SlashNodes)) + verify(s3Client).getObjectAsString(anyString(), + org.mockito.Matchers.eq(createItemName(twoMillisecondsAfterNow) + SlashEdges)) + verifyNoMoreInteractionsForAllMocksThenReset() + } + } + it("should never delete any items when purge() is called") { + s3Store.purge(twoMillisecondsAfterNow) shouldEqual 0 + if (!useRealS3) { + verifyNoMoreInteractionsForAllMocksThenReset() + } + } + it("should throw an IllegalArgumentException when read() is called with a 0 value of listObjectsBatchSize") { + s3Store = new S3SnapshotStore(s3Client, bucketName, folderName, 0) + an [IllegalArgumentException] should be thrownBy s3Store.read(twoMillisecondsBeforeNow) + if (!useRealS3) { + verifyNoMoreInteractionsForAllMocksThenReset() + } + } + } + + private def convertTupleToObjectSummary(tuple: (String, String)): immutable.Seq[S3ObjectSummary] = { + val s3ObjectSummary1 = new S3ObjectSummary + s3ObjectSummary1.setBucketName(bucketName) + s3ObjectSummary1.setKey(tuple._1) + val s3ObjectSummary2 = new S3ObjectSummary + s3ObjectSummary2.setBucketName(bucketName) + s3ObjectSummary2.setKey(tuple._2) + List(s3ObjectSummary1, s3ObjectSummary2) + } + + private def convertStringToS3ObjectSummary: util.List[S3ObjectSummary] = { + val listBuilder = List.newBuilder[S3ObjectSummary] + for (tuple <- itemNamesWrittenToS3) { + val list: immutable.Seq[S3ObjectSummary] = convertTupleToObjectSummary(tuple) + listBuilder += list.head + listBuilder += list(1) + } + listBuilder.result().asJava + } + + private def verifiesForRead(loopTimes: Int) = { + verify(s3Client, times(loopTimes)).listObjectsV2(bucketName) + verify(listObjectsV2Result, times(loopTimes)).getObjectSummaries + verify(listObjectsV2Result, times(loopTimes)).getNextContinuationToken + verify(listObjectsV2Result, times(loopTimes)).isTruncated + } + + private def whensForRead = { + when(listObjectsV2Result.getNextContinuationToken).thenReturn(nextContinuationToken) + when(s3Client.listObjectsV2(anyString())).thenReturn(listObjectsV2Result) + } + + private def whensForWrite(doesBucketExist: Boolean) = { + when(s3Client.doesBucketExistV2(anyString())).thenReturn(doesBucketExist) + when(listObjectsV2Result.getNextContinuationToken).thenReturn(nextContinuationToken) + } + + private def verifyNoMoreInteractionsForAllMocksThenReset(): Unit = { + verifyNoMoreInteractions(s3Client, listObjectsV2Result) + Mockito.reset(s3Client, listObjectsV2Result) + } + + private def createItemName(thisInstant: Instant) = { + folderName + "/" + ISO_INSTANT.format(thisInstant) + } +} \ No newline at end of file diff --git a/service-graph/snapshot-store/src/test/scala/com/expedia/www/haystack/service/graph/snapshot/store/SnapshotStoreSpec.scala b/service-graph/snapshot-store/src/test/scala/com/expedia/www/haystack/service/graph/snapshot/store/SnapshotStoreSpec.scala new file mode 100644 index 000000000..1d4172830 --- /dev/null +++ b/service-graph/snapshot-store/src/test/scala/com/expedia/www/haystack/service/graph/snapshot/store/SnapshotStoreSpec.scala @@ -0,0 +1,48 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.service.graph.snapshot.store +import java.time.Instant + +class SnapshotStoreSpec extends SnapshotStoreSpecBase { + private val snapshotStore = new SnapshotStore { + override def write(instant: Instant, content: String): AnyRef = { + None + } + + override def read(instant: Instant): Option[String] = { + None + } + + override def purge(instant: Instant): Integer = { + 0 + } + + override def build(constructorArguments: Array[String]): SnapshotStore = { + this + } + } + + describe("SnapshotStore") { + it("should create the correct ISO 8601 file name") { + snapshotStore.createIso8601FileName(Instant.EPOCH) shouldEqual "1970-01-01T00:00:00.000Z" + snapshotStore.createIso8601FileName(Instant.EPOCH.plusMillis(1)) shouldEqual "1970-01-01T00:00:00.001Z" + snapshotStore.createIso8601FileName(Instant.EPOCH.plusMillis(-1)) shouldEqual "1969-12-31T23:59:59.999Z" + } + } + +} diff --git a/service-graph/snapshot-store/src/test/scala/com/expedia/www/haystack/service/graph/snapshot/store/SnapshotStoreSpecBase.scala b/service-graph/snapshot-store/src/test/scala/com/expedia/www/haystack/service/graph/snapshot/store/SnapshotStoreSpecBase.scala new file mode 100644 index 000000000..b912b1fab --- /dev/null +++ b/service-graph/snapshot-store/src/test/scala/com/expedia/www/haystack/service/graph/snapshot/store/SnapshotStoreSpecBase.scala @@ -0,0 +1,46 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.service.graph.snapshot.store + +import java.time.Instant +import java.time.temporal.ChronoUnit + +import org.scalatest.{FunSpec, Matchers} + +import scala.io.{BufferedSource, Codec, Source} + +class SnapshotStoreSpecBase extends FunSpec with Matchers { + protected val now: Instant = Instant.EPOCH + + protected val twoMillisecondsBeforeNow: Instant = now.minus(2, ChronoUnit.MILLIS) + + protected val oneMillisecondBeforeNow: Instant = now.minus(1, ChronoUnit.MILLIS) + + protected val oneMillisecondAfterNow: Instant = now.plus(1, ChronoUnit.MILLIS) + + protected val twoMillisecondsAfterNow: Instant = now.plus(2, ChronoUnit.MILLIS) + + def readFile(fileName: String): String = { + implicit val codec: Codec = Codec.UTF8 + lazy val bufferedSource: BufferedSource = Source.fromResource(fileName) + val fileContents = bufferedSource.getLines.mkString("\n") + bufferedSource.close() + fileContents + "\n" + } + +} diff --git a/service-graph/snapshotter/Makefile b/service-graph/snapshotter/Makefile new file mode 100644 index 000000000..8a034ac22 --- /dev/null +++ b/service-graph/snapshotter/Makefile @@ -0,0 +1,11 @@ +.PHONY: release + +export DOCKER_ORG := expediadotcom +export DOCKER_IMAGE_NAME := haystack-service-graph-snapshotter +PWD := $(shell pwd) + +docker-image: + docker build -t $(DOCKER_IMAGE_NAME) -f build/docker/Dockerfile . + +release: docker-image + ../deployment/scripts/publish-to-docker-hub.sh diff --git a/service-graph/snapshotter/README.md b/service-graph/snapshotter/README.md new file mode 100644 index 000000000..c79025a65 --- /dev/null +++ b/service-graph/snapshotter/README.md @@ -0,0 +1,27 @@ +#Haystack : snapshotter + +The "snapshot" feature of the service graph is a Scala "main" application that runs the specified +[snapshot-store](https://github.com/ExpediaDotCom/haystack-service-graph). The +[Scala Main class](https://github.com/ExpediaDotCom/haystack-service-graph/blob/master/snapshotter/src/main/scala/com/expedia/www/haystack/service/graph/snapshotter/Main.scala) +expects as its first argument the fully qualified class name of the snapshot store to use. More precisely: + +1. The first parameter is the fully qualified class name of the implementation of the snapshot store to run. There are +are currently two implementations: + * [com.expedia.www.haystack.service.graph.snapshot.store.FileSnapshotStore](https://github.com/ExpediaDotCom/haystack-service-graph/blob/master/snapshot-store/src/main/scala/com.expedia.www.haystack.service.graph.snapshot.store.FileSnapshotStore) + * [com.expedia.www.haystack.service.graph.snapshot.store.S3SnapshotStore](https://github.com/ExpediaDotCom/haystack-service-graph/blob/master/snapshot-store/src/main/scala/com.expedia.www.haystack.service.graph.snapshot.store.S3SnapshotStore) +2. The rest of the arguments are passed to the constructor of the class specified by args(0). + * For FileSnapshotStore, the only additional argument required is the directory name where the snapshots will be stored, + e.g. /var/snapshots + * For S3SnapshotStore, there are three additional arguments, which are in order: + * the bucket name + * the folder name inside the bucket + * the number of items to fetch at one time when calling the S3 listObjectsV2 API; the best value to choose, + assuming sufficient memory on the JVM running the snapshotter, is the maximum number of snapshots that will exist + in S3 before being purged. For example, with a one hour snapshot interval and a snapshot TTL of 1 year, + 366 * 24 = 8784 would be a good value (perhaps rounded to 10,000). + +## Building + +``` +mvn clean package +``` \ No newline at end of file diff --git a/service-graph/snapshotter/build/docker/Dockerfile b/service-graph/snapshotter/build/docker/Dockerfile new file mode 100644 index 000000000..fad2dd585 --- /dev/null +++ b/service-graph/snapshotter/build/docker/Dockerfile @@ -0,0 +1,25 @@ +FROM openjdk:8-jre +MAINTAINER Haystack + +ENV APP_NAME haystack-service-graph-snapshotter +ENV APP_HOME /app/bin +ENV JMXTRANS_AGENT jmxtrans-agent-1.2.6 +ENV DOCKERIZE_VERSION v0.6.1 + +ADD https://github.com/jwilder/dockerize/releases/download/${DOCKERIZE_VERSION}/dockerize-alpine-linux-amd64-${DOCKERIZE_VERSION}.tar.gz dockerize.tar.gz +RUN tar xzf dockerize.tar.gz +RUN chmod +x dockerize + +RUN mkdir -p ${APP_HOME} +RUN chmod a+w /app + +COPY target/${APP_NAME}.jar ${APP_HOME}/ +COPY build/docker/start-app.sh ${APP_HOME}/ +COPY build/docker/jmxtrans-agent.xml ${APP_HOME}/ + +RUN chmod +x ${APP_HOME}/start-app.sh +ADD https://github.com/jmxtrans/jmxtrans-agent/releases/download/${JMXTRANS_AGENT}/${JMXTRANS_AGENT}.jar ${APP_HOME}/ + +WORKDIR ${APP_HOME} + +ENTRYPOINT ["./start-app.sh"] diff --git a/service-graph/snapshotter/build/docker/jmxtrans-agent.xml b/service-graph/snapshotter/build/docker/jmxtrans-agent.xml new file mode 100644 index 000000000..cea1c8b91 --- /dev/null +++ b/service-graph/snapshotter/build/docker/jmxtrans-agent.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + ${HAYSTACK_GRAPHITE_HOST:monitoring-influxdb-graphite.kube-system.svc} + ${HAYSTACK_GRAPHITE_PORT:2003} + ${HAYSTACK_GRAPHITE_ENABLED:false} + + haystack.service-graph.snapshotter.#hostname#. + + 60 + diff --git a/service-graph/snapshotter/build/docker/start-app.sh b/service-graph/snapshotter/build/docker/start-app.sh new file mode 100755 index 000000000..11f44788c --- /dev/null +++ b/service-graph/snapshotter/build/docker/start-app.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +[ -z "$JAVA_XMS" ] && JAVA_XMS=512m +[ -z "$JAVA_XMX" ] && JAVA_XMX=512m + +set -e +JAVA_OPTS="${JAVA_OPTS} \ +-javaagent:${APP_HOME}/${JMXTRANS_AGENT}.jar=${APP_HOME}/jmxtrans-agent.xml \ +-XX:+UseG1GC \ +-Xmx${JAVA_XMX} \ +-Xms${JAVA_XMS} \ +-Dapplication.name=${APP_NAME} \ +-Dapplication.home=${APP_HOME}" + +if [[ -n "$SERVICE_DEBUG_ON" ]] && [[ "$SERVICE_DEBUG_ON" == true ]]; then + JAVA_OPTS="$JAVA_OPTS -Xdebug -Xrunjdwp:transport=dt_socket,address=5005,server=y" +fi + +exec java ${JAVA_OPTS} -jar "${APP_HOME}/${APP_NAME}.jar" "$@" diff --git a/service-graph/snapshotter/pom.xml b/service-graph/snapshotter/pom.xml new file mode 100644 index 000000000..8a14f6bcb --- /dev/null +++ b/service-graph/snapshotter/pom.xml @@ -0,0 +1,135 @@ + + + + + haystack-service-graph + com.expedia.www + 1.0.15-SNAPSHOT + + + 4.0.0 + haystack-service-graph-snapshotter + jar + + + + + Apache License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + ${project.artifactId}-${project.version} + com.expedia.www.haystack.service.graph.snapshotter.Main + + + + + com.expedia.www + haystack-logback-metrics-appender + + + com.expedia.www + haystack-service-graph-snapshot-store + + + org.apache.commons + commons-lang3 + + + org.mockito + mockito-all + + + org.scalaj + scalaj-http_${scala.major.minor.version} + + + + + ${finalName} + + + org.scalatest + scalatest-maven-plugin + + + test + + test + + + org.expedia.www.haystack.commons.scalatest.IntegrationSuite + + + + + + + org.scoverage + scoverage-maven-plugin + + true + 100 + + + + + net.alchim31.maven + scala-maven-plugin + + + + org.scalastyle + scalastyle-maven-plugin + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + org.apache.maven.plugins + maven-shade-plugin + ${maven-shade-plugin-version} + + + package + + shade + + + + + ${mainClass} + + + + + + + + + diff --git a/service-graph/snapshotter/src/main/resources/app.conf b/service-graph/snapshotter/src/main/resources/app.conf new file mode 100644 index 000000000..73b52f100 --- /dev/null +++ b/service-graph/snapshotter/src/main/resources/app.conf @@ -0,0 +1,12 @@ +snapshotter { + # 1 year in milliseconds: 365.2425 days/year + # * 24 hours/day + # * 60 minutes/hour + # * 60 seconds/second + # * 1000 milliseconds/second + # = 31,556,952,000 + purge.age.ms = 315569520000 + + # Determines the "from" parameter in the call to retrieve the service graph: 3,600,000 milliseconds = 1 hour + window.size.ms = 3600000 +} \ No newline at end of file diff --git a/service-graph/snapshotter/src/main/resources/logback.xml b/service-graph/snapshotter/src/main/resources/logback.xml new file mode 100644 index 000000000..a54f534c6 --- /dev/null +++ b/service-graph/snapshotter/src/main/resources/logback.xml @@ -0,0 +1,21 @@ + + + + + + + true + + + + + + %d{yyyy-MM-dd HH:mm:ss:SSS} %thread, %level, %logger{70}, "%msg" %replace(%ex){'[\n]+', '\\n'}%nopex%n + + + + + + + + diff --git a/service-graph/snapshotter/src/main/scala/com/expedia/www/haystack/service/graph/snapshotter/AppConfiguration.scala b/service-graph/snapshotter/src/main/scala/com/expedia/www/haystack/service/graph/snapshotter/AppConfiguration.scala new file mode 100644 index 000000000..57806b1a5 --- /dev/null +++ b/service-graph/snapshotter/src/main/scala/com/expedia/www/haystack/service/graph/snapshotter/AppConfiguration.scala @@ -0,0 +1,42 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.service.graph.snapshotter + +import com.expedia.www.haystack.commons.config.ConfigurationLoader +import org.apache.commons.lang3.StringUtils + +/** + * This class reads the configuration from the given resource name + * + * @param resourceName name of the resource file to load + */ +class AppConfiguration(resourceName: String) { + + require(StringUtils.isNotBlank(resourceName)) + + private val config = ConfigurationLoader.loadConfigFileWithEnvOverrides(resourceName = this.resourceName) + + /** + * Default constructor that loads configuration from the resource named "app.conf" + */ + def this() = this("app.conf") + + val purgeAgeMs: Long = config.getLong("snapshotter.purge.age.ms") + + val windowSizeMs: Long = config.getLong("snapshotter.window.size.ms") +} diff --git a/service-graph/snapshotter/src/main/scala/com/expedia/www/haystack/service/graph/snapshotter/Main.scala b/service-graph/snapshotter/src/main/scala/com/expedia/www/haystack/service/graph/snapshotter/Main.scala new file mode 100644 index 000000000..9d21a8986 --- /dev/null +++ b/service-graph/snapshotter/src/main/scala/com/expedia/www/haystack/service/graph/snapshotter/Main.scala @@ -0,0 +1,115 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.service.graph.snapshotter + +import java.time.{Clock, Instant} + +import com.expedia.www.haystack.service.graph.snapshot.store.SnapshotStore +import org.slf4j.{Logger, LoggerFactory} +import scalaj.http.{Http, HttpRequest} + +object Main { + val ServiceGraphUrlRequiredMsg = + "The first argument must specify the service graph URL" + val StringStoreClassRequiredMsg = + "The second argument must specify the fully qualified class name of a class that implements SnapshotStore" + val UrlBaseRequiredMsg = + "The third argument must specify the base of the service graph URL" + val ServiceGraphUrlSuffix: String = "?from=%d" + val appConfiguration = new AppConfiguration() + + var logger: Logger = LoggerFactory.getLogger(Main.getClass) + var clock: Clock = Clock.systemUTC() + var factory: Factory = new Factory + + /** Main method + * @param args specifies the class to run and its parameters. + * ==args(0)== + * The first parameter is the fully qualified class name of the implementation of + * [[com.expedia.www.haystack.service.graph.snapshot.store.SnapshotStore]] to run. + * There are currently two implementations: + * - [[com.expedia.www.haystack.service.graph.snapshot.store.FileSnapshotStore]] + * - [[com.expedia.www.haystack.service.graph.snapshot.store.S3SnapshotStore]] + * ==args(1+)== + * The rest of the arguments are passed to the constructor of the class specified by args(0). + * See the documentation in the build() method of the desired implementation for argument details. + * ===Examples=== + * ====FileSnapshotStore==== + * To run FileSnapshotStore and use /var/snapshots for snapshot storage, the arguments would be: + * - com.expedia.www.haystack.service.graph.snapshot.store.FileSnapshotStore + * - /var/snapshots + * ====S3SnapshotStore==== + * To run S3SnapshotStore and use the "Haystack" bucket with subfolder "snapshots" for snapshot storage, and a batch + * size of 10,000 when calling the S3 "listObjectsV2" API, the arguments would be: + * - com.expedia.www.haystack.service.graph.snapshot.store.S3SnapshotStore + * - Haystack + * - snapshots + * - 10000 + */ + def main(args: Array[String]): Unit = { + if (args.length == 0) { + logger.error(ServiceGraphUrlRequiredMsg) + } else if (args.length == 1) { + logger.error(StringStoreClassRequiredMsg) + } else if (args.length == 2) { + logger.error(UrlBaseRequiredMsg) + } else { + val snapshotStore = instantiateSnapshotStore(args) + val now = clock.instant() + val json = getCurrentServiceGraph(args(0) + ServiceGraphUrlSuffix, now) + storeServiceGraphInTheStringStore(snapshotStore, now, json) + purgeOldSnapshots(snapshotStore, now) + } + } + + private def instantiateSnapshotStore(args: Array[String]): SnapshotStore = { + def createStringStoreInstanceWithDefaultConstructor: SnapshotStore = { + val fullyQualifiedClassName = args(1) + val klass = Class.forName(fullyQualifiedClassName) + val instanceBuiltByDefaultConstructor = klass.newInstance().asInstanceOf[SnapshotStore] + instanceBuiltByDefaultConstructor + } + + val snapshotStore = createStringStoreInstanceWithDefaultConstructor.build(args.drop(1)) + snapshotStore + } + + private def getCurrentServiceGraph(url: String, instant: Instant) = { + val request = factory.createHttpRequest(url, instant.toEpochMilli - appConfiguration.windowSizeMs) + val httpResponse = request.asString + httpResponse.body + } + + private def storeServiceGraphInTheStringStore(snapshotStore: SnapshotStore, + instant: Instant, + json: String): AnyRef = { + snapshotStore.write(instant, json) + } + + private def purgeOldSnapshots(snapshotStore: SnapshotStore, + instant: Instant): Integer = { + snapshotStore.purge(instant.minusMillis(appConfiguration.purgeAgeMs)) + } +} + +class Factory { + def createHttpRequest(url: String, windowSizeMs: Long): HttpRequest = { + val urlWithParameter = url.format(windowSizeMs) + Http(urlWithParameter) + } +} diff --git a/service-graph/snapshotter/src/test/resources/app.conf b/service-graph/snapshotter/src/test/resources/app.conf new file mode 100644 index 000000000..3a7f2f1cf --- /dev/null +++ b/service-graph/snapshotter/src/test/resources/app.conf @@ -0,0 +1,7 @@ +snapshotter { + # Setting purge age to 0 lets a unit test in MainSpec verify that purge is called when running Main.main() + purge.age.ms = 0 + + # 3,600,000 milliseconds = 1 hour + window.size.ms = 3600000 +} \ No newline at end of file diff --git a/service-graph/snapshotter/src/test/resources/serviceGraph.json b/service-graph/snapshotter/src/test/resources/serviceGraph.json new file mode 100644 index 000000000..7f03f0708 --- /dev/null +++ b/service-graph/snapshotter/src/test/resources/serviceGraph.json @@ -0,0 +1,2547 @@ +{ + "edges": [ + { + "source": { + "name": "detail-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws,dc" + } + }, + "destination": { + "name": "provideradapter-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 202931, + "lastSeen": 1544575410111, + "errorCount": 0 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587937 + }, + { + "source": { + "name": "daily-data-update-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "dc" + } + }, + "destination": { + "name": "async-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 5136140, + "lastSeen": 1544575571142, + "errorCount": 0 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587796 + }, + { + "source": { + "name": "search-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "dc" + } + }, + "destination": { + "name": "provideradapter-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 134125, + "lastSeen": 1544575498167, + "errorCount": 21882 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587999 + }, + { + "source": { + "name": "internet-proxy", + "tags": {} + }, + "destination": { + "name": "front-door-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws,dc" + } + }, + "stats": { + "count": 17367200, + "lastSeen": 1544575567920, + "errorCount": 103264 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575588026 + }, + { + "source": { + "name": "westeros-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "stark-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 157201, + "lastSeen": 1544575421793, + "errorCount": 36 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575588024 + }, + { + "source": { + "name": "internet-proxy", + "tags": {} + }, + "destination": { + "name": "rails-web", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 130, + "lastSeen": 1544573988361, + "errorCount": 0 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544574600000 + }, + { + "source": { + "name": "shopping-pricing", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "new-shopping-pricing", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 3531583, + "lastSeen": 1544575569804, + "errorCount": 0 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587705 + }, + { + "source": { + "name": "authentication-service", + "tags": {} + }, + "destination": { + "name": "userinteraction-web", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 76788, + "lastSeen": 1544575553640, + "errorCount": 0 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587815 + }, + { + "source": { + "name": "stark-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "context-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 180327, + "lastSeen": 1544575421874, + "errorCount": 2088 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587693 + }, + { + "source": { + "name": "stark-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "template-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 169216, + "lastSeen": 1544575421889, + "errorCount": 1 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575588068 + }, + { + "source": { + "name": "shopping-search-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "westeros-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 22890079, + "lastSeen": 1544575572251, + "errorCount": 0 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587672 + }, + { + "source": { + "name": "api-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "shopping-content-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 2, + "lastSeen": 1544554416805, + "errorCount": 0 + }, + "effectiveFrom": 1544553000000, + "effectiveTo": 1544554800000 + }, + { + "source": { + "name": "detail-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "dc" + } + }, + "destination": { + "name": "chargeback-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 60, + "lastSeen": 1544574583227, + "errorCount": 0 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544574600000 + }, + { + "source": { + "name": "detail-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws,dc" + } + }, + "destination": { + "name": "adapter-aws", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 303022, + "lastSeen": 1544575592096, + "errorCount": 1786 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587954 + }, + { + "source": { + "name": "new-shopping-pricing", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "multishop", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 3893347, + "lastSeen": 1544575569844, + "errorCount": 2 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587734 + }, + { + "source": { + "name": "westeros-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "guide-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 1442998, + "lastSeen": 1544575571677, + "errorCount": 61 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587753 + }, + { + "source": { + "name": "search-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws,dc" + } + }, + "destination": { + "name": "help-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 2826475, + "lastSeen": 1544575555819, + "errorCount": 1624211 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587681 + }, + { + "source": { + "name": "authentication-service", + "tags": {} + }, + "destination": { + "name": "front-door-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "dc" + } + }, + "stats": { + "count": 99413, + "lastSeen": 1544575499415, + "errorCount": 0 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587706 + }, + { + "source": { + "name": "boss-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "dc" + } + }, + "destination": { + "name": "provideradapter-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 3258, + "lastSeen": 1544575229219, + "errorCount": 800 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587979 + }, + { + "source": { + "name": "payment-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "fx", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 233615, + "lastSeen": 1544575277983, + "errorCount": 16 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587689 + }, + { + "source": { + "name": "api-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "location-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 196315, + "lastSeen": 1544575522183, + "errorCount": 4 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587866 + }, + { + "source": { + "name": "boss-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "booking-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 3, + "lastSeen": 1544559964099, + "errorCount": 1 + }, + "effectiveFrom": 1544558400000, + "effectiveTo": 1544560200000 + }, + { + "source": { + "name": "boss-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "dc" + } + }, + "destination": { + "name": "chargeback-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 169, + "lastSeen": 1544574617075, + "errorCount": 38 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575588010 + }, + { + "source": { + "name": "front-door-service", + "tags": { + "tier": "1,2", + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws,dc" + } + }, + "destination": { + "name": "checkout-payment-domain-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "dc" + } + }, + "stats": { + "count": 342981, + "lastSeen": 1544575571750, + "errorCount": 126 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587681 + }, + { + "source": { + "name": "forge-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "context-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 5042343, + "lastSeen": 1544575571591, + "errorCount": 405 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587826 + }, + { + "source": { + "name": "front-door-service", + "tags": { + "tier": "3,1,2", + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws,dc" + } + }, + "destination": { + "name": "location-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws,dc" + } + }, + "stats": { + "count": 21474934, + "lastSeen": 1544575571886, + "errorCount": 26012 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575588099 + }, + { + "source": { + "name": "internet-proxy", + "tags": {} + }, + "destination": { + "name": "his-web", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 698168, + "lastSeen": 1544575554236, + "errorCount": 87771 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587677 + }, + { + "source": { + "name": "internet-proxy", + "tags": {} + }, + "destination": { + "name": "hers-web", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 2907715, + "lastSeen": 1544575570922, + "errorCount": 33645 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587793 + }, + { + "source": { + "name": "front-door-service", + "tags": { + "tier": "1", + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "new-shopping-pricing", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 116, + "lastSeen": 1544574157710, + "errorCount": 0 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544574600000 + }, + { + "source": { + "name": "internet-proxy", + "tags": {} + }, + "destination": { + "name": "lpt-web", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 1821807, + "lastSeen": 1544575567232, + "errorCount": 6746 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575588011 + }, + { + "source": { + "name": "satellite", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "endurance-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 12111, + "lastSeen": 1544575506385, + "errorCount": 140 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575588024 + }, + { + "source": { + "name": "front-door-service", + "tags": { + "tier": "1", + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws,dc" + } + }, + "destination": { + "name": "detail-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws,dc" + } + }, + "stats": { + "count": 570805, + "lastSeen": 1544575504575, + "errorCount": 32 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587811 + }, + { + "source": { + "name": "internet-proxy", + "tags": {} + }, + "destination": { + "name": "search-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "dc" + } + }, + "stats": { + "count": 74, + "lastSeen": 1544573552180, + "errorCount": 0 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544574600000 + }, + { + "source": { + "name": "loom-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "lists-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 18356, + "lastSeen": 1544575525038, + "errorCount": 0 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575588011 + }, + { + "source": { + "name": "front-door-service", + "tags": { + "tier": "2", + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "dc" + } + }, + "destination": { + "name": "mars", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 73614, + "lastSeen": 1544575526411, + "errorCount": 0 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587848 + }, + { + "source": { + "name": "mormont-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "seo-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 596144, + "lastSeen": 1544575525706, + "errorCount": 3 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587937 + }, + { + "source": { + "name": "info-site-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "shopping-cart", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 6, + "lastSeen": 1544572410049, + "errorCount": 0 + }, + "effectiveFrom": 1544500800000, + "effectiveTo": 1544572800000 + }, + { + "source": { + "name": "shopping-detail-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "shopping-pricing", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 2838890, + "lastSeen": 1544575569418, + "errorCount": 0 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587682 + }, + { + "source": { + "name": "loom-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "api-customer", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 8583, + "lastSeen": 1544575208652, + "errorCount": 0 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587796 + }, + { + "source": { + "name": "multishop", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "pricing-engine", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 15560819, + "lastSeen": 1544575569839, + "errorCount": 35 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587689 + }, + { + "source": { + "name": "westeros-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "forge-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 3161466, + "lastSeen": 1544575571588, + "errorCount": 4817 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587639 + }, + { + "source": { + "name": "front-door-service", + "tags": { + "tier": "2", + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws,dc" + } + }, + "destination": { + "name": "third-party-provider-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 9735, + "lastSeen": 1544575279207, + "errorCount": 3 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587827 + }, + { + "source": { + "name": "internet-proxy", + "tags": {} + }, + "destination": { + "name": "insurance-shopping-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 410, + "lastSeen": 1544574748096, + "errorCount": 0 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587918 + }, + { + "source": { + "name": "shopping-detail-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "shopping-user-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 1214950, + "lastSeen": 1544575528611, + "errorCount": 0 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587723 + }, + { + "source": { + "name": "ticket-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "boss-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "dc" + } + }, + "stats": { + "count": 20845, + "lastSeen": 1544575478589, + "errorCount": 68 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587644 + }, + { + "source": { + "name": "internet-proxy", + "tags": {} + }, + "destination": { + "name": "third-party-api-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 5000, + "lastSeen": 1544574936681, + "errorCount": 0 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587722 + }, + { + "source": { + "name": "internet-proxy", + "tags": {} + }, + "destination": { + "name": "progressive-webapp-api", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 687220, + "lastSeen": 1544575524183, + "errorCount": 20 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587866 + }, + { + "source": { + "name": "internet-proxy", + "tags": {} + }, + "destination": { + "name": "compositor-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 243059, + "lastSeen": 1544575396324, + "errorCount": 22 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575588026 + }, + { + "source": { + "name": "westeros-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "mormont-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 652939, + "lastSeen": 1544575525612, + "errorCount": 68 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587815 + }, + { + "source": { + "name": "boss-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "dc" + } + }, + "destination": { + "name": "ticket-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 27317, + "lastSeen": 1544575505207, + "errorCount": 0 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575588010 + }, + { + "source": { + "name": "internet-proxy", + "tags": {} + }, + "destination": { + "name": "userinteraction-web", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 2907462, + "lastSeen": 1544575570897, + "errorCount": 10681 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587808 + }, + { + "source": { + "name": "internet-proxy", + "tags": {} + }, + "destination": { + "name": "info-site-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 84704, + "lastSeen": 1544575480474, + "errorCount": 0 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587827 + }, + { + "source": { + "name": "boss-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "dc" + } + }, + "destination": { + "name": "melisandre-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 125064, + "lastSeen": 1544575501716, + "errorCount": 2985 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575588087 + }, + { + "source": { + "name": "progressive-webapp-api", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "shopping-detail-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 2469688, + "lastSeen": 1544575569284, + "errorCount": 0 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587909 + }, + { + "source": { + "name": "guide-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "template-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 806, + "lastSeen": 1544573623862, + "errorCount": 0 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544574600000 + }, + { + "source": { + "name": "api-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "shopping-detail-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 8, + "lastSeen": 1544569762201, + "errorCount": 0 + }, + "effectiveFrom": 1544491800000, + "effectiveTo": 1544571000000 + }, + { + "source": { + "name": "search-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws,dc" + } + }, + "destination": { + "name": "controller-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 20783217, + "lastSeen": 1544575572498, + "errorCount": 246 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587641 + }, + { + "source": { + "name": "progressive-webapp-api", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "shopping-search-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 973286, + "lastSeen": 1544575570804, + "errorCount": 0 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575588061 + }, + { + "source": { + "name": "endurance-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "location-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 18729, + "lastSeen": 1544575266335, + "errorCount": 16 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587723 + }, + { + "source": { + "name": "help-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "adapter-aws", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 379955, + "lastSeen": 1544575554493, + "errorCount": 0 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587808 + }, + { + "source": { + "name": "internet-proxy", + "tags": {} + }, + "destination": { + "name": "targaryen-web", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 8861, + "lastSeen": 1544575485661, + "errorCount": 324 + }, + "effectiveFrom": 1544500800000, + "effectiveTo": 1544575587634 + }, + { + "source": { + "name": "guide-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "seo-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 654, + "lastSeen": 1544573560757, + "errorCount": 0 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544574600000 + }, + { + "source": { + "name": "internet-proxy", + "tags": {} + }, + "destination": { + "name": "drogo-api", + "tags": {} + }, + "stats": { + "count": 11154, + "lastSeen": 1544575207980, + "errorCount": 11 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587669 + }, + { + "source": { + "name": "payment-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "lannister-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 13974992, + "lastSeen": 1544575556069, + "errorCount": 9 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587798 + }, + { + "source": { + "name": "info-site-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "detail-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws,dc" + } + }, + "stats": { + "count": 234369, + "lastSeen": 1544575570813, + "errorCount": 6024 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587952 + }, + { + "source": { + "name": "shopping-search-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "shopping-content-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 19029006, + "lastSeen": 1544575571645, + "errorCount": 0 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587677 + }, + { + "source": { + "name": "new-shopping-pricing", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "fx", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 304458, + "lastSeen": 1544575397477, + "errorCount": 5 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587674 + }, + { + "source": { + "name": "internet-proxy", + "tags": {} + }, + "destination": { + "name": "loom-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 90053, + "lastSeen": 1544575481890, + "errorCount": 7 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587788 + }, + { + "source": { + "name": "new-shopping-pricing", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "payment-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 20680712, + "lastSeen": 1544575556069, + "errorCount": 32 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587782 + }, + { + "source": { + "name": "api-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "shopping-search-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 56878, + "lastSeen": 1544575522176, + "errorCount": 0 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587882 + }, + { + "source": { + "name": "search-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws,dc" + } + }, + "destination": { + "name": "margaery-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 14646996, + "lastSeen": 1544575571980, + "errorCount": 145 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587689 + }, + { + "source": { + "name": "loom-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "api-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 985553, + "lastSeen": 1544575571604, + "errorCount": 0 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575588060 + }, + { + "source": { + "name": "tyrion-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "chargeback-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 176, + "lastSeen": 1544573823589, + "errorCount": 0 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544574600000 + }, + { + "source": { + "name": "help-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "rules-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 237982, + "lastSeen": 1544575571933, + "errorCount": 0 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587954 + }, + { + "source": { + "name": "shopping-search-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "shopping-pricing", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 33794745, + "lastSeen": 1544575571645, + "errorCount": 0 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587875 + }, + { + "source": { + "name": "front-door-service", + "tags": { + "tier": "2", + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "dc" + } + }, + "destination": { + "name": "bolton-service", + "tags": {} + }, + "stats": { + "count": 319715, + "lastSeen": 1544575500355, + "errorCount": 1004 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587658 + }, + { + "source": { + "name": "ticket-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "melisandre-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 13072, + "lastSeen": 1544575568510, + "errorCount": 59 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587716 + }, + { + "source": { + "name": "help-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "provideradapter-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 1403232, + "lastSeen": 1544575555851, + "errorCount": 0 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587827 + }, + { + "source": { + "name": "tyrion-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "provideradapter-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 26, + "lastSeen": 1544571310584, + "errorCount": 0 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544572800000 + }, + { + "source": { + "name": "internet-proxy", + "tags": {} + }, + "destination": { + "name": "location-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 32518, + "lastSeen": 1544575275982, + "errorCount": 75 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587870 + }, + { + "source": { + "name": "forge-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "template-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 3548918, + "lastSeen": 1544575571596, + "errorCount": 30 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587738 + }, + { + "source": { + "name": "tips-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "location-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 764, + "lastSeen": 1544574548576, + "errorCount": 0 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544574600000 + }, + { + "source": { + "name": "shopping-search-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "baratheon-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 578577, + "lastSeen": 1544575571747, + "errorCount": 0 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587662 + }, + { + "source": { + "name": "westeros-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "hodor-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 6512590, + "lastSeen": 1544575570949, + "errorCount": 1296924 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587762 + }, + { + "source": { + "name": "front-door-service", + "tags": { + "tier": "1", + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "dc" + } + }, + "destination": { + "name": "ecommerce-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 73, + "lastSeen": 1544572581996, + "errorCount": 0 + }, + "effectiveFrom": 1544493600000, + "effectiveTo": 1544572800000 + }, + { + "source": { + "name": "detail-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws,dc" + } + }, + "destination": { + "name": "provideradapter-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 9094, + "lastSeen": 1544575240906, + "errorCount": 0 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575588090 + }, + { + "source": { + "name": "forge-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "seo-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 3369864, + "lastSeen": 1544575571631, + "errorCount": 6 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587848 + }, + { + "source": { + "name": "internet-proxy", + "tags": {} + }, + "destination": { + "name": "varys-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 68388, + "lastSeen": 1544575520781, + "errorCount": 80 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587934 + }, + { + "source": { + "name": "bronn-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "location-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 99784, + "lastSeen": 1544575571751, + "errorCount": 0 + }, + "effectiveFrom": 1544563800000, + "effectiveTo": 1544575587662 + }, + { + "source": { + "name": "stark-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "seo-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 167207, + "lastSeen": 1544575421914, + "errorCount": 1 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587725 + }, + { + "source": { + "name": "tyrion-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "adapter-aws", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 8112, + "lastSeen": 1544575271597, + "errorCount": 0 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575588026 + }, + { + "source": { + "name": "guide-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "context-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 1984413, + "lastSeen": 1544575571681, + "errorCount": 1983415 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587723 + }, + { + "source": { + "name": "boss-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "dc" + } + }, + "destination": { + "name": "booking-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 37252, + "lastSeen": 1544575265116, + "errorCount": 1136 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575588025 + }, + { + "source": { + "name": "shopping-search-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "location-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 1473126, + "lastSeen": 1544575571747, + "errorCount": 0 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587672 + }, + { + "source": { + "name": "shopping-search-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "bronn-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 1, + "lastSeen": 1544565534137, + "errorCount": 0 + }, + "effectiveFrom": 1544563800000, + "effectiveTo": 1544565600000 + }, + { + "source": { + "name": "baratheon-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "bronn-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 100667, + "lastSeen": 1544575571750, + "errorCount": 1 + }, + "effectiveFrom": 1544563800000, + "effectiveTo": 1544575587848 + }, + { + "source": { + "name": "varys-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "location-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 3, + "lastSeen": 1544552474213, + "errorCount": 0 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544553000000 + }, + { + "source": { + "name": "endurance-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "authentication-service", + "tags": {} + }, + "stats": { + "count": 8719, + "lastSeen": 1544575265175, + "errorCount": 77 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587674 + }, + { + "source": { + "name": "brienne-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "geo-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 38627, + "lastSeen": 1544557965110, + "errorCount": 0 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544558400000 + }, + { + "source": { + "name": "front-door-service", + "tags": { + "tier": "1", + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws,dc" + } + }, + "destination": { + "name": "search-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws,dc" + } + }, + "stats": { + "count": 587165, + "lastSeen": 1544575520505, + "errorCount": 1 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587935 + }, + { + "source": { + "name": "front-door-service", + "tags": { + "tier": "1", + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "dc" + } + }, + "destination": { + "name": "cache-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 72562, + "lastSeen": 1544575498524, + "errorCount": 206 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587934 + }, + { + "source": { + "name": "hodor-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "location-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 10500513, + "lastSeen": 1544575570958, + "errorCount": 76483 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575588061 + }, + { + "source": { + "name": "margaery-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "airpricingservice", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 16824540, + "lastSeen": 1544575571994, + "errorCount": 0 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587641 + }, + { + "source": { + "name": "shopping-content-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "location-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 30148726, + "lastSeen": 1544575571973, + "errorCount": 0 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575588099 + }, + { + "source": { + "name": "front-door-service", + "tags": { + "tier": "1", + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "dc" + } + }, + "destination": { + "name": "domain-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 46, + "lastSeen": 1544569236656, + "errorCount": 0 + }, + "effectiveFrom": 1544497200000, + "effectiveTo": 1544571000000 + }, + { + "source": { + "name": "progressive-webapp-api", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "shopping-content-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 5277998, + "lastSeen": 1544575572135, + "errorCount": 0 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587909 + }, + { + "source": { + "name": "progressive-webapp-api", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "location-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 3891685, + "lastSeen": 1544575571930, + "errorCount": 0 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575588087 + }, + { + "source": { + "name": "front-door-service", + "tags": { + "tier": "2", + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws,dc" + } + }, + "destination": { + "name": "authentication-service", + "tags": {} + }, + "stats": { + "count": 170461, + "lastSeen": 1544575499257, + "errorCount": 11 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587699 + }, + { + "source": { + "name": "internet-proxy", + "tags": {} + }, + "destination": { + "name": "session-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 611347, + "lastSeen": 1544575568839, + "errorCount": 1 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575588024 + }, + { + "source": { + "name": "ticket-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "booking-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 13140, + "lastSeen": 1544575218424, + "errorCount": 23 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587914 + }, + { + "source": { + "name": "shopping-detail-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "shopping-content-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 5779855, + "lastSeen": 1544575569418, + "errorCount": 0 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587937 + }, + { + "source": { + "name": "front-door-service", + "tags": { + "tier": "2", + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "dc" + } + }, + "destination": { + "name": "greyjoy-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 3984995, + "lastSeen": 1544575570003, + "errorCount": 106528 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587704 + }, + { + "source": { + "name": "boss-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "ticket-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 2, + "lastSeen": 1544559967407, + "errorCount": 0 + }, + "effectiveFrom": 1544558400000, + "effectiveTo": 1544560200000 + }, + { + "source": { + "name": "detail-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws,dc" + } + }, + "destination": { + "name": "margaery-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 2674151, + "lastSeen": 1544575594133, + "errorCount": 49 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587953 + }, + { + "source": { + "name": "shopping-search-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "shopping-user-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 18737197, + "lastSeen": 1544575571645, + "errorCount": 0 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587979 + }, + { + "source": { + "name": "internet-proxy", + "tags": {} + }, + "destination": { + "name": "satellite", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 217364, + "lastSeen": 1544575482983, + "errorCount": 290 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587723 + }, + { + "source": { + "name": "internet-proxy", + "tags": {} + }, + "destination": { + "name": "shopping-cart", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 10386, + "lastSeen": 1544575280832, + "errorCount": 204 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587797 + }, + { + "source": { + "name": "mormont-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "context-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 844962, + "lastSeen": 1544575554067, + "errorCount": 28241 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587815 + }, + { + "source": { + "name": "internet-proxy", + "tags": {} + }, + "destination": { + "name": "user-profile-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 151657, + "lastSeen": 1544575488509, + "errorCount": 12 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575588060 + }, + { + "source": { + "name": "front-door-service", + "tags": { + "tier": "1", + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "dc" + } + }, + "destination": { + "name": "shae-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "dc" + } + }, + "stats": { + "count": 7126, + "lastSeen": 1544575152362, + "errorCount": 0 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587866 + }, + { + "source": { + "name": "mormont-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "destination": { + "name": "template-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 832484, + "lastSeen": 1544575554075, + "errorCount": 5 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587723 + }, + { + "source": { + "name": "detail-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "dc" + } + }, + "destination": { + "name": "rules-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 586462, + "lastSeen": 1544575592045, + "errorCount": 0 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587763 + }, + { + "source": { + "name": "detail-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "dc" + } + }, + "destination": { + "name": "controller-service", + "tags": { + "X-HAYSTACK-INFRASTRUCTURE-PROVIDER": "aws" + } + }, + "stats": { + "count": 1297487, + "lastSeen": 1544575592045, + "errorCount": 0 + }, + "effectiveFrom": 1544488200000, + "effectiveTo": 1544575587693 + } + ] +} \ No newline at end of file diff --git a/service-graph/snapshotter/src/test/resources/serviceGraph_edges.csv b/service-graph/snapshotter/src/test/resources/serviceGraph_edges.csv new file mode 100644 index 000000000..3d4ebfa64 --- /dev/null +++ b/service-graph/snapshotter/src/test/resources/serviceGraph_edges.csv @@ -0,0 +1,124 @@ +id,sourceId,destinationId,statsCount,statsLastSeen,statsErrorCount,effectiveFrom,effectiveTo +1,1,45,202931,1544575410111,0,1544488200000,1544575587937 +2,2,46,5136140,1544575571142,0,1544488200000,1544575587796 +3,3,45,134125,1544575498167,21882,1544488200000,1544575587999 +4,4,47,17367200,1544575567920,103264,1544488200000,1544575588026 +5,5,8,157201,1544575421793,36,1544488200000,1544575588024 +6,4,48,130,1544573988361,0,1544488200000,1544574600000 +7,6,12,3531583,1544575569804,0,1544488200000,1544575587705 +8,7,49,76788,1544575553640,0,1544488200000,1544575587815 +9,8,50,180327,1544575421874,2088,1544488200000,1544575587693 +10,8,51,169216,1544575421889,1,1544488200000,1544575588068 +11,9,5,22890079,1544575572251,0,1544488200000,1544575587672 +12,10,44,2,1544554416805,0,1544553000000,1544554800000 +13,11,52,60,1544574583227,0,1544488200000,1544574600000 +14,1,53,303022,1544575592096,1786,1544488200000,1544575587954 +15,12,28,3893347,1544575569844,2,1544488200000,1544575587734 +16,5,32,1442998,1544575571677,61,1544488200000,1544575587753 +17,13,34,2826475,1544575555819,1624211,1544488200000,1544575587681 +18,7,54,99413,1544575499415,0,1544488200000,1544575587706 +19,14,45,3258,1544575229219,800,1544488200000,1544575587979 +20,15,55,233615,1544575277983,16,1544488200000,1544575587689 +21,10,56,196315,1544575522183,4,1544488200000,1544575587866 +22,16,57,3,1544559964099,1,1544558400000,1544560200000 +23,14,52,169,1544574617075,38,1544488200000,1544575588010 +24,17,58,342981,1544575571750,126,1544488200000,1544575587681 +25,18,50,5042343,1544575571591,405,1544488200000,1544575587826 +26,19,59,21474934,1544575571886,26012,1544488200000,1544575588099 +27,4,60,698168,1544575554236,87771,1544488200000,1544575587677 +28,4,61,2907715,1544575570922,33645,1544488200000,1544575587793 +29,20,12,116,1544574157710,0,1544488200000,1544574600000 +30,4,62,1821807,1544575567232,6746,1544488200000,1544575588011 +31,21,33,12111,1544575506385,140,1544488200000,1544575588024 +32,22,1,570805,1544575504575,32,1544488200000,1544575587811 +33,4,3,74,1544573552180,0,1544488200000,1544574600000 +34,23,63,18356,1544575525038,0,1544488200000,1544575588011 +35,24,64,73614,1544575526411,0,1544488200000,1544575587848 +36,25,65,596144,1544575525706,3,1544488200000,1544575587937 +37,26,66,6,1544572410049,0,1544500800000,1544572800000 +38,27,6,2838890,1544575569418,0,1544488200000,1544575587682 +39,23,67,8583,1544575208652,0,1544488200000,1544575587796 +40,28,68,15560819,1544575569839,35,1544488200000,1544575587689 +41,5,18,3161466,1544575571588,4817,1544488200000,1544575587639 +42,29,69,9735,1544575279207,3,1544488200000,1544575587827 +43,4,70,410,1544574748096,0,1544488200000,1544575587918 +44,27,71,1214950,1544575528611,0,1544488200000,1544575587723 +45,30,14,20845,1544575478589,68,1544488200000,1544575587644 +46,4,72,5000,1544574936681,0,1544488200000,1544575587722 +47,4,31,687220,1544575524183,20,1544488200000,1544575587866 +48,4,73,243059,1544575396324,22,1544488200000,1544575588026 +49,5,25,652939,1544575525612,68,1544488200000,1544575587815 +50,14,30,27317,1544575505207,0,1544488200000,1544575588010 +51,4,49,2907462,1544575570897,10681,1544488200000,1544575587808 +52,4,26,84704,1544575480474,0,1544488200000,1544575587827 +53,14,74,125064,1544575501716,2985,1544488200000,1544575588087 +54,31,27,2469688,1544575569284,0,1544488200000,1544575587909 +55,32,51,806,1544573623862,0,1544488200000,1544574600000 +56,10,27,8,1544569762201,0,1544491800000,1544571000000 +57,13,75,20783217,1544575572498,246,1544488200000,1544575587641 +58,31,9,973286,1544575570804,0,1544488200000,1544575588061 +59,33,56,18729,1544575266335,16,1544488200000,1544575587723 +60,34,53,379955,1544575554493,0,1544488200000,1544575587808 +61,4,76,8861,1544575485661,324,1544500800000,1544575587634 +62,32,65,654,1544573560757,0,1544488200000,1544574600000 +63,4,77,11154,1544575207980,11,1544488200000,1544575587669 +64,15,78,13974992,1544575556069,9,1544488200000,1544575587798 +65,26,1,234369,1544575570813,6024,1544488200000,1544575587952 +66,9,44,19029006,1544575571645,0,1544488200000,1544575587677 +67,12,55,304458,1544575397477,5,1544488200000,1544575587674 +68,4,23,90053,1544575481890,7,1544488200000,1544575587788 +69,12,15,20680712,1544575556069,32,1544488200000,1544575587782 +70,10,9,56878,1544575522176,0,1544488200000,1544575587882 +71,13,43,14646996,1544575571980,145,1544488200000,1544575587689 +72,23,10,985553,1544575571604,0,1544488200000,1544575588060 +73,35,52,176,1544573823589,0,1544488200000,1544574600000 +74,34,79,237982,1544575571933,0,1544488200000,1544575587954 +75,9,6,33794745,1544575571645,0,1544488200000,1544575587875 +76,24,80,319715,1544575500355,1004,1544488200000,1544575587658 +77,30,74,13072,1544575568510,59,1544488200000,1544575587716 +78,34,45,1403232,1544575555851,0,1544488200000,1544575587827 +79,35,45,26,1544571310584,0,1544488200000,1544572800000 +80,4,56,32518,1544575275982,75,1544488200000,1544575587870 +81,18,51,3548918,1544575571596,30,1544488200000,1544575587738 +82,36,56,764,1544574548576,0,1544488200000,1544574600000 +83,9,39,578577,1544575571747,0,1544488200000,1544575587662 +84,5,42,6512590,1544575570949,1296924,1544488200000,1544575587762 +85,37,81,73,1544572581996,0,1544493600000,1544572800000 +86,1,45,9094,1544575240906,0,1544488200000,1544575588090 +87,18,65,3369864,1544575571631,6,1544488200000,1544575587848 +88,4,40,68388,1544575520781,80,1544488200000,1544575587934 +89,38,56,99784,1544575571751,0,1544563800000,1544575587662 +90,8,65,167207,1544575421914,1,1544488200000,1544575587725 +91,35,53,8112,1544575271597,0,1544488200000,1544575588026 +92,32,50,1984413,1544575571681,1983415,1544488200000,1544575587723 +93,14,57,37252,1544575265116,1136,1544488200000,1544575588025 +94,9,56,1473126,1544575571747,0,1544488200000,1544575587672 +95,9,38,1,1544565534137,0,1544563800000,1544565600000 +96,39,38,100667,1544575571750,1,1544563800000,1544575587848 +97,40,56,3,1544552474213,0,1544488200000,1544553000000 +98,33,7,8719,1544575265175,77,1544488200000,1544575587674 +99,41,82,38627,1544557965110,0,1544488200000,1544558400000 +100,22,13,587165,1544575520505,1,1544488200000,1544575587935 +101,37,83,72562,1544575498524,206,1544488200000,1544575587934 +102,42,56,10500513,1544575570958,76483,1544488200000,1544575588061 +103,43,84,16824540,1544575571994,0,1544488200000,1544575587641 +104,44,56,30148726,1544575571973,0,1544488200000,1544575588099 +105,37,85,46,1544569236656,0,1544497200000,1544571000000 +106,31,44,5277998,1544575572135,0,1544488200000,1544575587909 +107,31,56,3891685,1544575571930,0,1544488200000,1544575588087 +108,29,7,170461,1544575499257,11,1544488200000,1544575587699 +109,4,86,611347,1544575568839,1,1544488200000,1544575588024 +110,30,57,13140,1544575218424,23,1544488200000,1544575587914 +111,27,44,5779855,1544575569418,0,1544488200000,1544575587937 +112,24,87,3984995,1544575570003,106528,1544488200000,1544575587704 +113,16,30,2,1544559967407,0,1544558400000,1544560200000 +114,1,43,2674151,1544575594133,49,1544488200000,1544575587953 +115,9,71,18737197,1544575571645,0,1544488200000,1544575587979 +116,4,21,217364,1544575482983,290,1544488200000,1544575587723 +117,4,66,10386,1544575280832,204,1544488200000,1544575587797 +118,25,50,844962,1544575554067,28241,1544488200000,1544575587815 +119,4,88,151657,1544575488509,12,1544488200000,1544575588060 +120,37,89,7126,1544575152362,0,1544488200000,1544575587866 +121,25,51,832484,1544575554075,5,1544488200000,1544575587723 +122,11,79,586462,1544575592045,0,1544488200000,1544575587763 +123,11,75,1297487,1544575592045,0,1544488200000,1544575587693 diff --git a/service-graph/snapshotter/src/test/resources/serviceGraph_nodes.csv b/service-graph/snapshotter/src/test/resources/serviceGraph_nodes.csv new file mode 100644 index 000000000..6f38ed66c --- /dev/null +++ b/service-graph/snapshotter/src/test/resources/serviceGraph_nodes.csv @@ -0,0 +1,90 @@ +id,name,X-HAYSTACK-INFRASTRUCTURE-PROVIDER,tier +38,bronn-service,aws, +19,front-door-service,"aws,dc","3,1,2" +68,pricing-engine,aws, +54,front-door-service,dc, +77,drogo-api,, +30,ticket-service,aws, +71,shopping-user-service,aws, +10,api-service,aws, +15,payment-service,aws, +88,user-profile-service,aws, +63,lists-service,aws, +20,front-door-service,aws,1 +66,shopping-cart,aws, +80,bolton-service,, +81,ecommerce-service,aws, +14,boss-service,dc, +6,shopping-pricing,aws, +70,insurance-shopping-service,aws, +65,seo-service,aws, +41,brienne-service,aws, +45,provideradapter-service,aws, +55,fx,aws, +2,daily-data-update-service,dc, +34,help-service,aws, +11,detail-service,dc, +46,async-service,aws, +26,info-site-service,aws, +22,front-door-service,"aws,dc",1 +39,baratheon-service,aws, +89,shae-service,dc, +53,adapter-aws,aws, +31,progressive-webapp-api,aws, +37,front-door-service,dc,1 +85,domain-service,aws, +62,lpt-web,aws, +69,third-party-provider-service,aws, +40,varys-service,aws, +78,lannister-service,aws, +7,authentication-service,, +76,targaryen-web,aws, +61,hers-web,aws, +25,mormont-service,aws, +82,geo-service,aws, +1,detail-service,"aws,dc", +4,internet-proxy,, +86,session-service,aws, +24,front-door-service,dc,2 +33,endurance-service,aws, +72,third-party-api-service,aws, +49,userinteraction-web,aws, +9,shopping-search-service,aws, +17,front-door-service,"aws,dc","1,2" +13,search-service,"aws,dc", +64,mars,aws, +12,new-shopping-pricing,aws, +67,api-customer,aws, +8,stark-service,aws, +27,shopping-detail-service,aws, +36,tips-service,aws, +44,shopping-content-service,aws, +83,cache-service,aws, +58,checkout-payment-domain-service,dc, +16,boss-service,aws, +23,loom-service,aws, +79,rules-service,aws, +48,rails-web,aws, +5,westeros-service,aws, +35,tyrion-service,aws, +75,controller-service,aws, +51,template-service,aws, +60,his-web,aws, +28,multishop,aws, +87,greyjoy-service,aws, +84,airpricingservice,aws, +32,guide-service,aws, +18,forge-service,aws, +47,front-door-service,"aws,dc", +73,compositor-service,aws, +42,hodor-service,aws, +43,margaery-service,aws, +52,chargeback-service,aws, +59,location-service,"aws,dc", +56,location-service,aws, +29,front-door-service,"aws,dc",2 +21,satellite,aws, +3,search-service,dc, +74,melisandre-service,aws, +50,context-service,aws, +57,booking-service,aws, diff --git a/service-graph/snapshotter/src/test/scala/com/expedia/www/haystack/service/graph/snapshotter/MainSpec.scala b/service-graph/snapshotter/src/test/scala/com/expedia/www/haystack/service/graph/snapshotter/MainSpec.scala new file mode 100644 index 000000000..9d4876b0d --- /dev/null +++ b/service-graph/snapshotter/src/test/scala/com/expedia/www/haystack/service/graph/snapshotter/MainSpec.scala @@ -0,0 +1,204 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.service.graph.snapshotter + +import java.io.File +import java.nio.file.{Files, Path} +import java.time.{Clock, Instant} +import java.util.concurrent.TimeUnit.HOURS + +import com.amazonaws.services.s3.AmazonS3 +import com.expedia.www.haystack.service.graph.snapshot.store.Constants.{DotCsv, SlashEdges, SlashNodes} +import com.expedia.www.haystack.service.graph.snapshot.store.S3SnapshotStore.createItemName +import com.expedia.www.haystack.service.graph.snapshot.store.{FileSnapshotStore, S3SnapshotStore, SnapshotStore} +import com.expedia.www.haystack.service.graph.snapshotter.Main.{ServiceGraphUrlRequiredMsg, StringStoreClassRequiredMsg, UrlBaseRequiredMsg} +import org.mockito.Matchers.any +import org.mockito.Mockito.{times, verify, verifyNoMoreInteractions, when} +import org.scalatest.mockito.MockitoSugar +import org.scalatest.{BeforeAndAfter, FunSpec, Matchers} +import org.slf4j.Logger +import scalaj.http.{HttpRequest, HttpResponse} + +import scala.io.{BufferedSource, Codec, Source} + +class MainSpec extends FunSpec with Matchers with MockitoSugar with BeforeAndAfter with SnapshotStore { + private var mockLogger: Logger = _ + private var realLogger: Logger = _ + + private var mockFactory: Factory = _ + private var realFactory: Factory = _ + + private var mockClock: Clock = _ + private var realClock: Clock = _ + + private var mockAmazonS3: AmazonS3 = _ + private var realAmazonS3: AmazonS3 = _ + + private val mockHttpRequest = mock[HttpRequest] + + private def readFile(fileName: String): String = { + implicit val codec: Codec = Codec.UTF8 + lazy val bufferedSource: BufferedSource = Source.fromResource(fileName) + val fileContents = bufferedSource.getLines.mkString("\n") + bufferedSource.close() + fileContents + "\n" + } + + private val body = readFile("serviceGraph.json") + private val edges = readFile("serviceGraph_edges.csv") + private val nodes = readFile("serviceGraph_nodes.csv") + private val httpResponse: HttpResponse[String] = new HttpResponse[String](body = body, code = 0, headers = Map()) + private val now = Instant.now() + + private var tempDirectory: Path = _ + + before { + saveReaObjectsThatWillBeReplacedWithMocks() + createMocks() + replaceRealObjectsWithMocks() + + tempDirectory = Files.createTempDirectory(this.getClass.getSimpleName) + + def saveReaObjectsThatWillBeReplacedWithMocks(): Unit = { + realLogger = Main.logger + realFactory = Main.factory + realClock = Main.clock + realAmazonS3 = S3SnapshotStore.amazonS3 + } + + def createMocks(): Unit = { + mockLogger = mock[Logger] + mockFactory = mock[Factory] + mockClock = mock[Clock] + mockAmazonS3 = mock[AmazonS3] + } + + def replaceRealObjectsWithMocks(): Unit = { + Main.logger = mockLogger + Main.factory = mockFactory + Main.clock = mockClock + S3SnapshotStore.amazonS3 = mockAmazonS3 + } + } + + after { + restoreRealObjects() + recursiveDelete(tempDirectory.toFile) + verifyNoMoreInteractions(mockLogger) + verifyNoMoreInteractions(mockFactory) + verifyNoMoreInteractions(mockClock) + verifyNoMoreInteractions(mockAmazonS3) + + def restoreRealObjects(): Unit = { + Main.logger = realLogger + Main.factory = realFactory + Main.clock = realClock + } + + def recursiveDelete(file: File) { + if (file.isDirectory) + Option(file.listFiles).map(_.toList).getOrElse(Nil).foreach(recursiveDelete) + file.delete + } + } + + describe("Main.main() called with no arguments") { + it("should log an error") { + Main.main(Array()) + verify(mockLogger).error(ServiceGraphUrlRequiredMsg) + } + } + + describe("Main.main() called with one argument") { + it("should log an error") { + Main.main(Array(serviceGraphUrlBase)) + verify(mockLogger).error(StringStoreClassRequiredMsg) + } + } + + private val fullyQualifiedFileSnaphotStoreClassName = new FileSnapshotStore().getClass.getCanonicalName + + describe("Main.main() called with two arguments") { + it("should log an error") { + Main.main(Array(serviceGraphUrlBase, fullyQualifiedFileSnaphotStoreClassName)) + verify(mockLogger).error(UrlBaseRequiredMsg) + } + } + + val serviceGraphUrlBase: String = "http://apis/graph/servicegraph" + val serviceGraphUrl: String = serviceGraphUrlBase + Main.ServiceGraphUrlSuffix + + describe("Main.main() called with FileSnapshotStore arguments") { + it("should create a FileSnapshotStore, write to it, then call purge()") { + def verifyDirectoryIsEmptyToProveThatPurgeWasCalled = { + tempDirectory.toFile.listFiles().length shouldBe 0 + } + + when(mockFactory.createHttpRequest(any(), any())).thenReturn(mockHttpRequest) + when(mockHttpRequest.asString).thenReturn(httpResponse) + when(mockClock.instant()).thenReturn(now) + + Main.main(Array(serviceGraphUrlBase, fullyQualifiedFileSnaphotStoreClassName, tempDirectory.toString)) + + verifyDirectoryIsEmptyToProveThatPurgeWasCalled + verifiesForCallToServiceGraphUrl(1) + } + } + + describe("Main.main() called with all S3SnapshotStore arguments") { + it("should create an S3SnapshotStore, write to it, then call purge()") { + val bucketName = "haystack-snapshots" + val folderName = "hourly-snapshots" + val fileNameBase = createIso8601FileName(now) + when(mockFactory.createHttpRequest(any(), any())).thenReturn(mockHttpRequest) + when(mockHttpRequest.asString).thenReturn(httpResponse) + when(mockClock.instant()).thenReturn(now) + + Main.main(Array(serviceGraphUrlBase, new S3SnapshotStore().getClass.getCanonicalName, bucketName, folderName, "1000")) + + verifiesForCallToServiceGraphUrl(2) + verify(mockAmazonS3).doesBucketExistV2(bucketName) + verify(mockAmazonS3).createBucket(bucketName) + verify(mockAmazonS3).putObject(bucketName, createItemName(folderName, fileNameBase + SlashEdges + DotCsv), edges) + verify(mockAmazonS3).putObject(bucketName, createItemName(folderName, fileNameBase + SlashNodes + DotCsv), nodes) + } + } + + private def verifiesForCallToServiceGraphUrl(wantedNumberOfInvocations: Int) = { + verify(mockFactory).createHttpRequest(serviceGraphUrl, now.toEpochMilli - HOURS.toMillis(1)) + verify(mockHttpRequest, times(wantedNumberOfInvocations)).asString + verify(mockClock).instant() + } + + describe("Factory.createHttpRequest()") { + it("should properly construct the URL") { + val factory = new Factory + val httpRequest = factory.createHttpRequest(serviceGraphUrl, now.toEpochMilli) + val url = httpRequest.url + url should startWith(serviceGraphUrlBase) + url should endWith(Main.ServiceGraphUrlSuffix.format(now.toEpochMilli)) + } + } + + //noinspection NotImplementedCode + def build(constructorArguments: Array[String]): SnapshotStore = ??? + //noinspection NotImplementedCode + def read(instant: java.time.Instant): Option[String] = ??? + //noinspection NotImplementedCode + def write(instant: java.time.Instant,content: String): AnyRef = ??? +} diff --git a/traces/.gitignore b/traces/.gitignore new file mode 100644 index 000000000..5383dcbfc --- /dev/null +++ b/traces/.gitignore @@ -0,0 +1,13 @@ +*.class +*.iml +*.log +.classpath +.project +logs/ +target/ +.idea/ +node_modules +package-lock.json +*.ipr +*.iws +reader/isHealthy diff --git a/traces/.mvn/wrapper/MavenWrapperDownloader.java b/traces/.mvn/wrapper/MavenWrapperDownloader.java new file mode 100755 index 000000000..fa4f7b499 --- /dev/null +++ b/traces/.mvn/wrapper/MavenWrapperDownloader.java @@ -0,0 +1,110 @@ +/* +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +*/ + +import java.net.*; +import java.io.*; +import java.nio.channels.*; +import java.util.Properties; + +public class MavenWrapperDownloader { + + /** + * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. + */ + private static final String DEFAULT_DOWNLOAD_URL = + "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar"; + + /** + * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to + * use instead of the default one. + */ + private static final String MAVEN_WRAPPER_PROPERTIES_PATH = + ".mvn/wrapper/maven-wrapper.properties"; + + /** + * Path where the maven-wrapper.jar will be saved to. + */ + private static final String MAVEN_WRAPPER_JAR_PATH = + ".mvn/wrapper/maven-wrapper.jar"; + + /** + * Name of the property which should be used to override the default download url for the wrapper. + */ + private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; + + public static void main(String args[]) { + System.out.println("- Downloader started"); + File baseDirectory = new File(args[0]); + System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); + + // If the maven-wrapper.properties exists, read it and check if it contains a custom + // wrapperUrl parameter. + File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); + String url = DEFAULT_DOWNLOAD_URL; + if(mavenWrapperPropertyFile.exists()) { + FileInputStream mavenWrapperPropertyFileInputStream = null; + try { + mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); + Properties mavenWrapperProperties = new Properties(); + mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); + url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); + } catch (IOException e) { + System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); + } finally { + try { + if(mavenWrapperPropertyFileInputStream != null) { + mavenWrapperPropertyFileInputStream.close(); + } + } catch (IOException e) { + // Ignore ... + } + } + } + System.out.println("- Downloading from: : " + url); + + File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); + if(!outputFile.getParentFile().exists()) { + if(!outputFile.getParentFile().mkdirs()) { + System.out.println( + "- ERROR creating output direcrory '" + outputFile.getParentFile().getAbsolutePath() + "'"); + } + } + System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); + try { + downloadFileFromURL(url, outputFile); + System.out.println("Done"); + System.exit(0); + } catch (Throwable e) { + System.out.println("- Error downloading"); + e.printStackTrace(); + System.exit(1); + } + } + + private static void downloadFileFromURL(String urlString, File destination) throws Exception { + URL website = new URL(urlString); + ReadableByteChannel rbc; + rbc = Channels.newChannel(website.openStream()); + FileOutputStream fos = new FileOutputStream(destination); + fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); + fos.close(); + rbc.close(); + } + +} diff --git a/traces/.mvn/wrapper/maven-wrapper.jar b/traces/.mvn/wrapper/maven-wrapper.jar new file mode 100755 index 0000000000000000000000000000000000000000..01e67997377a393fd672c7dcde9dccbedf0cb1e9 GIT binary patch literal 48337 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 -m "Release description..." +git push origin +``` + +`` must follow semantic versioning scheme. + +Or one can also tag using UI: https://github.com/ExpediaDotCom/haystack-traces/releases + +It is preferred to create an annotated tag using `git tag -a` and then use the release UI to add release notes for the tag. + +* After the release is completed, please update the `pom.xml` files to next `-SNAPSHOT` version to match the next release diff --git a/traces/backends/Makefile b/traces/backends/Makefile new file mode 100644 index 000000000..e55c9c4ed --- /dev/null +++ b/traces/backends/Makefile @@ -0,0 +1,23 @@ +.PHONY: all cassandra memory release + +PWD := $(shell pwd) + +all: cassandra memory + +cassandra: build_cassandra + cd cassandra && $(MAKE) integration_test + +build_cassandra: + cd ../ && ./mvnw -q package -DfinalName=haystack-trace-backend-cassandra -pl backends/cassandra -am + + +memory: build_memory + cd memory && $(MAKE) integration_test + +build_memory: + cd ../ && ./mvnw -q package -DfinalName=haystack-trace-backend-memory -pl backends/memory -am + +# release all backends +release: + cd cassandra && $(MAKE) docker_build && $(MAKE) release + cd memory && $(MAKE) docker_build && $(MAKE) release diff --git a/traces/backends/cassandra/Makefile b/traces/backends/cassandra/Makefile new file mode 100644 index 000000000..6fb32b1fe --- /dev/null +++ b/traces/backends/cassandra/Makefile @@ -0,0 +1,24 @@ +.PHONY: docker_build prepare_integration_test_env integration_test release + +export DOCKER_ORG := expediadotcom +export DOCKER_IMAGE_NAME := haystack-trace-backend-cassandra +PWD := $(shell pwd) +SERVICE_DEBUG_ON ?= false + +docker_build: + # build docker image using existing app jar + docker build -t $(DOCKER_IMAGE_NAME) -f build/docker/Dockerfile . + +prepare_integration_test_env: docker_build + # prepare environment to run integration tests against + docker-compose -f build/integration-tests/docker-compose.yml -p sandbox up -d + sleep 30 + +integration_test: prepare_integration_test_env + cd ../../ &&./mvnw -q integration-test -pl backends/cassandra -am + docker-compose -f build/integration-tests/docker-compose.yml -p sandbox stop + docker rm $(shell docker ps -a -q) + docker volume rm $(shell docker volume ls -q) + +release: + ../../deployment/scripts/publish-to-docker-hub.sh diff --git a/traces/backends/cassandra/README.md b/traces/backends/cassandra/README.md new file mode 100644 index 000000000..a9528bb35 --- /dev/null +++ b/traces/backends/cassandra/README.md @@ -0,0 +1,15 @@ +# Storage Backend - Cassandra + + +Grpc service which can read a write spans to a cassandra cluster + +## Technical Details + +In order to understand this service, we recommend to read the details of [haystack](https://github.com/ExpediaDotCom/haystack) project. +This service reads from [Cassandra](http://cassandra.apache.org/). API endpoints are exposed as [GRPC](https://grpc.io/) endpoints. + +Will fill in more details as we go.. + +## Building + +Check the details on [Build Section](../README.md) diff --git a/traces/backends/cassandra/build/docker/Dockerfile b/traces/backends/cassandra/build/docker/Dockerfile new file mode 100644 index 000000000..f9a428197 --- /dev/null +++ b/traces/backends/cassandra/build/docker/Dockerfile @@ -0,0 +1,30 @@ +FROM openjdk:8-jre +MAINTAINER Haystack + +ENV APP_NAME haystack-trace-backend-cassandra +ENV APP_HOME /app/bin +ENV JMXTRANS_AGENT jmxtrans-agent-1.2.6 +ENV DOCKERIZE_VERSION v0.6.1 + +ADD https://github.com/jwilder/dockerize/releases/download/${DOCKERIZE_VERSION}/dockerize-alpine-linux-amd64-${DOCKERIZE_VERSION}.tar.gz dockerize.tar.gz +RUN tar xzf dockerize.tar.gz +RUN chmod +x dockerize + +RUN mkdir -p ${APP_HOME} + +COPY target/${APP_NAME}.jar ${APP_HOME}/ +COPY build/docker/start-app.sh ${APP_HOME}/ +RUN chmod +x ${APP_HOME}/start-app.sh + +RUN GRPC_HEALTH_PROBE_VERSION=v0.2.0 && \ + wget -qO/bin/grpc_health_probe https://github.com/grpc-ecosystem/grpc-health-probe/releases/download/${GRPC_HEALTH_PROBE_VERSION}/grpc_health_probe-linux-amd64 && \ + chmod +x /bin/grpc_health_probe + +COPY build/docker/jmxtrans-agent.xml ${APP_HOME}/ +ADD https://github.com/jmxtrans/jmxtrans-agent/releases/download/${JMXTRANS_AGENT}/${JMXTRANS_AGENT}.jar ${APP_HOME}/ + +WORKDIR ${APP_HOME} + +EXPOSE 8090 + +ENTRYPOINT ["./start-app.sh"] diff --git a/traces/backends/cassandra/build/docker/jmxtrans-agent.xml b/traces/backends/cassandra/build/docker/jmxtrans-agent.xml new file mode 100644 index 000000000..6237d6900 --- /dev/null +++ b/traces/backends/cassandra/build/docker/jmxtrans-agent.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + ${HAYSTACK_GRAPHITE_HOST:monitoring-influxdb-graphite.kube-system.svc} + ${HAYSTACK_GRAPHITE_PORT:2003} + ${HAYSTACK_GRAPHITE_ENABLED:true} + haystack.traces.backend-cassandra.#hostname#. + + 30 + diff --git a/traces/backends/cassandra/build/docker/start-app.sh b/traces/backends/cassandra/build/docker/start-app.sh new file mode 100755 index 000000000..ba2c65569 --- /dev/null +++ b/traces/backends/cassandra/build/docker/start-app.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +[ -z "$JAVA_XMS" ] && JAVA_XMS=1024m +[ -z "$JAVA_XMX" ] && JAVA_XMX=1024m +[ -z "$JAVA_GC_OPTS" ] && JAVA_GC_OPTS="-XX:+UseG1GC" + +set -e +JAVA_OPTS="${JAVA_OPTS} \ +-javaagent:${APP_HOME}/${JMXTRANS_AGENT}.jar=${APP_HOME}/jmxtrans-agent.xml \ +${JAVA_GC_OPTS} \ +-Xmx${JAVA_XMX} \ +-Xms${JAVA_XMS} \ +-XX:+ExitOnOutOfMemoryError \ +-Dapplication.name=${APP_NAME} \ +-Dapplication.home=${APP_HOME}" + +if [[ -n "$SERVICE_DEBUG_ON" ]] && [[ "$SERVICE_DEBUG_ON" == true ]]; then + JAVA_OPTS="$JAVA_OPTS -Xdebug -Xrunjdwp:transport=dt_socket,address=5005,server=y" +fi + +exec java ${JAVA_OPTS} -jar "${APP_HOME}/${APP_NAME}.jar" diff --git a/traces/backends/cassandra/build/integration-tests/docker-compose.yml b/traces/backends/cassandra/build/integration-tests/docker-compose.yml new file mode 100644 index 000000000..0f9ade550 --- /dev/null +++ b/traces/backends/cassandra/build/integration-tests/docker-compose.yml @@ -0,0 +1,9 @@ +version: '3' +services: + cassandra: + image: cassandra:3.11.0 + environment: + MAX_HEAP_SIZE: 256m + HEAP_NEWSIZE: 256m + ports: + - "9042:9042" diff --git a/traces/backends/cassandra/pom.xml b/traces/backends/cassandra/pom.xml new file mode 100644 index 000000000..c3f6acd6f --- /dev/null +++ b/traces/backends/cassandra/pom.xml @@ -0,0 +1,173 @@ + + + + + haystack-trace-backends + com.expedia.www + 1.0.9-SNAPSHOT + ../pom.xml + + + 4.0.0 + haystack-trace-backend-cassandra + jar + + + com.expedia.www.haystack.trace.storage.backends.cassandra.Service + ${project.artifactId}-${project.version} + 3.3.0.1 + 3.6.0 + + + + + + com.datastax.cassandra + cassandra-driver-extras + ${cassandra.driver.version} + + + com.google.guava + guava + + + + + com.google.protobuf + protobuf-java + + + + io.grpc + grpc-protobuf + + + + io.grpc + grpc-stub + + + + io.grpc + grpc-services + + + + io.grpc + grpc-netty + + + + io.netty + netty-handler + + + + org.apache.commons + commons-lang3 + + + + org.apache.httpcomponents + httpclient + + + + com.amazonaws + aws-java-sdk-ec2 + + + + + + ${finalName} + + + org.scalatest + scalatest-maven-plugin + + + test + + test + + + + cass1,cass2 + cassandra_cql_schema_1 + + com.expedia.www.haystack.trace.storage.backends.cassandra.unit + + + + integration-test + integration-test + + test + + + + /src/backends/cassandra/build/integration-tests/docker-app.conf + + com.expedia.www.haystack.trace.storage.backends.cassandra.integration + + + + + + + org.apache.maven.plugins + maven-shade-plugin + + true + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + + + + package + + shade + + + + + reference.conf + + + ${mainClass} + + + + + + + + + net.alchim31.maven + scala-maven-plugin + + + + org.scalastyle + scalastyle-maven-plugin + + + org.apache.maven.plugins + maven-compiler-plugin + + + + + diff --git a/traces/backends/cassandra/src/main/resources/config/base.conf b/traces/backends/cassandra/src/main/resources/config/base.conf new file mode 100644 index 000000000..f2e82924a --- /dev/null +++ b/traces/backends/cassandra/src/main/resources/config/base.conf @@ -0,0 +1,62 @@ +health.status.path = "/app/isHealthy" + +service { + port = 8090 + ssl { + enabled = false + cert.path = "" + private.key.path = "" + } + max.message.size = 52428800 # 50MB in bytes +} + +cassandra { + # multiple endpoints can be provided as comma separated list + endpoints = "cassandra" + + # enable the auto.discovery mode, if true then we ignore the endpoints(above) and use auto discovery + # mechanism to find cassandra nodes. For today we only support aws node discovery provider + auto.discovery { + enabled: false + // aws: { + // region: "us-west-2" + // tags: { + // Role: haystack-cassandra + // Environment: ewetest + // } + // } + } + + connections { + max.per.host = 50 + read.timeout.ms = 30000 + conn.timeout.ms = 10000 + keep.alive = true + } + + retries { + max = 10 + backoff { + initial.ms = 100 + factor = 2 + } + } + + consistency.level = "one" + + on.error.consistency.level = [ + "com.datastax.driver.core.exceptions.UnavailableException", + "any" + ] + + ttl.sec = 259200 + + keyspace: { + # auto creates the keyspace and table name in cassandra(if absent) + # if schema field is empty or not present, then no operation is performed + auto.create.schema = "CREATE KEYSPACE IF NOT EXISTS haystack WITH REPLICATION = { 'class': 'SimpleStrategy', 'replication_factor' : 1 } AND durable_writes = false; CREATE TABLE IF NOT EXISTS haystack.traces (id varchar, ts timestamp, spans blob, PRIMARY KEY ((id), ts)) WITH CLUSTERING ORDER BY (ts ASC) AND compaction = { 'class' : 'DateTieredCompactionStrategy', 'max_sstable_age_days': '3' } AND gc_grace_seconds = 86400;" + + name: "haystack" + table.name: "traces" + } +} diff --git a/traces/backends/cassandra/src/main/resources/logback.xml b/traces/backends/cassandra/src/main/resources/logback.xml new file mode 100644 index 000000000..7ef04ff2c --- /dev/null +++ b/traces/backends/cassandra/src/main/resources/logback.xml @@ -0,0 +1,27 @@ + + + + + + + true + + + + + + %d{yyyy-MM-dd HH:mm:ss:SSS} %thread, %level, %logger{70}, "%msg" %replace(%ex){'[\n]+', '\\n'}%nopex%n + + + + + + ${HAYSTACK_LOG_QUEUE_SIZE:-500} + ${HAYSTACK_LOG_DISCARD_THRESHOLD:-0} + + + + + + + \ No newline at end of file diff --git a/traces/backends/cassandra/src/main/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/Service.scala b/traces/backends/cassandra/src/main/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/Service.scala new file mode 100644 index 000000000..f0bd2b0e0 --- /dev/null +++ b/traces/backends/cassandra/src/main/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/Service.scala @@ -0,0 +1,95 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.trace.storage.backends.cassandra + +import java.io.File + +import com.codahale.metrics.JmxReporter +import com.expedia.www.haystack.commons.logger.LoggerUtils +import com.expedia.www.haystack.commons.metrics.MetricsSupport +import com.expedia.www.haystack.trace.storage.backends.cassandra.client.{CassandraClusterFactory, CassandraSession} +import com.expedia.www.haystack.trace.storage.backends.cassandra.config.ProjectConfiguration +import com.expedia.www.haystack.trace.storage.backends.cassandra.services.{GrpcHealthService, SpansPersistenceService} +import com.expedia.www.haystack.trace.storage.backends.cassandra.store.{CassandraTraceRecordReader, CassandraTraceRecordWriter} +import io.grpc.netty.NettyServerBuilder +import org.slf4j.{Logger, LoggerFactory} + +object Service extends MetricsSupport { + private val LOGGER: Logger = LoggerFactory.getLogger("CassandraBackend") + + // primary executor for service's async tasks + implicit private val executor = scala.concurrent.ExecutionContext.global + + def main(args: Array[String]): Unit = { + startJmxReporter() + startService() + } + + private def startJmxReporter(): Unit = { + JmxReporter + .forRegistry(metricRegistry) + .build() + .start() + } + + private def startService(): Unit = { + try { + val config = new ProjectConfiguration + val serviceConfig = config.serviceConfig + val cassandraSession = new CassandraSession(config.cassandraConfig.clientConfig, new CassandraClusterFactory) + + val tracerRecordWriter = new CassandraTraceRecordWriter(cassandraSession, config.cassandraConfig) + val tracerRecordReader = new CassandraTraceRecordReader(cassandraSession, config.cassandraConfig.clientConfig) + + val serverBuilder = NettyServerBuilder + .forPort(serviceConfig.port) + .directExecutor() + .addService(new GrpcHealthService()) + .addService(new SpansPersistenceService(reader = tracerRecordReader, writer = tracerRecordWriter)(executor)) + + // enable ssl if enabled + if (serviceConfig.ssl.enabled) { + serverBuilder.useTransportSecurity(new File(serviceConfig.ssl.certChainFilePath), new File(serviceConfig.ssl.privateKeyPath)) + } + + // default max message size in grpc is 4MB. if our max message size is greater than 4MB then we should configure this + // limit in the netty based grpc server. + if (serviceConfig.maxSizeInBytes > 4 * 1024 * 1024) serverBuilder.maxMessageSize(serviceConfig.maxSizeInBytes) + + val server = serverBuilder.build().start() + + LOGGER.info(s"server started, listening on ${serviceConfig.port}") + + Runtime.getRuntime.addShutdownHook(new Thread() { + override def run(): Unit = { + LOGGER.info("shutting down gRPC server since JVM is shutting down") + cassandraSession.close() + server.shutdown() + LOGGER.info("server has been shutdown now") + } + }) + + server.awaitTermination() + } catch { + case ex: Throwable => + ex.printStackTrace() + LOGGER.error("Fatal error observed while running the app", ex) + LoggerUtils.shutdownLogger() + System.exit(1) + } + } +} diff --git a/traces/backends/cassandra/src/main/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/client/AwsNodeDiscoverer.scala b/traces/backends/cassandra/src/main/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/client/AwsNodeDiscoverer.scala new file mode 100644 index 000000000..5cf7dacc5 --- /dev/null +++ b/traces/backends/cassandra/src/main/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/client/AwsNodeDiscoverer.scala @@ -0,0 +1,82 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trace.storage.backends.cassandra.client + +import java.util.Collections + +import com.amazonaws.regions.{Region, Regions} +import com.amazonaws.services.ec2.AmazonEC2Client +import com.amazonaws.services.ec2.model.{DescribeInstancesRequest, Filter, Instance, InstanceStateName} +import org.slf4j.LoggerFactory + +import scala.collection.JavaConverters._ + +object AwsNodeDiscoverer { + private val LOGGER = LoggerFactory.getLogger(AwsNodeDiscoverer.getClass) + + /** + * discovers the EC2 ip addresses on AWS for a given region and set of tags + * @param region aws region + * @param tags a set of ec2 node tags + * @return + */ + def discover(region: String, + tags: Map[String, String]): Seq[String] = { + LOGGER.info(s"discovering EC2 nodes for region=$region, and tags=${tags.mkString(",")}") + + val awsRegion = Region.getRegion(Regions.fromName(region)) + val client:AmazonEC2Client = new AmazonEC2Client().withRegion(awsRegion) + try { + discover(client, tags) + } catch { + case ex: Exception => + LOGGER.error(s"Fail to discover EC2 nodes for region=$region and tags=$tags with reason", ex) + throw new RuntimeException(ex) + } finally { + client.shutdown() + } + } + + /** + * discovers the EC2 ip addresses on AWS for a given region and set of tags + * @param client ec2 client + * @param tags a set of ec2 node tags + * @return + */ + private[haystack] def discover(client: AmazonEC2Client, tags: Map[String, String]): Seq[String] = { + val filters = tags.map { case (key, value) => new Filter("tag:" + key, Collections.singletonList(value)) } + val request = new DescribeInstancesRequest().withFilters(filters.asJavaCollection) + + val result = client.describeInstances(request) + + val nodes = result.getReservations + .asScala + .flatMap(_.getInstances.asScala) + .filter(isValidInstance) + .map(_.getPrivateIpAddress) + + LOGGER.info("EC2 nodes discovered [{}]", nodes.mkString(",")) + nodes + } + + // check if an ec2 instance is in running state + private def isValidInstance(instance: Instance): Boolean = { + // instance should be in running state + InstanceStateName.Running.toString.equals(instance.getState.getName) + } +} diff --git a/traces/backends/cassandra/src/main/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/client/CassandraClusterFactory.scala b/traces/backends/cassandra/src/main/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/client/CassandraClusterFactory.scala new file mode 100644 index 000000000..2201c7f2d --- /dev/null +++ b/traces/backends/cassandra/src/main/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/client/CassandraClusterFactory.scala @@ -0,0 +1,64 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trace.storage.backends.cassandra.client + +import com.datastax.driver.core._ +import com.datastax.driver.core.policies.{DefaultRetryPolicy, LatencyAwarePolicy, RoundRobinPolicy, TokenAwarePolicy} +import com.datastax.driver.extras.codecs.date.SimpleTimestampCodec +import com.expedia.www.haystack.trace.storage.backends.cassandra.config.entities.{AwsNodeDiscoveryConfiguration, ClientConfiguration, CredentialsConfiguration} + +class CassandraClusterFactory extends ClusterFactory { + + private def discoverNodes(nodeDiscoveryConfig: Option[AwsNodeDiscoveryConfiguration]): Seq[String] = { + nodeDiscoveryConfig match { + case Some(awsDiscovery) => AwsNodeDiscoverer.discover(awsDiscovery.region, awsDiscovery.tags) + case _ => Nil + } + } + + + override def buildCluster(config: ClientConfiguration): Cluster = { + val contactPoints = if (config.autoDiscoverEnabled) discoverNodes(config.awsNodeDiscovery) else config.endpoints + require(contactPoints.nonEmpty, "cassandra contact points can't be empty!!!") + + val tokenAwarePolicy = new TokenAwarePolicy(new LatencyAwarePolicy.Builder(new RoundRobinPolicy()).build()) + val authProvider = fetchAuthProvider(config.plaintextCredentials) + val cluster = Cluster.builder() + .withClusterName("cassandra-cluster") + .addContactPoints(contactPoints: _*) + .withRetryPolicy(DefaultRetryPolicy.INSTANCE) + .withAuthProvider(authProvider) + .withSocketOptions(new SocketOptions() + .setKeepAlive(config.socket.keepAlive) + .setConnectTimeoutMillis(config.socket.connectionTimeoutMillis) + .setReadTimeoutMillis(config.socket.readTimeoutMills)) + .withLoadBalancingPolicy(tokenAwarePolicy) + .withPoolingOptions(new PoolingOptions().setMaxConnectionsPerHost(HostDistance.LOCAL, config.socket.maxConnectionPerHost)) + .build() + cluster.getConfiguration.getCodecRegistry.register(SimpleTimestampCodec.instance) + + cluster + } + + private def fetchAuthProvider(plaintextCredentials: Option[CredentialsConfiguration]): AuthProvider = { + plaintextCredentials match { + case Some(credentialsConfiguration) => new PlainTextAuthProvider(credentialsConfiguration.username, credentialsConfiguration.password) + case _ => AuthProvider.NONE + } + } +} diff --git a/traces/backends/cassandra/src/main/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/client/CassandraSession.scala b/traces/backends/cassandra/src/main/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/client/CassandraSession.scala new file mode 100644 index 000000000..f2e24a103 --- /dev/null +++ b/traces/backends/cassandra/src/main/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/client/CassandraSession.scala @@ -0,0 +1,138 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trace.storage.backends.cassandra.client + +import java.nio.ByteBuffer +import java.util.Date + +import com.datastax.driver.core._ +import com.datastax.driver.core.exceptions.NoHostAvailableException +import com.datastax.driver.core.querybuilder.QueryBuilder +import com.expedia.www.haystack.trace.storage.backends.cassandra.config.entities.{ClientConfiguration, KeyspaceConfiguration} +import org.slf4j.LoggerFactory +import com.expedia.www.haystack.trace.storage.backends.cassandra.client.CassandraTableSchema._ + +import scala.collection.JavaConverters._ +import scala.util.{Failure, Success, Try} + +object CassandraSession { + private val LOGGER = LoggerFactory.getLogger(classOf[CassandraSession]) + + def connect(config: ClientConfiguration, + factory: ClusterFactory): (Cluster, Session) = this.synchronized { + def tryConnect(): (Cluster, Session) = { + val cluster = factory.buildCluster(config) + Try(cluster.connect()) match { + case Success(session) => (cluster, session) + case Failure(e: NoHostAvailableException) => + LOGGER.warn("Failed to connect to cassandra. Will try again", e) + Thread.sleep(5000) + tryConnect() + case Failure(e) => throw e + } + } + + tryConnect() + } +} + +class CassandraSession(config: ClientConfiguration, factory: ClusterFactory) { + import CassandraSession._ + + /** + * builds a session object to interact with cassandra cluster + * Also ensure that keyspace and table names exists in cassandra. + */ + lazy val (cluster, session) = connect(config, factory) + + def ensureKeyspace(keyspace: KeyspaceConfiguration): Unit = { + LOGGER.info("ensuring kespace exists with {}", keyspace) + CassandraTableSchema.ensureExists(keyspace.name, keyspace.table, keyspace.autoCreateSchema, session) + } + + lazy val selectRawTracesPreparedStmt: PreparedStatement = { + import QueryBuilder.bindMarker + session.prepare( + QueryBuilder + .select() + .from(config.tracesKeyspace.name, config.tracesKeyspace.table) + .where(QueryBuilder.in(ID_COLUMN_NAME, bindMarker(ID_COLUMN_NAME)))) + } + + + def createSpanInsertPreparedStatement(keyspace: KeyspaceConfiguration): PreparedStatement = { + import QueryBuilder.{bindMarker, ttl} + + val insert = QueryBuilder + .insertInto(keyspace.name, keyspace.table) + .value(ID_COLUMN_NAME, bindMarker(ID_COLUMN_NAME)) + .value(TIMESTAMP_COLUMN_NAME, bindMarker(TIMESTAMP_COLUMN_NAME)) + .value(SPANS_COLUMN_NAME, bindMarker(SPANS_COLUMN_NAME)) + .using(ttl(keyspace.recordTTLInSec)) + + session.prepare(insert) + } + + /** + * close the session and client + */ + def close(): Unit = { + Try(session.close()) + Try(cluster.close()) + } + + + /** + * create bound statement for writing to cassandra table + * + * @param traceId trace id + * @param spanBufferBytes data bytes of spanBuffer that belong to a given trace id + * @param consistencyLevel consistency level for cassandra write + * @param insertTraceStatement prepared statement to use + * @return + */ + def newTraceInsertBoundStatement(traceId: String, + spanBufferBytes: Array[Byte], + consistencyLevel: ConsistencyLevel, + insertTraceStatement: PreparedStatement): Statement = { + new BoundStatement(insertTraceStatement) + .setString(ID_COLUMN_NAME, traceId) + .setTimestamp(TIMESTAMP_COLUMN_NAME, new Date()) + .setBytes(SPANS_COLUMN_NAME, ByteBuffer.wrap(spanBufferBytes)) + .setConsistencyLevel(consistencyLevel) + } + + + /** + * create new select statement for retrieving Raw Traces data for traceIds + * + * @param traceIds list of trace id + * @return statement for select query for traceIds + */ + def newSelectRawTracesBoundStatement(traceIds: List[String]): Statement = { + new BoundStatement(selectRawTracesPreparedStmt).setList(ID_COLUMN_NAME, traceIds.asJava) + } + + /** + * executes the statement async and return the resultset future + * + * @param statement prepared statement to be executed + * @return future object of ResultSet + */ + def executeAsync(statement: Statement): ResultSetFuture = session.executeAsync(statement) +} diff --git a/traces/backends/cassandra/src/main/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/client/CassandraTableSchema.scala b/traces/backends/cassandra/src/main/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/client/CassandraTableSchema.scala new file mode 100644 index 000000000..8e8490ae6 --- /dev/null +++ b/traces/backends/cassandra/src/main/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/client/CassandraTableSchema.scala @@ -0,0 +1,69 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trace.storage.backends.cassandra.client + +import com.datastax.driver.core._ +import org.slf4j.LoggerFactory + +object CassandraTableSchema { + private val LOGGER = LoggerFactory.getLogger(CassandraTableSchema.getClass) + + val ID_COLUMN_NAME = "id" + val TIMESTAMP_COLUMN_NAME = "ts" + val SPANS_COLUMN_NAME = "spans" + val SERVICE_COLUMN_NAME = "service_name" + val OPERATION_COLUMN_NAME = "operation_name" + + + /** + * ensures the keyspace and table name exists in com.expedia.www.haystack.trace.storage.backends.cassandra + * + * @param keyspace com.expedia.www.haystack.trace.storage.backends.cassandra keyspace + * @param tableName table name in com.expedia.www.haystack.trace.storage.backends.cassandra + * @param session com.expedia.www.haystack.trace.storage.backends.cassandra client session + * @param autoCreateSchema if present, then apply the cql schema that should create the keyspace and com.expedia.www.haystack.trace.storage.backends.cassandra table, + * else throw an exception if fail to find the keyspace and table + */ + def ensureExists(keyspace: String, tableName: String, autoCreateSchema: Option[String], session: Session): Unit = { + val keyspaceMetadata = session.getCluster.getMetadata.getKeyspace(keyspace) + if (keyspaceMetadata == null || keyspaceMetadata.getTable(tableName) == null) { + autoCreateSchema match { + case Some(schema) => applyCqlSchema(session, schema) + case _ => throw new RuntimeException(s"Fail to find the keyspace=$keyspace and/or table=$tableName !!!!") + } + } + } + + /** + * apply the cql schema + * + * @param session session object to interact with com.expedia.www.haystack.trace.storage.backends.cassandra + * @param schema schema data + */ + private def applyCqlSchema(session: Session, schema: String): Unit = { + try { + for (cmd <- schema.split(";")) { + if (cmd.nonEmpty) session.execute(cmd) + } + } catch { + case ex: Exception => + LOGGER.error(s"Failed to apply cql $schema with following reason:", ex) + throw new RuntimeException(ex) + } + } +} diff --git a/traces/backends/cassandra/src/main/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/client/ClusterFactory.scala b/traces/backends/cassandra/src/main/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/client/ClusterFactory.scala new file mode 100644 index 000000000..9d643935e --- /dev/null +++ b/traces/backends/cassandra/src/main/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/client/ClusterFactory.scala @@ -0,0 +1,28 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trace.storage.backends.cassandra.client + +import com.datastax.driver.core.Cluster +import com.expedia.www.haystack.trace.storage.backends.cassandra.config.entities.ClientConfiguration + +/** + * factory that builds the cluster. this is useful for testing other classes + */ +trait ClusterFactory { + def buildCluster(config: ClientConfiguration): Cluster +} diff --git a/traces/backends/cassandra/src/main/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/config/ProjectConfiguration.scala b/traces/backends/cassandra/src/main/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/config/ProjectConfiguration.scala new file mode 100644 index 000000000..c747e3813 --- /dev/null +++ b/traces/backends/cassandra/src/main/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/config/ProjectConfiguration.scala @@ -0,0 +1,125 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trace.storage.backends.cassandra.config + +import com.datastax.driver.core.ConsistencyLevel +import com.expedia.www.haystack.commons.config.ConfigurationLoader +import com.expedia.www.haystack.commons.retries.RetryOperation +import com.expedia.www.haystack.trace.storage.backends.cassandra.config.entities._ +import com.typesafe.config.Config +import org.apache.commons.lang3.StringUtils + +import scala.collection.JavaConverters._ + +class ProjectConfiguration { + private val config = ConfigurationLoader.loadConfigFileWithEnvOverrides() + + val healthStatusFilePath: String = config.getString("health.status.path") + + val serviceConfig: ServiceConfiguration = { + val serviceConfig = config.getConfig("service") + + val ssl = serviceConfig.getConfig("ssl") + val sslConfig = SslConfiguration(ssl.getBoolean("enabled"), ssl.getString("cert.path"), ssl.getString("private.key.path")) + + ServiceConfiguration(serviceConfig.getInt("port"), sslConfig, serviceConfig.getInt("max.message.size")) + } + /** + * + * cassandra configuration object + */ + val cassandraConfig: CassandraConfiguration = { + + def toConsistencyLevel(level: String) = ConsistencyLevel.values().find(_.toString.equalsIgnoreCase(level)).get + + def consistencyLevelOnErrors(cs: Config) = { + val consistencyLevelOnErrors = cs.getStringList("on.error.consistency.level") + val consistencyLevelOnErrorList = scala.collection.mutable.ListBuffer[(Class[_], ConsistencyLevel)]() + + var idx = 0 + while (idx < consistencyLevelOnErrors.size()) { + val errorClass = consistencyLevelOnErrors.get(idx) + val level = consistencyLevelOnErrors.get(idx + 1) + consistencyLevelOnErrorList.+=((Class.forName(errorClass), toConsistencyLevel(level))) + idx = idx + 2 + } + + consistencyLevelOnErrorList.toList + } + + def keyspaceConfig(kConfig: Config, ttl: Int): KeyspaceConfiguration = { + val autoCreateSchemaField = "auto.create.schema" + val autoCreateSchema = if (kConfig.hasPath(autoCreateSchemaField) + && StringUtils.isNotEmpty(kConfig.getString(autoCreateSchemaField))) { + Some(kConfig.getString(autoCreateSchemaField)) + } else { + None + } + + KeyspaceConfiguration(kConfig.getString("name"), kConfig.getString("table.name"), ttl, autoCreateSchema) + } + + val cs = config.getConfig("cassandra") + + val awsConfig: Option[AwsNodeDiscoveryConfiguration] = + if (cs.hasPath("auto.discovery.aws")) { + val aws = cs.getConfig("auto.discovery.aws") + val tags = aws.getConfig("tags") + .entrySet() + .asScala + .map(elem => elem.getKey -> elem.getValue.unwrapped().toString) + .toMap + Some(AwsNodeDiscoveryConfiguration(aws.getString("region"), tags)) + } else { + None + } + + val credentialsConfig: Option[CredentialsConfiguration] = + if (cs.hasPath("credentials")) { + Some(CredentialsConfiguration(cs.getString("credentials.username"), cs.getString("credentials.password"))) + } else { + None + } + + val socketConfig = cs.getConfig("connections") + + val socket = SocketConfiguration( + socketConfig.getInt("max.per.host"), + socketConfig.getBoolean("keep.alive"), + socketConfig.getInt("conn.timeout.ms"), + socketConfig.getInt("read.timeout.ms")) + + val consistencyLevel = toConsistencyLevel(cs.getString("consistency.level")) + + CassandraConfiguration( + clientConfig = ClientConfiguration( + if (cs.hasPath("endpoints")) cs.getString("endpoints").split(",").toList else Nil, + cs.getBoolean("auto.discovery.enabled"), + awsConfig, + credentialsConfig, + keyspaceConfig(cs.getConfig("keyspace"), cs.getInt("ttl.sec")), + socket), + consistencyLevel = consistencyLevel, + retryConfig = RetryOperation.Config( + cs.getInt("retries.max"), + cs.getLong("retries.backoff.initial.ms"), + cs.getDouble("retries.backoff.factor")), + consistencyLevelOnErrors(cs)) + } + +} diff --git a/traces/backends/cassandra/src/main/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/config/entities/AwsNodeDiscoveryConfiguration.scala b/traces/backends/cassandra/src/main/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/config/entities/AwsNodeDiscoveryConfiguration.scala new file mode 100644 index 000000000..6eda87137 --- /dev/null +++ b/traces/backends/cassandra/src/main/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/config/entities/AwsNodeDiscoveryConfiguration.scala @@ -0,0 +1,25 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trace.storage.backends.cassandra.config.entities + +/** + * defines the parameters required for aws discovery + * @param region aws region e.g. us-east-1, us-west-2 + * @param tags: ec2 tags + */ +case class AwsNodeDiscoveryConfiguration(region: String, tags: Map[String, String]) diff --git a/traces/backends/cassandra/src/main/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/config/entities/ClientConfiguration.scala b/traces/backends/cassandra/src/main/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/config/entities/ClientConfiguration.scala new file mode 100644 index 000000000..c425932dc --- /dev/null +++ b/traces/backends/cassandra/src/main/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/config/entities/ClientConfiguration.scala @@ -0,0 +1,74 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trace.storage.backends.cassandra.config.entities + +import com.datastax.driver.core.ConsistencyLevel +import com.expedia.www.haystack.commons.retries.RetryOperation +import org.apache.commons.lang3.StringUtils + + +/** define the keyspace and table information in cassandra + * + * @param name : name of cassandra keyspace + * @param table : name of cassandra table + * @param recordTTLInSec : ttl of record in sec + * @param autoCreateSchema : apply cql and create keyspace and tables if not exist, optional + */ +case class KeyspaceConfiguration(name: String, + table: String, + recordTTLInSec: Int = -1, + autoCreateSchema: Option[String] = None) { + require(StringUtils.isNotEmpty(name)) + require(StringUtils.isNotEmpty(table)) +} + +/** + * defines the configuration parameters for cassandra client + * + * @param endpoints : list of cassandra endpoints + * @param autoDiscoverEnabled : if autodiscovery is enabled, then 'endpoints' config parameter will be ignored + * @param awsNodeDiscovery : discovery configuration for aws, optional. This is applied only if autoDiscoverEnabled is true + * @param tracesKeyspace : cassandra keyspace for traces + * @param socket : socket configuration like maxConnections, timeouts and keepAlive + */ +case class ClientConfiguration(endpoints: List[String], + autoDiscoverEnabled: Boolean, + awsNodeDiscovery: Option[AwsNodeDiscoveryConfiguration], + plaintextCredentials: Option[CredentialsConfiguration], + tracesKeyspace: KeyspaceConfiguration, + socket: SocketConfiguration) + +/** + * @param consistencyLevel: consistency level of writes + * @param retryConfig retry configuration if writes fail + * @param consistencyLevelOnError: downgraded consistency level on write error + */ +case class CassandraConfiguration(clientConfig: ClientConfiguration, + consistencyLevel: ConsistencyLevel, + retryConfig: RetryOperation.Config, + consistencyLevelOnError: List[(Class[_], ConsistencyLevel)]) { + def writeConsistencyLevel(error: Throwable): ConsistencyLevel = { + if (error == null) { + consistencyLevel + } else { + consistencyLevelOnError + .find(errorClass => errorClass._1.isAssignableFrom(error.getClass)) + .map(_._2).getOrElse(writeConsistencyLevel(error.getCause)) + } + } +} diff --git a/traces/backends/cassandra/src/main/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/config/entities/CredentialsConfiguration.scala b/traces/backends/cassandra/src/main/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/config/entities/CredentialsConfiguration.scala new file mode 100644 index 000000000..b0f74c313 --- /dev/null +++ b/traces/backends/cassandra/src/main/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/config/entities/CredentialsConfiguration.scala @@ -0,0 +1,21 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trace.storage.backends.cassandra.config.entities + +case class CredentialsConfiguration(username: String, + password: String) diff --git a/traces/backends/cassandra/src/main/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/config/entities/ServiceConfiguration.scala b/traces/backends/cassandra/src/main/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/config/entities/ServiceConfiguration.scala new file mode 100644 index 000000000..2f574e5c1 --- /dev/null +++ b/traces/backends/cassandra/src/main/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/config/entities/ServiceConfiguration.scala @@ -0,0 +1,23 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expedia.www.haystack.trace.storage.backends.cassandra.config.entities + +/** + * @param port port to start grpc servicer on + */ +case class ServiceConfiguration(port: Int, ssl: SslConfiguration, maxSizeInBytes: Int) +case class SslConfiguration(enabled: Boolean, certChainFilePath: String, privateKeyPath: String) diff --git a/traces/backends/cassandra/src/main/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/config/entities/SocketConfiguration.scala b/traces/backends/cassandra/src/main/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/config/entities/SocketConfiguration.scala new file mode 100644 index 000000000..147f38efa --- /dev/null +++ b/traces/backends/cassandra/src/main/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/config/entities/SocketConfiguration.scala @@ -0,0 +1,23 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trace.storage.backends.cassandra.config.entities + +case class SocketConfiguration(maxConnectionPerHost: Int, + keepAlive: Boolean, + connectionTimeoutMillis: Int, + readTimeoutMills: Int) diff --git a/traces/backends/cassandra/src/main/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/metrics/AppMetricNames.scala b/traces/backends/cassandra/src/main/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/metrics/AppMetricNames.scala new file mode 100644 index 000000000..bc1166fac --- /dev/null +++ b/traces/backends/cassandra/src/main/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/metrics/AppMetricNames.scala @@ -0,0 +1,26 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trace.storage.backends.cassandra.metrics + +object AppMetricNames { + val CASSANDRA_READ_TIME = "cassandra.read.time" + val CASSANDRA_READ_FAILURES = "cassandra.read.failures" + val CASSANDRA_WRITE_TIME = "cassandra.write.time" + val CASSANDRA_WRITE_FAILURE = "cassandra.write.failure" + val CASSANDRA_WRITE_WARNINGS = "cassandra.write.warnings" +} diff --git a/traces/backends/cassandra/src/main/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/services/GrpcHandler.scala b/traces/backends/cassandra/src/main/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/services/GrpcHandler.scala new file mode 100644 index 000000000..72a5b4003 --- /dev/null +++ b/traces/backends/cassandra/src/main/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/services/GrpcHandler.scala @@ -0,0 +1,63 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expedia.www.haystack.trace.storage.backends.cassandra.services + +import com.expedia.www.haystack.commons.metrics.MetricsSupport +import com.expedia.www.haystack.trace.storage.backends.cassandra.services.GrpcHandler._ +import com.google.protobuf.GeneratedMessageV3 +import io.grpc.Status +import io.grpc.stub.StreamObserver +import org.slf4j.{Logger, LoggerFactory} + +import scala.concurrent.{ExecutionContextExecutor, Future} +import scala.util.{Failure, Success} + +object GrpcHandler { + protected val LOGGER: Logger = LoggerFactory.getLogger(classOf[GrpcHandler]) +} + +/** + * Handler for Grpc response + * populates responseObserver with response object or error accordingly + * takes care of corresponding logging and updating counters + * + * @param operationName : name of operation + * @param executor : executor service on which handler is invoked + */ + +class GrpcHandler(operationName: String)(implicit val executor: ExecutionContextExecutor) extends MetricsSupport { + private val metricFriendlyOperationName = operationName.replace('/', '.') + private val timer = metricRegistry.timer(metricFriendlyOperationName) + private val failureMeter = metricRegistry.meter(s"$metricFriendlyOperationName.failures") + + def handle[Rs](request: GeneratedMessageV3, responseObserver: StreamObserver[Rs])(op: => Future[Rs]): Unit = { + val time = timer.time() + op onComplete { + case Success(response) => + responseObserver.onNext(response) + responseObserver.onCompleted() + time.stop() + LOGGER.debug(s"service invocation for operation=$operationName and request=${request.toString} completed successfully") + + case Failure(ex) => + responseObserver.onError(Status.fromThrowable(ex).asRuntimeException()) + failureMeter.mark() + time.stop() + LOGGER.debug(s"service invocation for operation=$operationName and request=${request.toString} failed with error", ex) + } + } +} diff --git a/traces/backends/cassandra/src/main/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/services/GrpcHealthService.scala b/traces/backends/cassandra/src/main/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/services/GrpcHealthService.scala new file mode 100644 index 000000000..966526f62 --- /dev/null +++ b/traces/backends/cassandra/src/main/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/services/GrpcHealthService.scala @@ -0,0 +1,31 @@ +/* + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expedia.www.haystack.trace.storage.backends.cassandra.services + +import io.grpc.health.v1.{HealthCheckRequest, HealthCheckResponse, HealthGrpc} +import io.grpc.stub.StreamObserver + +class GrpcHealthService extends HealthGrpc.HealthImplBase { + + override def check(request: HealthCheckRequest, responseObserver: StreamObserver[HealthCheckResponse]): Unit = { + responseObserver.onNext(HealthCheckResponse + .newBuilder() + .setStatus(HealthCheckResponse.ServingStatus.SERVING) + .build()) + responseObserver.onCompleted() + } +} diff --git a/traces/backends/cassandra/src/main/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/services/SpansPersistenceService.scala b/traces/backends/cassandra/src/main/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/services/SpansPersistenceService.scala new file mode 100644 index 000000000..0a99e4ba4 --- /dev/null +++ b/traces/backends/cassandra/src/main/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/services/SpansPersistenceService.scala @@ -0,0 +1,58 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expedia.www.haystack.trace.storage.backends.cassandra.services + +import com.expedia.open.tracing.backend.WriteSpansResponse.ResultCode +import com.expedia.open.tracing.backend._ +import com.expedia.www.haystack.trace.storage.backends.cassandra.store.{CassandraTraceRecordReader, CassandraTraceRecordWriter} +import io.grpc.stub.StreamObserver + +import scala.collection.JavaConverters._ +import scala.concurrent.ExecutionContextExecutor + +class SpansPersistenceService(reader: CassandraTraceRecordReader, + writer: CassandraTraceRecordWriter) + (implicit val executor: ExecutionContextExecutor) extends StorageBackendGrpc.StorageBackendImplBase { + + private val handleReadSpansResponse = new GrpcHandler(StorageBackendGrpc.METHOD_READ_SPANS.getFullMethodName) + private val handleWriteSpansResponse = new GrpcHandler(StorageBackendGrpc.METHOD_WRITE_SPANS.getFullMethodName) + + override def writeSpans(request: WriteSpansRequest, responseObserver: StreamObserver[WriteSpansResponse]): Unit = { + handleWriteSpansResponse.handle(request, responseObserver) { + writer.writeTraceRecords(request.getRecordsList.asScala.toList) map (_ => + WriteSpansResponse.newBuilder().setCode(ResultCode.SUCCESS).build()) + } + } + + /** + *

+    * read buffered spans from backend
+    * 
+ */ + override def readSpans(request: ReadSpansRequest, responseObserver: StreamObserver[ReadSpansResponse]): Unit = { + + handleReadSpansResponse.handle(request, responseObserver) { + reader.readTraceRecords(request.getTraceIdsList.iterator().asScala.toList).map { + records => { + ReadSpansResponse.newBuilder() + .addAllRecords(records.asJava) + .build() + } + } + } + } +} diff --git a/traces/backends/cassandra/src/main/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/store/CassandraTraceRecordReadResultListener.scala b/traces/backends/cassandra/src/main/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/store/CassandraTraceRecordReadResultListener.scala new file mode 100644 index 000000000..c4fb0c08d --- /dev/null +++ b/traces/backends/cassandra/src/main/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/store/CassandraTraceRecordReadResultListener.scala @@ -0,0 +1,87 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expedia.www.haystack.trace.storage.backends.cassandra.store + +import com.codahale.metrics.{Meter, Timer} +import com.datastax.driver.core.exceptions.NoHostAvailableException +import com.datastax.driver.core.{ResultSet, ResultSetFuture, Row} +import com.expedia.open.tracing.api.Trace +import com.expedia.open.tracing.backend.TraceRecord +import com.expedia.www.haystack.trace.storage.backends.cassandra.client.CassandraTableSchema +import com.google.protobuf.ByteString +import org.slf4j.{Logger, LoggerFactory} + +import scala.collection.JavaConverters._ +import scala.concurrent.Promise +import scala.util.{Failure, Success, Try} + +object CassandraTraceRecordReadResultListener { + protected val LOGGER: Logger = LoggerFactory.getLogger(classOf[CassandraTraceRecordReadResultListener]) +} + +class CassandraTraceRecordReadResultListener(asyncResult: ResultSetFuture, + timer: Timer.Context, + failure: Meter, + promise: Promise[Seq[TraceRecord]]) extends Runnable { + + import CassandraTraceRecordReadResultListener._ + + override def run(): Unit = { + timer.close() + + Try(asyncResult.get) + .flatMap(tryGetTraceRows) + .flatMap(mapTraceRecords) + match { + case Success(records) => + promise.success(records) + case Failure(ex) => + if (fatalError(ex)) { + LOGGER.error("Fatal error in reading from cassandra, tearing down the app", ex) + } else { + LOGGER.error("Failed in reading the record from cassandra", ex) + } + failure.mark() + promise.failure(ex) + } + } + + private def fatalError(ex: Throwable): Boolean = { + if (ex.isInstanceOf[NoHostAvailableException]) true else ex.getCause != null && fatalError(ex.getCause) + } + + private def tryGetTraceRows(resultSet: ResultSet): Try[Seq[Row]] = { + val rows = resultSet.all().asScala + if (rows.isEmpty) Failure(new RuntimeException()) else Success(rows) + } + + private def mapTraceRecords(rows: Seq[Row]): Try[List[TraceRecord]] = { + Try { + rows.map(row => { + val spanBytes = row.getBytes(CassandraTableSchema.SPANS_COLUMN_NAME).array() + val timeStamp = row.getLong(CassandraTableSchema.TIMESTAMP_COLUMN_NAME) + val traceId = row.getString(CassandraTableSchema.ID_COLUMN_NAME) + val record = TraceRecord.newBuilder() + .setSpans(ByteString.copyFrom(spanBytes)) + .setTimestamp(timeStamp) + .setTraceId(traceId) + .build() + record + }).toList + } + } +} diff --git a/traces/backends/cassandra/src/main/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/store/CassandraTraceRecordReader.scala b/traces/backends/cassandra/src/main/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/store/CassandraTraceRecordReader.scala new file mode 100644 index 000000000..b99a93c70 --- /dev/null +++ b/traces/backends/cassandra/src/main/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/store/CassandraTraceRecordReader.scala @@ -0,0 +1,52 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expedia.www.haystack.trace.storage.backends.cassandra.store + +import com.expedia.open.tracing.backend.TraceRecord +import com.expedia.www.haystack.commons.metrics.MetricsSupport +import com.expedia.www.haystack.trace.storage.backends.cassandra.client.CassandraSession +import com.expedia.www.haystack.trace.storage.backends.cassandra.config.entities.ClientConfiguration +import com.expedia.www.haystack.trace.storage.backends.cassandra.metrics.AppMetricNames +import org.slf4j.LoggerFactory + +import scala.concurrent.{ExecutionContextExecutor, Future, Promise} + +class CassandraTraceRecordReader(cassandra: CassandraSession, config: ClientConfiguration) + (implicit val dispatcher: ExecutionContextExecutor) extends MetricsSupport { + private val LOGGER = LoggerFactory.getLogger(classOf[CassandraTraceRecordReader]) + + private lazy val readTimer = metricRegistry.timer(AppMetricNames.CASSANDRA_READ_TIME) + private lazy val readFailures = metricRegistry.meter(AppMetricNames.CASSANDRA_READ_FAILURES) + + def readTraceRecords(traceIds: List[String]): Future[Seq[TraceRecord]] = { + val timer = readTimer.time() + val promise = Promise[Seq[TraceRecord]] + + try { + val statement = cassandra.newSelectRawTracesBoundStatement(traceIds) + val asyncResult = cassandra.executeAsync(statement) + asyncResult.addListener(new CassandraTraceRecordReadResultListener(asyncResult, timer, readFailures, promise), dispatcher) + promise.future + } catch { + case ex: Exception => + readFailures.mark() + timer.stop() + LOGGER.error("Failed to read raw traces with exception", ex) + Future.failed(ex) + } + } +} \ No newline at end of file diff --git a/traces/backends/cassandra/src/main/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/store/CassandraTraceRecordWriteResultListener.scala b/traces/backends/cassandra/src/main/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/store/CassandraTraceRecordWriteResultListener.scala new file mode 100644 index 000000000..f180fc4ab --- /dev/null +++ b/traces/backends/cassandra/src/main/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/store/CassandraTraceRecordWriteResultListener.scala @@ -0,0 +1,65 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trace.storage.backends.cassandra.store + +import com.codahale.metrics.{Meter, Timer} +import com.datastax.driver.core.ResultSetFuture +import com.expedia.www.haystack.commons.metrics.MetricsSupport +import com.expedia.www.haystack.commons.retries.RetryOperation +import com.expedia.www.haystack.trace.storage.backends.cassandra.metrics.AppMetricNames +import org.slf4j.{Logger, LoggerFactory} + +import scala.collection.JavaConverters._ + +object CassandraTraceRecordWriteResultListener extends MetricsSupport { + protected val LOGGER: Logger = LoggerFactory.getLogger(CassandraTraceRecordWriteResultListener.getClass) + protected val writeFailures: Meter = metricRegistry.meter(AppMetricNames.CASSANDRA_WRITE_FAILURE) + protected val writeWarnings: Meter = metricRegistry.meter(AppMetricNames.CASSANDRA_WRITE_WARNINGS) +} + +class CassandraTraceRecordWriteResultListener(asyncResult: ResultSetFuture, + timer: Timer.Context, + retryOp: RetryOperation.Callback) extends Runnable { + + import CassandraTraceRecordWriteResultListener._ + + /** + * this is invoked when the cassandra aysnc write completes. + * We measure the time write operation takes and records any warnings or errors + */ + override def run(): Unit = { + try { + timer.close() + + val result = asyncResult.get() + if (result != null && + result.getExecutionInfo != null && + result.getExecutionInfo.getWarnings != null && + !result.getExecutionInfo.getWarnings.isEmpty) { + LOGGER.warn(s"Warning received in cassandra writes {}", result.getExecutionInfo.getWarnings.asScala.mkString(",")) + writeWarnings.mark(result.getExecutionInfo.getWarnings.size()) + } + if (retryOp != null) retryOp.onResult(result) + } catch { + case ex: Exception => + LOGGER.error("Fail to write the record to cassandra with exception", ex) + writeFailures.mark() + if (retryOp != null) retryOp.onError(ex, retry = true) + } + } +} \ No newline at end of file diff --git a/traces/backends/cassandra/src/main/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/store/CassandraTraceRecordWriter.scala b/traces/backends/cassandra/src/main/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/store/CassandraTraceRecordWriter.scala new file mode 100644 index 000000000..edb25c664 --- /dev/null +++ b/traces/backends/cassandra/src/main/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/store/CassandraTraceRecordWriter.scala @@ -0,0 +1,97 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trace.storage.backends.cassandra.store + +import java.util.concurrent.atomic.AtomicInteger + +import com.expedia.open.tracing.backend.TraceRecord +import com.expedia.www.haystack.commons.metrics.MetricsSupport +import com.expedia.www.haystack.commons.retries.RetryOperation._ +import com.expedia.www.haystack.trace.storage.backends.cassandra.client.CassandraSession +import com.expedia.www.haystack.trace.storage.backends.cassandra.config.entities.CassandraConfiguration +import com.expedia.www.haystack.trace.storage.backends.cassandra.metrics.AppMetricNames +import org.slf4j.LoggerFactory + +import scala.concurrent.{ExecutionContextExecutor, Future, Promise} +import scala.util.{Failure, Success} + +class CassandraTraceRecordWriter(cassandra: CassandraSession, + config: CassandraConfiguration)(implicit val dispatcher: ExecutionContextExecutor) + extends MetricsSupport { + + private val LOGGER = LoggerFactory.getLogger(classOf[CassandraTraceRecordWriter]) + private lazy val writeTimer = metricRegistry.timer(AppMetricNames.CASSANDRA_WRITE_TIME) + private lazy val writeFailures = metricRegistry.meter(AppMetricNames.CASSANDRA_WRITE_FAILURE) + + cassandra.ensureKeyspace(config.clientConfig.tracesKeyspace) + private val spanInsertPreparedStmt = cassandra.createSpanInsertPreparedStatement(config.clientConfig.tracesKeyspace) + + private def execute(record: TraceRecord): Future[Unit] = { + + val promise = Promise[Unit] + // execute the request async with retry + withRetryBackoff(retryCallback => { + val timer = writeTimer.time() + + // prepare the statement + val statement = cassandra.newTraceInsertBoundStatement(record.getTraceId, + record.getSpans.toByteArray, + config.writeConsistencyLevel(retryCallback.lastError()), + spanInsertPreparedStmt) + + val asyncResult = cassandra.executeAsync(statement) + asyncResult.addListener(new CassandraTraceRecordWriteResultListener(asyncResult, timer, retryCallback), dispatcher) + }, + config.retryConfig, + onSuccess = (_: Any) => promise.success(), + onFailure = ex => { + writeFailures.mark() + LOGGER.error(s"Fail to write to cassandra after ${config.retryConfig.maxRetries} retry attempts for ${record.getTraceId}", ex) + promise.failure(ex) + }) + promise.future + } + + /** + * writes the traceId and its spans to cassandra. Use the current timestamp as the sort key for the writes to same + * TraceId. Also if the parallel writes exceed the max inflight requests, then we block and this puts backpressure on + * upstream + * + * @param traceRecords : trace records which need to be written + * @return + */ + def writeTraceRecords(traceRecords: List[TraceRecord]): Future[Unit] = { + val promise = Promise[Unit] + val writableRecordsLatch = new AtomicInteger(traceRecords.size) + traceRecords.foreach(record => { + /* write spanBuffer for a given traceId */ + execute(record).onComplete { + case Success(_) => if (writableRecordsLatch.decrementAndGet() == 0) { + promise.success() + } + case Failure(ex) => + //TODO: We fail the response only if the last cassandra write fails, ideally we should be failing if any of the cassandra writes fail + if (writableRecordsLatch.decrementAndGet() == 0) { + promise.failure(ex) + } + } + }) + promise.future + + } +} diff --git a/traces/backends/cassandra/src/test/resources/config/base.conf b/traces/backends/cassandra/src/test/resources/config/base.conf new file mode 100644 index 000000000..e0e4bef90 --- /dev/null +++ b/traces/backends/cassandra/src/test/resources/config/base.conf @@ -0,0 +1,52 @@ +haystack.graphite.host = "monitoring-influxdb-graphite.kube-system.svc" + +service { + port = 8090 + ssl { + enabled = false + cert.path = "/ssl/cert" + private.key.path = "/ssl/private-key" + } + max.message.size = 52428800 # 50MB in bytes +} + +cassandra { + # multiple endpoints can be provided as comma separated list + endpoints = "cassandra" + + # if auto.discovery.enabled is true, we ignore the manually supplied endpoints(above) + auto.discovery { + enabled: false + ## optional AWS discovery + # aws: { + # region: "us-west-2" + # tags: { + # name: "cassandra" + # } + # } + } + + connections { + max.per.host = 100 + read.timeout.ms = 5000 + conn.timeout.ms = 10000 + keep.alive = true + } + ttl.sec = 86400 + + retries { + max = 10 + backoff { + initial.ms = 250 + factor = 2 + } + } + + keyspace { + name = "haystack" + table.name = "traces" + auto.create.schema = "CREATE KEYSPACE IF NOT EXISTS haystack WITH REPLICATION = { 'class': 'SimpleStrategy', 'replication_factor' : 1 } AND durable_writes = false; CREATE TABLE IF NOT EXISTS haystack.traces (id varchar, ts timestamp, spans blob, PRIMARY KEY ((id), ts)) WITH CLUSTERING ORDER BY (ts ASC) AND compaction = { 'class' : 'DateTieredCompactionStrategy', 'max_sstable_age_days': '3' } AND gc_grace_seconds = 86400;" + + + } +} diff --git a/traces/backends/cassandra/src/test/resources/logback-test.xml b/traces/backends/cassandra/src/test/resources/logback-test.xml new file mode 100644 index 000000000..298193e01 --- /dev/null +++ b/traces/backends/cassandra/src/test/resources/logback-test.xml @@ -0,0 +1 @@ + diff --git a/traces/backends/cassandra/src/test/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/integration/BaseIntegrationTestSpec.scala b/traces/backends/cassandra/src/test/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/integration/BaseIntegrationTestSpec.scala new file mode 100644 index 000000000..159c950ad --- /dev/null +++ b/traces/backends/cassandra/src/test/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/integration/BaseIntegrationTestSpec.scala @@ -0,0 +1,139 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expedia.www.haystack.trace.storage.backends.cassandra.integration + +import java.nio.ByteBuffer +import java.util.concurrent.Executors +import java.util.{Date, UUID} + +import com.datastax.driver.core.querybuilder.QueryBuilder +import com.datastax.driver.core.{Cluster, ResultSet, Session, SimpleStatement} +import com.expedia.open.tracing.Span +import com.expedia.open.tracing.backend.{StorageBackendGrpc, TraceRecord} +import com.expedia.open.tracing.buffer.SpanBuffer +import com.expedia.www.haystack.trace.storage.backends.cassandra.Service +import com.expedia.www.haystack.trace.storage.backends.cassandra.client.CassandraTableSchema +import com.google.protobuf.ByteString +import io.grpc.ManagedChannelBuilder +import io.grpc.health.v1.HealthGrpc +import org.scalatest._ + +import scala.collection.JavaConverters._ + +trait BaseIntegrationTestSpec extends FunSpec with GivenWhenThen with Matchers with BeforeAndAfterAll with BeforeAndAfterEach { + protected var client: StorageBackendGrpc.StorageBackendBlockingStub = _ + + protected var healthCheckClient: HealthGrpc.HealthBlockingStub = _ + private val CASSANDRA_ENDPOINT = "cassandra" + private val CASSANDRA_KEYSPACE = "haystack" + private val CASSANDRA_TABLE = "traces" + + private val executors = Executors.newSingleThreadExecutor() + + private var cassandraSession: Session = _ + + override def beforeAll() { + executors.submit(new Runnable { + override def run(): Unit = Service.main(null) + }) + //waiting for the service to start up + + Thread.sleep(5000) + // setup cassandra + cassandraSession = Cluster + .builder() + .addContactPoints(CASSANDRA_ENDPOINT) + .build() + .connect(CASSANDRA_KEYSPACE) + deleteCassandraTableRows() + client = StorageBackendGrpc.newBlockingStub(ManagedChannelBuilder.forAddress("localhost", 8090) + .usePlaintext(true) + .build()) + + healthCheckClient = HealthGrpc.newBlockingStub(ManagedChannelBuilder.forAddress("localhost", 8090) + .usePlaintext(true) + .build()) + } + + private def deleteCassandraTableRows(): Unit = { + cassandraSession.execute(new SimpleStatement(s"TRUNCATE $CASSANDRA_TABLE")) + } + + protected def putTraceInCassandra(traceId: String = UUID.randomUUID().toString, + spanId: String = UUID.randomUUID().toString, + serviceName: String = "", + operationName: String = "", + tags: Map[String, String] = Map.empty, + startTime: Long = System.currentTimeMillis() * 1000, + sleep: Boolean = true): Unit = { + insertTraceInCassandra(traceId, spanId, serviceName, operationName, tags, startTime) + // wait for few sec to let ES refresh its index + if (sleep) Thread.sleep(5000) + } + + protected def createTraceRecord(traceId: String = UUID.randomUUID().toString, + ): TraceRecord = { + val spans = "random span".getBytes + TraceRecord + .newBuilder() + .setTraceId(traceId) + .setTimestamp(System.currentTimeMillis()) + .setSpans(ByteString.copyFrom(spans)).build() + } + + private def insertTraceInCassandra(traceId: String, + spanId: String, + serviceName: String, + operationName: String, + tags: Map[String, String], + startTime: Long): ResultSet = { + val spanBuffer = createSpanBufferWithSingleSpan(traceId, spanId, serviceName, operationName, tags, startTime) + writeToCassandra(spanBuffer, traceId) + } + + private def writeToCassandra(spanBuffer: SpanBuffer, traceId: String) = { + + cassandraSession.execute(QueryBuilder + .insertInto(CASSANDRA_TABLE) + .value(CassandraTableSchema.ID_COLUMN_NAME, traceId) + .value(CassandraTableSchema.TIMESTAMP_COLUMN_NAME, new Date()) + .value(CassandraTableSchema.SPANS_COLUMN_NAME, ByteBuffer.wrap(spanBuffer.toByteArray))) + } + + private def createSpanBufferWithSingleSpan(traceId: String, + spanId: String, + serviceName: String, + operationName: String, + tags: Map[String, String], + startTime: Long) = { + val spanTags = tags.map(tag => com.expedia.open.tracing.Tag.newBuilder().setKey(tag._1).setVStr(tag._2).build()) + + SpanBuffer + .newBuilder() + .setTraceId(traceId) + .addChildSpans(Span + .newBuilder() + .setTraceId(traceId) + .setSpanId(spanId) + .setOperationName(operationName) + .setServiceName(serviceName) + .setStartTime(startTime) + .addAllTags(spanTags.asJava) + .build()) + .build() + } +} diff --git a/traces/backends/cassandra/src/test/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/integration/CassandraStorageBackendServiceIntegrationTestSpec.scala b/traces/backends/cassandra/src/test/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/integration/CassandraStorageBackendServiceIntegrationTestSpec.scala new file mode 100644 index 000000000..f9b1cecad --- /dev/null +++ b/traces/backends/cassandra/src/test/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/integration/CassandraStorageBackendServiceIntegrationTestSpec.scala @@ -0,0 +1,61 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expedia.www.haystack.trace.storage.backends.cassandra.integration + +import java.util.UUID + +import com.expedia.open.tracing.backend.{ReadSpansRequest, WriteSpansRequest} + +class CassandraStorageBackendServiceIntegrationTestSpec extends BaseIntegrationTestSpec { + + + describe("Cassandra Persistence Service read trace records") { + it("should get trace records for given traceID from cassandra") { + Given("trace in cassandra") + val traceId = UUID.randomUUID().toString + putTraceInCassandra(traceId) + + val readSpansRequest = ReadSpansRequest.newBuilder().addTraceIds(traceId).build() + + When("readspans is invoked") + val traceRecords = client.readSpans(readSpansRequest) + + Then("should return the trace") + traceRecords.getRecordsList should not be empty + traceRecords.getRecordsCount shouldEqual 1 + traceRecords.getRecordsList.get(0).getTraceId shouldEqual traceId + } + it("should write trace records for given traceID to cassandra") { + Given("trace in cassandra") + val traceId = UUID.randomUUID().toString + val record = createTraceRecord(traceId) + val writeSpansRequest = WriteSpansRequest.newBuilder().addRecords(record).build() + + When("writespans is invoked") + val traceRecords = client.writeSpans(writeSpansRequest) + + Then("should write the trace") + val readSpansRequest = ReadSpansRequest.newBuilder().addTraceIds(traceId).build() + val retrievedRecord = client.readSpans(readSpansRequest) + + retrievedRecord.getRecordsList should not be empty + retrievedRecord.getRecordsCount shouldEqual 1 + retrievedRecord.getRecordsList.get(0).getTraceId shouldEqual traceId + } + + } +} diff --git a/traces/backends/cassandra/src/test/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/unit/BaseUnitTestSpec.scala b/traces/backends/cassandra/src/test/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/unit/BaseUnitTestSpec.scala new file mode 100644 index 000000000..3f06b318b --- /dev/null +++ b/traces/backends/cassandra/src/test/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/unit/BaseUnitTestSpec.scala @@ -0,0 +1,22 @@ +/* + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expedia.www.haystack.trace.storage.backends.cassandra.unit + +import org.scalatest.{FunSpec, GivenWhenThen, Matchers} +import org.scalatest.easymock.EasyMockSugar + +trait BaseUnitTestSpec extends FunSpec with GivenWhenThen with Matchers with EasyMockSugar diff --git a/traces/backends/cassandra/src/test/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/unit/client/AwsNodeDiscovererSpec.scala b/traces/backends/cassandra/src/test/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/unit/client/AwsNodeDiscovererSpec.scala new file mode 100644 index 000000000..f70f3fe21 --- /dev/null +++ b/traces/backends/cassandra/src/test/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/unit/client/AwsNodeDiscovererSpec.scala @@ -0,0 +1,55 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trace.storage.backends.cassandra.unit.client + +import com.amazonaws.services.ec2.AmazonEC2Client +import com.amazonaws.services.ec2.model._ +import com.expedia.www.haystack.trace.storage.backends.cassandra.client.AwsNodeDiscoverer +import org.easymock.EasyMock +import org.scalatest.easymock.EasyMockSugar +import org.scalatest.{FunSpec, Matchers} + +import scala.collection.JavaConverters._ + +class AwsNodeDiscovererSpec extends FunSpec with Matchers with EasyMockSugar { + describe("AWS node discovery") { + it("should return only the running nodes for given ec2 tags") { + val client = mock[AmazonEC2Client] + val ec2Tags = Map("name" -> "cassandra") + + val instance_1 = new Instance().withPrivateIpAddress("10.0.0.1").withState(new InstanceState().withName(InstanceStateName.Running)) + val instance_2 = new Instance().withPrivateIpAddress("10.0.0.2").withState(new InstanceState().withName(InstanceStateName.Running)) + val instance_3 = new Instance().withPrivateIpAddress("10.0.0.3").withState(new InstanceState().withName(InstanceStateName.Terminated)) + val reservation = new Reservation().withInstances(instance_1, instance_2, instance_3) + + val capturedRequest = EasyMock.newCapture[DescribeInstancesRequest]() + expecting { + client.describeInstances(EasyMock.capture(capturedRequest)).andReturn(new DescribeInstancesResult().withReservations(reservation)) + } + + whenExecuting(client) { + val ips = AwsNodeDiscoverer.discover(client, ec2Tags) + ips should contain allOf ("10.0.0.1", "10.0.0.2") + capturedRequest.getValue.getFilters.asScala.foreach(filter => { + filter.getName shouldEqual "tag:name" + filter.getValues.asScala.head shouldEqual "cassandra" + }) + } + } + } +} \ No newline at end of file diff --git a/traces/backends/cassandra/src/test/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/unit/client/CassandraSessionSpec.scala b/traces/backends/cassandra/src/test/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/unit/client/CassandraSessionSpec.scala new file mode 100644 index 000000000..76fd4ec54 --- /dev/null +++ b/traces/backends/cassandra/src/test/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/unit/client/CassandraSessionSpec.scala @@ -0,0 +1,116 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trace.storage.backends.cassandra.unit.client + +import com.datastax.driver.core._ +import com.datastax.driver.core.querybuilder.{Insert, Select} +import com.expedia.www.haystack.trace.storage.backends.cassandra.client.{CassandraClusterFactory, CassandraSession} +import com.expedia.www.haystack.trace.storage.backends.cassandra.config.entities.{ClientConfiguration, KeyspaceConfiguration, SocketConfiguration} +import org.easymock.EasyMock +import org.scalatest.easymock.EasyMockSugar +import org.scalatest.{FunSpec, Matchers} + +class CassandraSessionSpec extends FunSpec with Matchers with EasyMockSugar { + describe("Cassandra Session") { + it("should connect to the cassandra cluster and provide prepared statement for inserts") { + val keyspaceName = "keyspace-1" + val tableName = "table-1" + + val factory = mock[CassandraClusterFactory] + val session = mock[Session] + val cluster = mock[Cluster] + val metadata = mock[Metadata] + val keyspaceMetadata = mock[KeyspaceMetadata] + val tableMetadata = mock[TableMetadata] + val insertPrepStatement = mock[PreparedStatement] + val keyspaceConfig = KeyspaceConfiguration(keyspaceName, tableName, 100, None) + + val config = ClientConfiguration(List("cassandra1"), + autoDiscoverEnabled = false, + None, + None, + keyspaceConfig, + SocketConfiguration(10, keepAlive = true, 1000, 1000)) + + val captured = EasyMock.newCapture[Insert.Options]() + expecting { + factory.buildCluster(config).andReturn(cluster).once() + cluster.connect().andReturn(session).once() + keyspaceMetadata.getTable(tableName).andReturn(tableMetadata).once() + metadata.getKeyspace(keyspaceName).andReturn(keyspaceMetadata).once() + cluster.getMetadata.andReturn(metadata).once() + session.getCluster.andReturn(cluster).once() + session.prepare(EasyMock.capture(captured)).andReturn(insertPrepStatement).anyTimes() + session.close().once() + cluster.close().once() + } + + whenExecuting(factory, cluster, session, metadata, keyspaceMetadata, tableMetadata, insertPrepStatement) { + val session = new CassandraSession(config, factory) + session.ensureKeyspace(config.tracesKeyspace) + val stmt = session.createSpanInsertPreparedStatement(keyspaceConfig) + stmt shouldBe insertPrepStatement + captured.getValue.getQueryString() shouldEqual "INSERT INTO \"keyspace-1\".\"table-1\" (id,ts,spans) VALUES (:id,:ts,:spans) USING TTL 100;" + session.close() + } + } + + it("should connect to the cassandra cluster and provide prepared statement for select with traces") { + val keyspaceName = "keyspace-1" + val tableName = "table-1" + + val factory = mock[CassandraClusterFactory] + val session = mock[Session] + val cluster = mock[Cluster] + val metadata = mock[Metadata] + val keyspaceMetadata = mock[KeyspaceMetadata] + val tableMetadata = mock[TableMetadata] + val selectPrepStatement = mock[PreparedStatement] + val keyspaceConfig = KeyspaceConfiguration(keyspaceName, tableName, 100, None) + + val config = ClientConfiguration(List("cassandra1"), + autoDiscoverEnabled = false, + None, + None, + keyspaceConfig, + SocketConfiguration(10, keepAlive = true, 1000, 1000)) + + val captured = EasyMock.newCapture[Select.Where]() + expecting { + factory.buildCluster(config).andReturn(cluster).once() + cluster.connect().andReturn(session).once() + keyspaceMetadata.getTable(tableName).andReturn(tableMetadata).once() + metadata.getKeyspace(keyspaceName).andReturn(keyspaceMetadata).once() + cluster.getMetadata.andReturn(metadata).once() + session.getCluster.andReturn(cluster).once() + session.prepare(EasyMock.capture(captured)).andReturn(selectPrepStatement).anyTimes() + session.close().once() + cluster.close().once() + } + + whenExecuting(factory, cluster, session, metadata, keyspaceMetadata, tableMetadata, selectPrepStatement) { + val session = new CassandraSession(config, factory) + session.ensureKeyspace(config.tracesKeyspace) + val stmt = session.selectRawTracesPreparedStmt + stmt shouldBe selectPrepStatement + captured.getValue.getQueryString() shouldEqual "SELECT * FROM \"keyspace-1\".\"table-1\" WHERE id IN :id;" + session.close() + } + } + } +} diff --git a/traces/backends/cassandra/src/test/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/unit/client/CassandraTableSchemaSpec.scala b/traces/backends/cassandra/src/test/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/unit/client/CassandraTableSchemaSpec.scala new file mode 100644 index 000000000..aad03ad6a --- /dev/null +++ b/traces/backends/cassandra/src/test/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/unit/client/CassandraTableSchemaSpec.scala @@ -0,0 +1,133 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trace.storage.backends.cassandra.unit.client + +import com.datastax.driver.core._ +import com.expedia.www.haystack.trace.storage.backends.cassandra.client.CassandraTableSchema +import org.scalatest.easymock.EasyMockSugar +import org.scalatest.{FunSpec, Matchers} + +class CassandraTableSchemaSpec extends FunSpec with Matchers with EasyMockSugar { + + + it("should apply the schema if table does not exist in cassandra") { + val session = mock[Session] + val cluster = mock[Cluster] + val metadata = mock[Metadata] + val keyspaceMetadata = mock[KeyspaceMetadata] + val keyspace = "my-keyspace" + val cassandraTableName = "my-table" + + expecting { + session.execute("apply schema").andReturn(null).once + keyspaceMetadata.getTable(cassandraTableName).andReturn(null).once() + metadata.getKeyspace(keyspace).andReturn(keyspaceMetadata).once() + cluster.getMetadata.andReturn(metadata).once() + session.getCluster.andReturn(cluster).once() + } + whenExecuting(session, cluster, metadata, keyspaceMetadata) { + CassandraTableSchema.ensureExists(keyspace, cassandraTableName, Some("apply schema"), session) + } + } + + it("should apply the schema if keyspace and table does not exist in cassandra") { + val session = mock[Session] + val cluster = mock[Cluster] + val metadata = mock[Metadata] + val keyspace = "my-keyspace" + val cassandraTableName = "my-table" + + expecting { + session.execute("apply schema").andReturn(null).once + session.execute("apply schema2").andReturn(null).once + metadata.getKeyspace(keyspace).andReturn(null).once() + cluster.getMetadata.andReturn(metadata).once() + session.getCluster.andReturn(cluster).once() + } + whenExecuting(session, cluster, metadata) { + CassandraTableSchema.ensureExists(keyspace, cassandraTableName, Some("apply schema;apply schema2"), session) + } + } + + it("should not apply the schema if keyspace and table both exists in cassandra") { + val session = mock[Session] + val cluster = mock[Cluster] + val metadata = mock[Metadata] + val keyspaceMetadata = mock[KeyspaceMetadata] + val tableMetadata = mock[TableMetadata] + + val keyspace = "my-keyspace" + val cassandraTableName = "my-table" + + expecting { + keyspaceMetadata.getTable(cassandraTableName).andReturn(tableMetadata).once() + metadata.getKeyspace(keyspace).andReturn(keyspaceMetadata).once() + cluster.getMetadata.andReturn(metadata).once() + session.getCluster.andReturn(cluster).once() + } + whenExecuting(session, cluster, metadata, keyspaceMetadata, tableMetadata) { + CassandraTableSchema.ensureExists(keyspace, cassandraTableName, Some("apply schema"), session) + } + } + + it("should throw an exception if keyspace and table does not exists in cassandra and no schema is applied") { + val session = mock[Session] + val cluster = mock[Cluster] + val metadata = mock[Metadata] + val keyspaceMetadata = mock[KeyspaceMetadata] + + val keyspace = "my-keyspace" + val cassandraTableName = "my-table" + + expecting { + keyspaceMetadata.getTable(cassandraTableName).andReturn(null).once() + metadata.getKeyspace(keyspace).andReturn(keyspaceMetadata).once() + cluster.getMetadata.andReturn(metadata).once() + session.getCluster.andReturn(cluster).once() + } + whenExecuting(session, cluster, metadata, keyspaceMetadata) { + val thrown = intercept[Exception] { + CassandraTableSchema.ensureExists(keyspace, cassandraTableName, None, session) + } + thrown.getMessage shouldEqual s"Fail to find the keyspace=$keyspace and/or table=$cassandraTableName !!!!" + } + } + + it("should thrown an exception if fail to apply the schema when keyspace/table does not exist in cassandra") { + val session = mock[Session] + val cluster = mock[Cluster] + val metadata = mock[Metadata] + val applySchemaException = new RuntimeException + val keyspace = "my-keyspace" + val cassandraTableName = "my-table" + + expecting { + session.execute("apply schema").andThrow(applySchemaException) + metadata.getKeyspace(keyspace).andReturn(null).once() + cluster.getMetadata.andReturn(metadata).once() + session.getCluster.andReturn(cluster).once() + } + whenExecuting(session, cluster, metadata) { + val thrown = intercept[Exception] { + CassandraTableSchema.ensureExists(keyspace, cassandraTableName, Some("apply schema;apply schema2"), session) + } + thrown.getCause shouldBe applySchemaException + } + } + +} diff --git a/traces/backends/cassandra/src/test/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/unit/config/ConfigurationLoaderSpec.scala b/traces/backends/cassandra/src/test/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/unit/config/ConfigurationLoaderSpec.scala new file mode 100644 index 000000000..713e1dce7 --- /dev/null +++ b/traces/backends/cassandra/src/test/scala/com/expedia/www/haystack/trace/storage/backends/cassandra/unit/config/ConfigurationLoaderSpec.scala @@ -0,0 +1,65 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.expedia.www.haystack.trace.storage.backends.cassandra.unit.config + +import com.datastax.driver.core.ConsistencyLevel +import com.datastax.driver.core.exceptions.UnavailableException +import com.expedia.www.haystack.trace.storage.backends.cassandra.config.ProjectConfiguration +import com.expedia.www.haystack.trace.storage.backends.cassandra.config.entities.ServiceConfiguration +import com.expedia.www.haystack.trace.storage.backends.cassandra.unit.BaseUnitTestSpec + +class ConfigurationLoaderSpec extends BaseUnitTestSpec { + describe("ConfigurationLoader") { + val project = new ProjectConfiguration() + it("should load the service config from base.conf") { + val serviceConfig: ServiceConfiguration = project.serviceConfig + serviceConfig.port shouldBe 8090 + serviceConfig.ssl.enabled shouldBe false + serviceConfig.ssl.certChainFilePath shouldBe "/ssl/cert" + serviceConfig.ssl.privateKeyPath shouldBe "/ssl/private-key" + } + it("should load the cassandra config from base.conf and few properties overridden from env variable") { + val cassandraWriteConfig = project.cassandraConfig + val clientConfig = cassandraWriteConfig.clientConfig + + cassandraWriteConfig.consistencyLevel shouldEqual ConsistencyLevel.ONE + clientConfig.autoDiscoverEnabled shouldBe false + // this will fail if run inside an editor, we override this config using env variable inside pom.xml + clientConfig.endpoints should contain allOf("cass1", "cass2") + clientConfig.tracesKeyspace.autoCreateSchema shouldBe Some("cassandra_cql_schema_1") + clientConfig.tracesKeyspace.name shouldBe "haystack" + clientConfig.tracesKeyspace.table shouldBe "traces" + clientConfig.tracesKeyspace.recordTTLInSec shouldBe 86400 + + clientConfig.awsNodeDiscovery shouldBe empty + clientConfig.socket.keepAlive shouldBe true + clientConfig.socket.maxConnectionPerHost shouldBe 100 + clientConfig.socket.readTimeoutMills shouldBe 5000 + clientConfig.socket.connectionTimeoutMillis shouldBe 10000 + cassandraWriteConfig.retryConfig.maxRetries shouldBe 10 + cassandraWriteConfig.retryConfig.backOffInMillis shouldBe 250 + cassandraWriteConfig.retryConfig.backoffFactor shouldBe 2 + + // test consistency level on error + val writeError = new UnavailableException(ConsistencyLevel.ONE, 0, 0) + cassandraWriteConfig.writeConsistencyLevel(writeError) shouldEqual ConsistencyLevel.ANY + cassandraWriteConfig.writeConsistencyLevel(new RuntimeException(writeError)) shouldEqual ConsistencyLevel.ANY + cassandraWriteConfig.writeConsistencyLevel(null) shouldEqual ConsistencyLevel.ONE + cassandraWriteConfig.writeConsistencyLevel(new RuntimeException) shouldEqual ConsistencyLevel.ONE + } + + } +} diff --git a/traces/backends/memory/Makefile b/traces/backends/memory/Makefile new file mode 100644 index 000000000..9089e3fff --- /dev/null +++ b/traces/backends/memory/Makefile @@ -0,0 +1,16 @@ +.PHONY: docker_build prepare_integration_test_env integration_test release + +export DOCKER_ORG := expediadotcom +export DOCKER_IMAGE_NAME := haystack-trace-backend-memory +PWD := $(shell pwd) +SERVICE_DEBUG_ON ?= false + +docker_build: + # build docker image using existing app jar + docker build -t $(DOCKER_IMAGE_NAME) -f build/docker/Dockerfile . + +integration_test: + cd ../../ && ./mvnw -q integration-test -pl backends/memory -am + +release: + ../../deployment/scripts/publish-to-docker-hub.sh diff --git a/traces/backends/memory/README.md b/traces/backends/memory/README.md new file mode 100644 index 000000000..90e334716 --- /dev/null +++ b/traces/backends/memory/README.md @@ -0,0 +1,14 @@ +# Storage Backend - In Memory + +Grpc service which can read a write spans to a an in memory map + +## Technical Details + +In order to understand this service, we recommend to read the details of [haystack](https://github.com/ExpediaDotCom/haystack) project. +This service reads from an in memory map. API endpoints are exposed as [GRPC](https://grpc.io/) endpoints. + +Will fill in more details as we go.. + +## Building + +Check the details on [Build Section](../README.md) diff --git a/traces/backends/memory/build/docker/Dockerfile b/traces/backends/memory/build/docker/Dockerfile new file mode 100644 index 000000000..f76a36f70 --- /dev/null +++ b/traces/backends/memory/build/docker/Dockerfile @@ -0,0 +1,30 @@ +FROM openjdk:8-jre +MAINTAINER Haystack + +ENV APP_NAME haystack-trace-backend-memory +ENV APP_HOME /app/bin +ENV JMXTRANS_AGENT jmxtrans-agent-1.2.6 +ENV DOCKERIZE_VERSION v0.6.1 + +ADD https://github.com/jwilder/dockerize/releases/download/${DOCKERIZE_VERSION}/dockerize-alpine-linux-amd64-${DOCKERIZE_VERSION}.tar.gz dockerize.tar.gz +RUN tar xzf dockerize.tar.gz +RUN chmod +x dockerize + +RUN mkdir -p ${APP_HOME} + +COPY target/${APP_NAME}.jar ${APP_HOME}/ +COPY build/docker/start-app.sh ${APP_HOME}/ +RUN chmod +x ${APP_HOME}/start-app.sh + +RUN GRPC_HEALTH_PROBE_VERSION=v0.2.0 && \ + wget -qO/bin/grpc_health_probe https://github.com/grpc-ecosystem/grpc-health-probe/releases/download/${GRPC_HEALTH_PROBE_VERSION}/grpc_health_probe-linux-amd64 && \ + chmod +x /bin/grpc_health_probe + +COPY build/docker/jmxtrans-agent.xml ${APP_HOME}/ +ADD https://github.com/jmxtrans/jmxtrans-agent/releases/download/${JMXTRANS_AGENT}/${JMXTRANS_AGENT}.jar ${APP_HOME}/ + +WORKDIR ${APP_HOME} + +EXPOSE 8090 + +ENTRYPOINT ["./start-app.sh"] diff --git a/traces/backends/memory/build/docker/jmxtrans-agent.xml b/traces/backends/memory/build/docker/jmxtrans-agent.xml new file mode 100644 index 000000000..8b4c1a298 --- /dev/null +++ b/traces/backends/memory/build/docker/jmxtrans-agent.xml @@ -0,0 +1,13 @@ + + + + + + + ${HAYSTACK_GRAPHITE_HOST:monitoring-influxdb-graphite.kube-system.svc} + ${HAYSTACK_GRAPHITE_PORT:2003} + ${HAYSTACK_GRAPHITE_ENABLED:true} + haystack.traces.backend-cassandra.#hostname#. + + 30 + diff --git a/traces/backends/memory/build/docker/start-app.sh b/traces/backends/memory/build/docker/start-app.sh new file mode 100755 index 000000000..ba2c65569 --- /dev/null +++ b/traces/backends/memory/build/docker/start-app.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +[ -z "$JAVA_XMS" ] && JAVA_XMS=1024m +[ -z "$JAVA_XMX" ] && JAVA_XMX=1024m +[ -z "$JAVA_GC_OPTS" ] && JAVA_GC_OPTS="-XX:+UseG1GC" + +set -e +JAVA_OPTS="${JAVA_OPTS} \ +-javaagent:${APP_HOME}/${JMXTRANS_AGENT}.jar=${APP_HOME}/jmxtrans-agent.xml \ +${JAVA_GC_OPTS} \ +-Xmx${JAVA_XMX} \ +-Xms${JAVA_XMS} \ +-XX:+ExitOnOutOfMemoryError \ +-Dapplication.name=${APP_NAME} \ +-Dapplication.home=${APP_HOME}" + +if [[ -n "$SERVICE_DEBUG_ON" ]] && [[ "$SERVICE_DEBUG_ON" == true ]]; then + JAVA_OPTS="$JAVA_OPTS -Xdebug -Xrunjdwp:transport=dt_socket,address=5005,server=y" +fi + +exec java ${JAVA_OPTS} -jar "${APP_HOME}/${APP_NAME}.jar" diff --git a/traces/backends/memory/pom.xml b/traces/backends/memory/pom.xml new file mode 100644 index 000000000..d05fb6483 --- /dev/null +++ b/traces/backends/memory/pom.xml @@ -0,0 +1,142 @@ + + + + + haystack-trace-backends + com.expedia.www + 1.0.9-SNAPSHOT + ../pom.xml + + + 4.0.0 + haystack-trace-backend-memory + jar + + + com.expedia.www.haystack.trace.storage.backends.memory.Service + ${project.artifactId}-${project.version} + 3.3.0.1 + + + + + + com.google.protobuf + protobuf-java + + + + io.grpc + grpc-protobuf + + + + io.grpc + grpc-stub + + + + io.grpc + grpc-services + + + + io.grpc + grpc-netty + + + + io.netty + netty-handler + + + + org.apache.commons + commons-lang3 + + + + + + ${finalName} + + + org.scalatest + scalatest-maven-plugin + + + test + + test + + + com.expedia.www.haystack.trace.storage.backends.memory.unit + + + + integration-test + integration-test + + test + + + com.expedia.www.haystack.trace.storage.backends.memory.integration + + + + + + + org.apache.maven.plugins + maven-shade-plugin + + true + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + + + + package + + shade + + + + + reference.conf + + + ${mainClass} + + + + + + + + + net.alchim31.maven + scala-maven-plugin + + + + org.scalastyle + scalastyle-maven-plugin + + + org.apache.maven.plugins + maven-compiler-plugin + + + + + diff --git a/traces/backends/memory/src/main/resources/config/base.conf b/traces/backends/memory/src/main/resources/config/base.conf new file mode 100644 index 000000000..3b1d854c5 --- /dev/null +++ b/traces/backends/memory/src/main/resources/config/base.conf @@ -0,0 +1,9 @@ +health.status.path = "isHealthy" +service { + port = 8090 + ssl { + enabled = false + cert.path = "" + private.key.path = "" + } +} diff --git a/traces/backends/memory/src/main/resources/logback.xml b/traces/backends/memory/src/main/resources/logback.xml new file mode 100644 index 000000000..e2e2c58e9 --- /dev/null +++ b/traces/backends/memory/src/main/resources/logback.xml @@ -0,0 +1,28 @@ + + + + + + + true + + + + + + %d{yyyy-MM-dd HH:mm:ss:SSS} %thread, %level, %logger{70}, "%msg" %replace(%ex){'[\n]+', '\\n'}%nopex%n + + + + + + ${HAYSTACK_LOG_QUEUE_SIZE:-500} + ${HAYSTACK_LOG_DISCARD_THRESHOLD:-0} + + + + + + + + \ No newline at end of file diff --git a/traces/backends/memory/src/main/scala/com/expedia/www/haystack/trace/storage/backends/memory/Service.scala b/traces/backends/memory/src/main/scala/com/expedia/www/haystack/trace/storage/backends/memory/Service.scala new file mode 100644 index 000000000..c1ff30d0e --- /dev/null +++ b/traces/backends/memory/src/main/scala/com/expedia/www/haystack/trace/storage/backends/memory/Service.scala @@ -0,0 +1,93 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.trace.storage.backends.memory + +import java.io.File + +import com.codahale.metrics.JmxReporter +import com.expedia.www.haystack.commons.logger.LoggerUtils +import com.expedia.www.haystack.commons.metrics.MetricsSupport +import com.expedia.www.haystack.trace.storage.backends.memory.config.ProjectConfiguration +import com.expedia.www.haystack.trace.storage.backends.memory.services.{GrpcHealthService, SpansPersistenceService} +import com.expedia.www.haystack.trace.storage.backends.memory.store.InMemoryTraceRecordStore +import io.grpc.netty.NettyServerBuilder +import org.slf4j.{Logger, LoggerFactory} + +object Service extends MetricsSupport { + private val LOGGER: Logger = LoggerFactory.getLogger("MemoryBackend") + + // primary executor for service's async tasks + implicit private val executor = scala.concurrent.ExecutionContext.global + + def main(args: Array[String]): Unit = { + startJmxReporter() + startService(args) + } + + private def startJmxReporter(): Unit = { + JmxReporter + .forRegistry(metricRegistry) + .build() + .start() + } + + private def startService(args: Array[String]): Unit = { + try { + val config = new ProjectConfiguration + val serviceConfig = config.serviceConfig + var port = serviceConfig.port + if(args!=null && args.length!=0) { + port = args(0).toInt + } + + val tracerRecordStore = new InMemoryTraceRecordStore() + + val serverBuilder = NettyServerBuilder + .forPort(port) + .directExecutor() + .addService(new GrpcHealthService()) + .addService(new SpansPersistenceService(store = tracerRecordStore)(executor)) + + + // enable ssl if enabled + if (serviceConfig.ssl.enabled) { + serverBuilder.useTransportSecurity(new File(serviceConfig.ssl.certChainFilePath), new File(serviceConfig.ssl.privateKeyPath)) + } + + + val server = serverBuilder.build().start() + + LOGGER.info(s"server started, listening on ${serviceConfig.port}") + + Runtime.getRuntime.addShutdownHook(new Thread() { + override def run(): Unit = { + LOGGER.info("shutting down gRPC server since JVM is shutting down") + server.shutdown() + LOGGER.info("server has been shutdown now") + } + }) + + server.awaitTermination() + } catch { + case ex: Throwable => + ex.printStackTrace() + LOGGER.error("Fatal error observed while running the app", ex) + LoggerUtils.shutdownLogger() + System.exit(1) + } + } +} diff --git a/traces/backends/memory/src/main/scala/com/expedia/www/haystack/trace/storage/backends/memory/config/ProjectConfiguration.scala b/traces/backends/memory/src/main/scala/com/expedia/www/haystack/trace/storage/backends/memory/config/ProjectConfiguration.scala new file mode 100644 index 000000000..8ade8b187 --- /dev/null +++ b/traces/backends/memory/src/main/scala/com/expedia/www/haystack/trace/storage/backends/memory/config/ProjectConfiguration.scala @@ -0,0 +1,36 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trace.storage.backends.memory.config + +import com.expedia.www.haystack.commons.config.ConfigurationLoader +import com.expedia.www.haystack.trace.storage.backends.memory.config.entities.{ServiceConfiguration, SslConfiguration} + +class ProjectConfiguration { + private val config = ConfigurationLoader.loadConfigFileWithEnvOverrides() + + val healthStatusFilePath: String = config.getString("health.status.path") + + val serviceConfig: ServiceConfiguration = { + val serviceConfig = config.getConfig("service") + + val ssl = serviceConfig.getConfig("ssl") + val sslConfig = SslConfiguration(ssl.getBoolean("enabled"), ssl.getString("cert.path"), ssl.getString("private.key.path")) + + ServiceConfiguration(serviceConfig.getInt("port"), sslConfig) + } +} diff --git a/traces/backends/memory/src/main/scala/com/expedia/www/haystack/trace/storage/backends/memory/config/entities/ServiceConfiguration.scala b/traces/backends/memory/src/main/scala/com/expedia/www/haystack/trace/storage/backends/memory/config/entities/ServiceConfiguration.scala new file mode 100644 index 000000000..3760407c3 --- /dev/null +++ b/traces/backends/memory/src/main/scala/com/expedia/www/haystack/trace/storage/backends/memory/config/entities/ServiceConfiguration.scala @@ -0,0 +1,23 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expedia.www.haystack.trace.storage.backends.memory.config.entities + +/** + * @param port port to start grpc servicer on + */ +case class ServiceConfiguration(port: Int, ssl: SslConfiguration) +case class SslConfiguration(enabled: Boolean, certChainFilePath: String, privateKeyPath: String) diff --git a/traces/backends/memory/src/main/scala/com/expedia/www/haystack/trace/storage/backends/memory/services/GrpcHealthService.scala b/traces/backends/memory/src/main/scala/com/expedia/www/haystack/trace/storage/backends/memory/services/GrpcHealthService.scala new file mode 100644 index 000000000..2ccaf25ce --- /dev/null +++ b/traces/backends/memory/src/main/scala/com/expedia/www/haystack/trace/storage/backends/memory/services/GrpcHealthService.scala @@ -0,0 +1,31 @@ +/* + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expedia.www.haystack.trace.storage.backends.memory.services + +import io.grpc.health.v1.{HealthCheckRequest, HealthCheckResponse, HealthGrpc} +import io.grpc.stub.StreamObserver + +class GrpcHealthService extends HealthGrpc.HealthImplBase { + + override def check(request: HealthCheckRequest, responseObserver: StreamObserver[HealthCheckResponse]): Unit = { + responseObserver.onNext(HealthCheckResponse + .newBuilder() + .setStatus(HealthCheckResponse.ServingStatus.SERVING) + .build()) + responseObserver.onCompleted() + } +} diff --git a/traces/backends/memory/src/main/scala/com/expedia/www/haystack/trace/storage/backends/memory/services/SpansPersistenceService.scala b/traces/backends/memory/src/main/scala/com/expedia/www/haystack/trace/storage/backends/memory/services/SpansPersistenceService.scala new file mode 100644 index 000000000..00b5e3f35 --- /dev/null +++ b/traces/backends/memory/src/main/scala/com/expedia/www/haystack/trace/storage/backends/memory/services/SpansPersistenceService.scala @@ -0,0 +1,54 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expedia.www.haystack.trace.storage.backends.memory.services + +import com.expedia.open.tracing.backend.WriteSpansResponse.ResultCode +import com.expedia.open.tracing.backend._ +import com.expedia.www.haystack.trace.storage.backends.memory.store.InMemoryTraceRecordStore +import io.grpc.stub.StreamObserver + +import scala.collection.JavaConverters._ +import scala.concurrent.ExecutionContextExecutor + +class SpansPersistenceService(store: InMemoryTraceRecordStore) + (implicit val executor: ExecutionContextExecutor) extends StorageBackendGrpc.StorageBackendImplBase { + + + override def writeSpans(request: WriteSpansRequest, responseObserver: StreamObserver[WriteSpansResponse]): Unit = { + store.writeTraceRecords(request.getRecordsList.asScala.toList) + val response = WriteSpansResponse.newBuilder().setCode( + ResultCode.SUCCESS + ).build() + responseObserver.onNext(response) + responseObserver.onCompleted() + } + + /** + *
+    * read buffered spans from backend
+    * 
+ */ + override def readSpans(request: ReadSpansRequest, responseObserver: StreamObserver[ReadSpansResponse]): Unit = { + + val records = store.readTraceRecords(request.getTraceIdsList.iterator().asScala.toList) + val response = ReadSpansResponse.newBuilder() + .addAllRecords(records.asJava) + .build() + responseObserver.onNext(response) + responseObserver.onCompleted() + } +} diff --git a/traces/backends/memory/src/main/scala/com/expedia/www/haystack/trace/storage/backends/memory/store/InMemoryTraceRecordStore.scala b/traces/backends/memory/src/main/scala/com/expedia/www/haystack/trace/storage/backends/memory/store/InMemoryTraceRecordStore.scala new file mode 100644 index 000000000..c2b184dff --- /dev/null +++ b/traces/backends/memory/src/main/scala/com/expedia/www/haystack/trace/storage/backends/memory/store/InMemoryTraceRecordStore.scala @@ -0,0 +1,71 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expedia.www.haystack.trace.storage.backends.memory.store + +import com.expedia.open.tracing.backend.TraceRecord +import com.expedia.www.haystack.commons.metrics.MetricsSupport +import org.slf4j.LoggerFactory + +import scala.concurrent.ExecutionContextExecutor + +class InMemoryTraceRecordStore() + (implicit val dispatcher: ExecutionContextExecutor) extends MetricsSupport with AutoCloseable { + private val LOGGER = LoggerFactory.getLogger(classOf[InMemoryTraceRecordStore]) + + + private var inMemoryTraceRecords = Map[String, List[TraceRecord]]() + + def readTraceRecords(traceIds: List[String]): Seq[TraceRecord] = { + + try { + traceIds.flatMap(traceId => { + inMemoryTraceRecords.getOrElse(traceId, List()) + }) + } catch { + case ex: Exception => + LOGGER.error("Failed to read raw traces with exception", ex) + List() + } + } + + /** + * writes the traceId and its spans to a in. Use the current timestamp as the sort key for the writes to same + * TraceId. Also if the parallel writes exceed the max inflight requests, then we block and this puts backpressure on + * upstream + * + * @param traceRecords : trace records which need to be written + * @return + */ + def writeTraceRecords(traceRecords: List[TraceRecord]): Unit = { + + + traceRecords.foreach(record => { + + try { + val existingRecords: List[TraceRecord] = inMemoryTraceRecords.getOrElse(record.getTraceId, List()) + val records = record :: existingRecords + inMemoryTraceRecords = inMemoryTraceRecords + (record.getTraceId -> records) + } catch { + case ex: Exception => + LOGGER.error("Fail to write the spans to memory with exception", ex) + + } + }) + } + + override def close(): Unit = () +} diff --git a/traces/backends/memory/src/test/resources/config/base.conf b/traces/backends/memory/src/test/resources/config/base.conf new file mode 100644 index 000000000..4bf5cb741 --- /dev/null +++ b/traces/backends/memory/src/test/resources/config/base.conf @@ -0,0 +1,10 @@ +health.status.path = "isHealthy" + +service { + port = 8090 + ssl { + enabled = false + cert.path = "/ssl/cert" + private.key.path = "/ssl/private-key" + } +} diff --git a/traces/backends/memory/src/test/resources/logback-test.xml b/traces/backends/memory/src/test/resources/logback-test.xml new file mode 100644 index 000000000..298193e01 --- /dev/null +++ b/traces/backends/memory/src/test/resources/logback-test.xml @@ -0,0 +1 @@ + diff --git a/traces/backends/memory/src/test/scala/com/expedia/www/haystack/trace/storage/backends/memory/integration/BaseIntegrationTestSpec.scala b/traces/backends/memory/src/test/scala/com/expedia/www/haystack/trace/storage/backends/memory/integration/BaseIntegrationTestSpec.scala new file mode 100644 index 000000000..1c72f5eb4 --- /dev/null +++ b/traces/backends/memory/src/test/scala/com/expedia/www/haystack/trace/storage/backends/memory/integration/BaseIntegrationTestSpec.scala @@ -0,0 +1,87 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expedia.www.haystack.trace.storage.backends.memory.integration + +import java.util.UUID +import java.util.concurrent.Executors + +import com.expedia.open.tracing.Span +import com.expedia.open.tracing.backend.StorageBackendGrpc +import com.expedia.open.tracing.buffer.SpanBuffer +import com.expedia.www.haystack.trace.storage.backends.memory.Service +import io.grpc.ManagedChannelBuilder +import org.scalatest._ + +import scala.collection.JavaConverters._ + +trait BaseIntegrationTestSpec extends FunSpec with GivenWhenThen with Matchers with BeforeAndAfterAll with BeforeAndAfterEach { + protected var client: StorageBackendGrpc.StorageBackendBlockingStub = _ + + + private val executors = Executors.newSingleThreadExecutor() + + + override def beforeAll() { + + + + + executors.submit(new Runnable { + override def run(): Unit = Service.main(null) + }) + + Thread.sleep(5000) + + client = StorageBackendGrpc.newBlockingStub(ManagedChannelBuilder.forAddress("localhost", 8090) + .usePlaintext(true) + .build()) + } + + protected def createSerializedSpanBuffer(traceId: String = UUID.randomUUID().toString, + spanId: String = UUID.randomUUID().toString, + serviceName: String = "test-service", + operationName: String = "test-operation", + tags: Map[String, String] = Map.empty, + startTime: Long = System.currentTimeMillis() * 1000, + sleep: Boolean = true): Array[Byte] = { + val spanBuffer = createSpanBufferWithSingleSpan(traceId, spanId, serviceName, operationName, tags, startTime) + spanBuffer.toByteArray + } + + private def createSpanBufferWithSingleSpan(traceId: String, + spanId: String, + serviceName: String, + operationName: String, + tags: Map[String, String], + startTime: Long) = { + val spanTags = tags.map(tag => com.expedia.open.tracing.Tag.newBuilder().setKey(tag._1).setVStr(tag._2).build()) + + SpanBuffer + .newBuilder() + .setTraceId(traceId) + .addChildSpans(Span + .newBuilder() + .setTraceId(traceId) + .setSpanId(spanId) + .setOperationName(operationName) + .setServiceName(serviceName) + .setStartTime(startTime) + .addAllTags(spanTags.asJava) + .build()) + .build() + } +} diff --git a/traces/backends/memory/src/test/scala/com/expedia/www/haystack/trace/storage/backends/memory/integration/InMemoryTraceBackendServiceIntegrationTestSpec.scala b/traces/backends/memory/src/test/scala/com/expedia/www/haystack/trace/storage/backends/memory/integration/InMemoryTraceBackendServiceIntegrationTestSpec.scala new file mode 100644 index 000000000..93aad6dd9 --- /dev/null +++ b/traces/backends/memory/src/test/scala/com/expedia/www/haystack/trace/storage/backends/memory/integration/InMemoryTraceBackendServiceIntegrationTestSpec.scala @@ -0,0 +1,49 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expedia.www.haystack.trace.storage.backends.memory.integration + +import java.util.UUID + +import com.expedia.open.tracing.backend.{ReadSpansRequest, TraceRecord, WriteSpansRequest} +import com.google.protobuf.ByteString + +class InMemoryTraceBackendServiceIntegrationTestSpec extends BaseIntegrationTestSpec { + + + describe("In Memory Persistence Service read trace records") { + it("should get trace records for given traceID from in memory") { + Given("trace-record ") + val traceId = UUID.randomUUID().toString + val serializedSpans = createSerializedSpanBuffer(traceId) + val traceRecord = TraceRecord.newBuilder() + .setTraceId(traceId) + .setSpans(ByteString.copyFrom(serializedSpans)) + .setTimestamp(System.currentTimeMillis()) + .build() + + When("write span is invoked") + val writeSpanRequest = WriteSpansRequest.newBuilder().addRecords(traceRecord).build() + val response = client.writeSpans(writeSpanRequest) + + Then("should be able to retrieve the trace-record back") + val readSpansResponse = client.readSpans(ReadSpansRequest.newBuilder().addTraceIds(traceId).build()) + readSpansResponse.getRecordsCount shouldBe 1 + readSpansResponse.getRecordsCount shouldEqual 1 + readSpansResponse.getRecordsList.get(0).getTraceId shouldEqual traceId + } + } +} diff --git a/traces/backends/memory/src/test/scala/com/expedia/www/haystack/trace/storage/backends/memory/unit/BaseUnitTestSpec.scala b/traces/backends/memory/src/test/scala/com/expedia/www/haystack/trace/storage/backends/memory/unit/BaseUnitTestSpec.scala new file mode 100644 index 000000000..fa83ad76c --- /dev/null +++ b/traces/backends/memory/src/test/scala/com/expedia/www/haystack/trace/storage/backends/memory/unit/BaseUnitTestSpec.scala @@ -0,0 +1,22 @@ +/* + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expedia.www.haystack.trace.storage.backends.memory.unit + +import org.scalatest.{FunSpec, GivenWhenThen, Matchers} +import org.scalatest.easymock.EasyMockSugar + +trait BaseUnitTestSpec extends FunSpec with GivenWhenThen with Matchers with EasyMockSugar diff --git a/traces/backends/memory/src/test/scala/com/expedia/www/haystack/trace/storage/backends/memory/unit/config/ConfigurationLoaderSpec.scala b/traces/backends/memory/src/test/scala/com/expedia/www/haystack/trace/storage/backends/memory/unit/config/ConfigurationLoaderSpec.scala new file mode 100644 index 000000000..10a784e6b --- /dev/null +++ b/traces/backends/memory/src/test/scala/com/expedia/www/haystack/trace/storage/backends/memory/unit/config/ConfigurationLoaderSpec.scala @@ -0,0 +1,33 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.expedia.www.haystack.trace.storage.backends.memory.unit.config + +import com.expedia.www.haystack.trace.storage.backends.memory.config.ProjectConfiguration +import com.expedia.www.haystack.trace.storage.backends.memory.config.entities.ServiceConfiguration +import com.expedia.www.haystack.trace.storage.backends.memory.unit.BaseUnitTestSpec + +class ConfigurationLoaderSpec extends BaseUnitTestSpec { + describe("ConfigurationLoader") { + val project = new ProjectConfiguration() + it("should load the service config from base.conf") { + val serviceConfig: ServiceConfiguration = project.serviceConfig + serviceConfig.port shouldBe 8090 + serviceConfig.ssl.enabled shouldBe false + serviceConfig.ssl.certChainFilePath shouldBe "/ssl/cert" + serviceConfig.ssl.privateKeyPath shouldBe "/ssl/private-key" + } + } +} diff --git a/traces/backends/pom.xml b/traces/backends/pom.xml new file mode 100644 index 000000000..f152a902b --- /dev/null +++ b/traces/backends/pom.xml @@ -0,0 +1,27 @@ + + + + 4.0.0 + haystack-trace-backends + 1.0.9-SNAPSHOT + pom + + + haystack-traces + com.expedia.www + 1.0.9-SNAPSHOT + ../pom.xml + + + + + cassandra + memory + + + + ${basedir}/../../checkstyles/scalastyle_config.xml + + + diff --git a/traces/checkstyles/scalastyle_config.xml b/traces/checkstyles/scalastyle_config.xml new file mode 100644 index 000000000..0b5ba9469 --- /dev/null +++ b/traces/checkstyles/scalastyle_config.xml @@ -0,0 +1,134 @@ + + Scalastyle standard configuration + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/traces/commons/pom.xml b/traces/commons/pom.xml new file mode 100644 index 000000000..b49a403eb --- /dev/null +++ b/traces/commons/pom.xml @@ -0,0 +1,121 @@ + + + + + haystack-traces + com.expedia.www + 1.0.9-SNAPSHOT + + + 4.0.0 + haystack-trace-commons + jar + + + + + vc.inreach.aws + aws-signing-request-interceptor + + + + com.amazonaws + aws-java-sdk-sts + + + + org.json4s + json4s-jackson_${scala.major.minor.version} + + + + + com.amazonaws + aws-java-sdk-ec2 + + + + io.searchbox + jest + + + + org.apache.commons + commons-lang3 + + + + org.xerial.snappy + snappy-java + + + + com.github.luben + zstd-jni + + + + com.google.protobuf + protobuf-java + + + + io.grpc + grpc-protobuf + + + + io.grpc + grpc-stub + + + + io.grpc + grpc-services + + + + io.grpc + grpc-netty + + + + org.json4s + json4s-ext_${scala.major.minor.version} + + + + + + org.scalatest + scalatest-maven-plugin + + com.expedia.www.haystack.trace.commons.unit + + + + + net.alchim31.maven + scala-maven-plugin + + + + org.scalastyle + scalastyle-maven-plugin + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + org.scoverage + scoverage-maven-plugin + + false + + + + + diff --git a/traces/commons/src/main/scala/com/expedia/www/haystack/trace/commons/clients/es/AWSSigningJestClientFactory.scala b/traces/commons/src/main/scala/com/expedia/www/haystack/trace/commons/clients/es/AWSSigningJestClientFactory.scala new file mode 100644 index 000000000..fa75e322b --- /dev/null +++ b/traces/commons/src/main/scala/com/expedia/www/haystack/trace/commons/clients/es/AWSSigningJestClientFactory.scala @@ -0,0 +1,70 @@ +/* + * Copyright 2019, Expedia Group. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trace.commons.clients.es + +import java.time.{LocalDateTime, ZoneId} + +import com.expedia.www.haystack.trace.commons.config.entities.AWSRequestSigningConfiguration +import com.google.common.base.Supplier +import io.searchbox.client.JestClientFactory +import org.apache.http.impl.client.HttpClientBuilder +import org.apache.http.impl.nio.client.HttpAsyncClientBuilder +import org.slf4j.LoggerFactory +import vc.inreach.aws.request.{AWSSigner, AWSSigningRequestInterceptor} +import com.amazonaws.auth.AWSCredentialsProvider +import com.amazonaws.auth.BasicAWSCredentials +import com.amazonaws.auth.DefaultAWSCredentialsProviderChain +import com.amazonaws.internal.StaticCredentialsProvider + + +/** + * wrapper for JestClientFactory. Provides support for AWS ES request signing by adding an interceptor to the client. + * + * @param awsRequestSigningConfig config required for request signing like creds, region + */ +class AWSSigningJestClientFactory(awsRequestSigningConfig: AWSRequestSigningConfiguration) extends JestClientFactory { + private val LOGGER = LoggerFactory.getLogger(classOf[AWSSigningJestClientFactory]) + + val awsSigner = new AWSSigner(getCredentialProvider, awsRequestSigningConfig.region, awsRequestSigningConfig.awsServiceName, new ClockSupplier) + val requestInterceptor = new AWSSigningRequestInterceptor(awsSigner) + + override def configureHttpClient(builder: HttpClientBuilder): HttpClientBuilder = { + builder.addInterceptorLast(requestInterceptor) + } + + override def configureHttpClient(builder: HttpAsyncClientBuilder): HttpAsyncClientBuilder = { + builder.addInterceptorLast(requestInterceptor) + } + + def getCredentialProvider: AWSCredentialsProvider = { + if (awsRequestSigningConfig.accessKey.isDefined) { + LOGGER.info("using static aws credential provider with access and secret key for ES") + new StaticCredentialsProvider( + new BasicAWSCredentials(awsRequestSigningConfig.accessKey.get, awsRequestSigningConfig.secretKey.get)) + } else { + LOGGER.info("using default credential provider chain for ES") + new DefaultAWSCredentialsProviderChain + } + } +} + +class ClockSupplier extends Supplier[LocalDateTime] { + override def get(): LocalDateTime = { + LocalDateTime.now(ZoneId.of("UTC")) + } +} diff --git a/traces/commons/src/main/scala/com/expedia/www/haystack/trace/commons/clients/es/document/ServiceMetadataDoc.scala b/traces/commons/src/main/scala/com/expedia/www/haystack/trace/commons/clients/es/document/ServiceMetadataDoc.scala new file mode 100644 index 000000000..b88b2c235 --- /dev/null +++ b/traces/commons/src/main/scala/com/expedia/www/haystack/trace/commons/clients/es/document/ServiceMetadataDoc.scala @@ -0,0 +1,26 @@ +/* + * Copyright 2018 Expedia, Group. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trace.commons.clients.es.document + +import org.json4s.jackson.Serialization + + +case class ServiceMetadataDoc(servicename: String, + operationname: String) { + val json: String = Serialization.write(this)(TraceIndexDoc.formats) +} diff --git a/traces/commons/src/main/scala/com/expedia/www/haystack/trace/commons/clients/es/document/TraceIndexDoc.scala b/traces/commons/src/main/scala/com/expedia/www/haystack/trace/commons/clients/es/document/TraceIndexDoc.scala new file mode 100644 index 000000000..eff43c504 --- /dev/null +++ b/traces/commons/src/main/scala/com/expedia/www/haystack/trace/commons/clients/es/document/TraceIndexDoc.scala @@ -0,0 +1,38 @@ +/* + * Copyright 2018 Expedia, Group. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trace.commons.clients.es.document + +import org.json4s.DefaultFormats +import org.json4s.jackson.Serialization + +import scala.collection.mutable + +object TraceIndexDoc { + implicit val formats = DefaultFormats + type TagKey = String + type TagValue = Any + + val SERVICE_KEY_NAME = "servicename" + val OPERATION_KEY_NAME = "operationname" + val DURATION_KEY_NAME = "duration" + val START_TIME_KEY_NAME = "starttime" +} + +case class TraceIndexDoc(traceid: String, rootduration: Long, starttime: Long, spans: Seq[mutable.Map[String, Any]]) { + val json: String = Serialization.write(this)(TraceIndexDoc.formats) +} diff --git a/traces/commons/src/main/scala/com/expedia/www/haystack/trace/commons/config/entities/AWSRequestSigningConfiguration.scala b/traces/commons/src/main/scala/com/expedia/www/haystack/trace/commons/config/entities/AWSRequestSigningConfiguration.scala new file mode 100644 index 000000000..ac41e847f --- /dev/null +++ b/traces/commons/src/main/scala/com/expedia/www/haystack/trace/commons/config/entities/AWSRequestSigningConfiguration.scala @@ -0,0 +1,33 @@ +/* + * Copyright 2019, Expedia Group. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trace.commons.config.entities + +/** + * defines the configuration parameters for AWS request signing + * + * @param enabled: signing will be performed if this flag is enabled + * @param region: aws region + * @param awsServiceName: aws service name for which signing needs to be done + * @param accessKey: aws access key. If not present DefaultAWSCredentialsProviderChain is used + * @param secretKey: aws secret key + */ +case class AWSRequestSigningConfiguration (enabled: Boolean, + region: String, + awsServiceName: String, + accessKey: Option[String], + secretKey: Option[String]) diff --git a/traces/commons/src/main/scala/com/expedia/www/haystack/trace/commons/config/entities/ReloadConfiguration.scala b/traces/commons/src/main/scala/com/expedia/www/haystack/trace/commons/config/entities/ReloadConfiguration.scala new file mode 100644 index 000000000..8fff2e7a6 --- /dev/null +++ b/traces/commons/src/main/scala/com/expedia/www/haystack/trace/commons/config/entities/ReloadConfiguration.scala @@ -0,0 +1,37 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trace.commons.config.entities + +import com.expedia.www.haystack.trace.commons.config.reload.Reloadable + +/** + * defines the configuration parameters for reloading the app configs from external store like ElasticSearch + * + * @param configStoreEndpoint: endpoint for external store where app configuration is stored + * @param databaseName: name of the database + * @param reloadIntervalInMillis: app config will be refreshed after this given interval in millis + * @param observers: list of reloadable configuration objects that subscribe to the reloader + * @param loadOnStartup: loads the app configuration from external store on startup, default is true + */ +case class ReloadConfiguration(configStoreEndpoint: String, + databaseName: String, + reloadIntervalInMillis: Int, + username: Option[String], + password: Option[String], + observers: Seq[Reloadable], + loadOnStartup: Boolean = true) diff --git a/traces/commons/src/main/scala/com/expedia/www/haystack/trace/commons/config/entities/TraceStoreBackends.scala b/traces/commons/src/main/scala/com/expedia/www/haystack/trace/commons/config/entities/TraceStoreBackends.scala new file mode 100644 index 000000000..af56f37f4 --- /dev/null +++ b/traces/commons/src/main/scala/com/expedia/www/haystack/trace/commons/config/entities/TraceStoreBackends.scala @@ -0,0 +1,32 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trace.commons.config.entities + + +/** + * defines the configuration parameters for trace-backend * + * @param host : trace backend grpc hostname + * @param port : trace backend grpc port + */ +case class GrpcClientConfig(host: String, port: Int) + +/** + * multiple store backends + * @param backends configuration of all trace store backends + */ +case class TraceStoreBackends(backends: Seq[GrpcClientConfig]) diff --git a/traces/commons/src/main/scala/com/expedia/www/haystack/trace/commons/config/entities/WhitelistIndexFieldConfiguration.scala b/traces/commons/src/main/scala/com/expedia/www/haystack/trace/commons/config/entities/WhitelistIndexFieldConfiguration.scala new file mode 100644 index 000000000..19a1ac429 --- /dev/null +++ b/traces/commons/src/main/scala/com/expedia/www/haystack/trace/commons/config/entities/WhitelistIndexFieldConfiguration.scala @@ -0,0 +1,121 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trace.commons.config.entities + +import java.util.concurrent.ConcurrentHashMap + +import com.expedia.www.haystack.trace.commons.config.entities.IndexFieldType.IndexFieldType +import com.expedia.www.haystack.trace.commons.config.reload.Reloadable +import org.apache.commons.lang3.StringUtils +import org.json4s.ext.EnumNameSerializer +import org.json4s.jackson.Serialization +import org.json4s.{DefaultFormats, Formats} +import org.slf4j.LoggerFactory + +import scala.collection.JavaConverters._ +import scala.collection.mutable + +object IndexFieldType extends Enumeration { + type IndexFieldType = Value + val string, long, int, double, bool = Value +} + +case class WhitelistIndexField(name: String, + `type`: IndexFieldType, + aliases: Set[String] = Set(), + enableRangeQuery: Boolean = false, + searchContext: String = "span", + enabled: Boolean = true) + +case class WhiteListIndexFields(fields: List[WhitelistIndexField]) + +case class WhitelistIndexFieldConfiguration() extends Reloadable { + private val LOGGER = LoggerFactory.getLogger(classOf[WhitelistIndexFieldConfiguration]) + + protected implicit val formats: Formats = DefaultFormats + new EnumNameSerializer(IndexFieldType) + + @volatile + private var currentVersion: Int = 0 + + val indexFieldMap = new ConcurrentHashMap[String, WhitelistIndexField]() + + var reloadConfigTableName: Option[String] = None + + private val onChangeListeners = mutable.ListBuffer[() => Unit]() + + // fail fast + override def name: String = reloadConfigTableName + .getOrElse(throw new RuntimeException("fail to find the reload config table name!")) + + /** + * this is called whenever the configuration reloader system reads the configuration object from external store + * we check if the config data has changed using the string's hashCode + * @param configData config object that is loaded at regular intervals from external store + */ + override def onReload(configData: String): Unit = { + if(StringUtils.isNotEmpty(configData) && hasConfigChanged(configData)) { + LOGGER.info("new indexing fields have been detected: " + configData) + val fieldsToIndex = Serialization.read[WhiteListIndexFields](configData) + + val lowercaseFieldNames = fieldsToIndex + .fields + .map(field => field.copy(name = field.name.toLowerCase, aliases = field.aliases.map(_.toLowerCase))) + + updateIndexFieldMap(WhiteListIndexFields(lowercaseFieldNames)) + // set the current version to newer one + currentVersion = configData.hashCode + + this.synchronized { + onChangeListeners.foreach(l => l()) + } + } + } + + def addOnChangeListener(listener: () => Unit): Unit = { + this.synchronized { + onChangeListeners.append(listener) + } + } + + private def updateIndexFieldMap(fList: WhiteListIndexFields): Unit = { + // remove the fields from the map if they are not present in the newly provided whitelist set + val indexableFieldNames = fList.fields.flatMap(field => field.aliases + field.name) + + indexFieldMap.values().removeIf((f: WhitelistIndexField) => !indexableFieldNames.contains(f.name)) + + // add the fields in the map + for(field <- fList.fields) { + indexFieldMap.put(field.name, field) + field.aliases.foreach(alias => indexFieldMap.put(alias, field)) + } + } + + /** + * detect if configuration has changed using the hashCode as version + * @param newConfigData new configuration data + * @return + */ + private def hasConfigChanged(newConfigData: String): Boolean = newConfigData.hashCode != currentVersion + + /** + * @return the whitelist index fields + */ + def whitelistIndexFields: List[WhitelistIndexField] = indexFieldMap.values().asScala.toList + + def globalTraceContextIndexFieldNames: Set[String] = whitelistIndexFields.filter(_.searchContext == "trace").map(_.name).toSet +} diff --git a/traces/commons/src/main/scala/com/expedia/www/haystack/trace/commons/config/reload/ConfigurationReloadElasticSearchProvider.scala b/traces/commons/src/main/scala/com/expedia/www/haystack/trace/commons/config/reload/ConfigurationReloadElasticSearchProvider.scala new file mode 100644 index 000000000..68222a8ea --- /dev/null +++ b/traces/commons/src/main/scala/com/expedia/www/haystack/trace/commons/config/reload/ConfigurationReloadElasticSearchProvider.scala @@ -0,0 +1,87 @@ +/* + * Copyright 2019, Expedia Group. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trace.commons.config.reload + +import com.expedia.www.haystack.commons.retries.RetryOperation +import com.expedia.www.haystack.trace.commons.clients.es.AWSSigningJestClientFactory +import com.expedia.www.haystack.trace.commons.config.entities.{AWSRequestSigningConfiguration, ReloadConfiguration} +import io.searchbox.client.config.HttpClientConfig +import io.searchbox.client.{JestClient, JestClientFactory} +import io.searchbox.core.Search + +import scala.util.{Failure, Success} + +class ConfigurationReloadElasticSearchProvider(reloadConfig: ReloadConfiguration, awsRequestSigningConfig: AWSRequestSigningConfiguration) + extends ConfigurationReloadProvider(reloadConfig) { + + private val matchAllQuery = "{\"query\":{\"match_all\":{\"boost\":1.0}}}" + + private val esClient: JestClient = { + val factory = { + if (awsRequestSigningConfig.enabled) { + LOGGER.info("using AWSSigningJestClientFactory for es client") + new AWSSigningJestClientFactory(awsRequestSigningConfig) + } else { + LOGGER.info("using JestClientFactory for es client") + new JestClientFactory() + } + } + + val builder = new HttpClientConfig.Builder(reloadConfig.configStoreEndpoint).multiThreaded(false) + + if (reloadConfig.username.isDefined && reloadConfig.password.isDefined) { + builder.defaultCredentials(reloadConfig.username.get, reloadConfig.password.get) + } + + factory.setHttpClientConfig(builder.build()) + factory.getObject + } + + /** + * loads the configuration from external store + */ + override def load(): Unit = { + reloadConfig.observers.foreach(observer => { + + val searchQuery = new Search.Builder(matchAllQuery) + .addIndex(reloadConfig.databaseName) + .addType(observer.name) + .build() + + RetryOperation.executeWithRetryBackoff(() => esClient.execute(searchQuery), RetryOperation.Config(3, 1000, 2)) match { + case Success(result) => + if (result.isSucceeded) { + LOGGER.info(s"Reloading(or loading) is successfully done for the configuration name =${observer.name}") + observer.onReload(result.getSourceAsString) + } else { + LOGGER.error(s"Fail to reload the configuration from elastic search with error: ${result.getErrorMessage} " + + s"for observer name=${observer.name}") + } + + case Failure(reason) => + LOGGER.error(s"Fail to reload the configuration from elastic search for observer name=${observer.name}. " + + s"Will try at next scheduled time", reason) + } + }) + } + + override def close(): Unit = { + this.esClient.shutdownClient() + super.close() + } +} diff --git a/traces/commons/src/main/scala/com/expedia/www/haystack/trace/commons/config/reload/ConfigurationReloadProvider.scala b/traces/commons/src/main/scala/com/expedia/www/haystack/trace/commons/config/reload/ConfigurationReloadProvider.scala new file mode 100644 index 000000000..539fb7823 --- /dev/null +++ b/traces/commons/src/main/scala/com/expedia/www/haystack/trace/commons/config/reload/ConfigurationReloadProvider.scala @@ -0,0 +1,41 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trace.commons.config.reload + +import java.util.concurrent.{Executors, TimeUnit} + +import com.expedia.www.haystack.trace.commons.config.entities.ReloadConfiguration +import org.slf4j.{Logger, LoggerFactory} + +abstract class ConfigurationReloadProvider(config: ReloadConfiguration) extends AutoCloseable { + protected val LOGGER: Logger = LoggerFactory.getLogger(classOf[ConfigurationReloadProvider]) + + private val executor = Executors.newSingleThreadScheduledExecutor() + + // schedule the reload process from an external store + if(config.reloadIntervalInMillis > -1) { + LOGGER.info("configuration reload scheduler has been started with a delay of {}ms", config.reloadIntervalInMillis) + executor.scheduleWithFixedDelay(() => { + load() + }, config.reloadIntervalInMillis, config.reloadIntervalInMillis, TimeUnit.MILLISECONDS) + } + + def load(): Unit + + def close(): Unit = executor.shutdownNow() +} diff --git a/traces/commons/src/main/scala/com/expedia/www/haystack/trace/commons/config/reload/Reloadable.scala b/traces/commons/src/main/scala/com/expedia/www/haystack/trace/commons/config/reload/Reloadable.scala new file mode 100644 index 000000000..97131ccee --- /dev/null +++ b/traces/commons/src/main/scala/com/expedia/www/haystack/trace/commons/config/reload/Reloadable.scala @@ -0,0 +1,30 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trace.commons.config.reload + +/** + * An entity(for e.g. a configuration object) that extends reloadable trait allows it to reload the config object + * dynamically. The config reloader reads the new configuration periodically from an external store and + * calls 'onReload()' method. + * The 'name' provides the tableName where the configuration is stored for this entity. + */ +trait Reloadable { + def name: String + + def onReload(newConfig: String): Unit +} diff --git a/traces/commons/src/main/scala/com/expedia/www/haystack/trace/commons/packer/PackedMessage.scala b/traces/commons/src/main/scala/com/expedia/www/haystack/trace/commons/packer/PackedMessage.scala new file mode 100644 index 000000000..9197f4d76 --- /dev/null +++ b/traces/commons/src/main/scala/com/expedia/www/haystack/trace/commons/packer/PackedMessage.scala @@ -0,0 +1,50 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trace.commons.packer + +import java.nio.ByteBuffer + +import com.google.protobuf.GeneratedMessageV3 +import org.json4s.jackson.Serialization +import org.json4s.{DefaultFormats, Formats} + +object PackedMessage { + implicit val formats: Formats = DefaultFormats + new org.json4s.ext.EnumSerializer(PackerType) + val MAGIC_BYTES: Array[Byte] = "hytc".getBytes("utf-8") +} + +case class PackedMessage[T <: GeneratedMessageV3](protoObj: T, + private val pack: (T => Array[Byte]), + private val metadata: PackedMetadata) { + import PackedMessage._ + private lazy val metadataBytes: Array[Byte] = Serialization.write(metadata).getBytes("utf-8") + + val packedDataBytes: Array[Byte] = { + val packedDataBytes = pack(protoObj) + if (PackerType.NONE == metadata.t) { + packedDataBytes + } else { + ByteBuffer + .allocate(MAGIC_BYTES.length + 4 + metadataBytes.length + packedDataBytes.length) + .put(MAGIC_BYTES) + .putInt(metadataBytes.length) + .put(metadataBytes) + .put(packedDataBytes).array() + } + } +} diff --git a/traces/commons/src/main/scala/com/expedia/www/haystack/trace/commons/packer/Packer.scala b/traces/commons/src/main/scala/com/expedia/www/haystack/trace/commons/packer/Packer.scala new file mode 100644 index 000000000..370829225 --- /dev/null +++ b/traces/commons/src/main/scala/com/expedia/www/haystack/trace/commons/packer/Packer.scala @@ -0,0 +1,77 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trace.commons.packer + +import java.io.{ByteArrayInputStream, ByteArrayOutputStream, OutputStream} +import java.util.zip.GZIPOutputStream + +import com.expedia.www.haystack.trace.commons.packer.PackerType.PackerType +import com.github.luben.zstd.ZstdOutputStream +import com.google.protobuf.GeneratedMessageV3 +import org.apache.commons.io.IOUtils +import org.xerial.snappy.SnappyOutputStream + +object PackerType extends Enumeration { + type PackerType = Value + val GZIP, SNAPPY, NONE, ZSTD = Value +} + +case class PackedMetadata(t: PackerType) + +abstract class Packer[T <: GeneratedMessageV3] { + val packerType: PackerType + + protected def compressStream(stream: OutputStream): OutputStream + + private def pack(protoObj: T): Array[Byte] = { + val outStream = new ByteArrayOutputStream + val compressedStream = compressStream(outStream) + if (compressedStream != null) { + IOUtils.copy(new ByteArrayInputStream(protoObj.toByteArray), compressedStream) + compressedStream.close() // this flushes the data to final outStream + outStream.toByteArray + } else { + protoObj.toByteArray + } + } + + def apply(protoObj: T): PackedMessage[T] = { + PackedMessage(protoObj, pack, PackedMetadata(packerType)) + } +} + +class NoopPacker[T <: GeneratedMessageV3] extends Packer[T] { + override val packerType = PackerType.NONE + override protected def compressStream(stream: OutputStream): OutputStream = null +} + +class SnappyPacker[T <: GeneratedMessageV3] extends Packer[T] { + override val packerType = PackerType.SNAPPY + override protected def compressStream(stream: OutputStream): OutputStream = new SnappyOutputStream(stream) +} + + +class ZstdPacker[T <: GeneratedMessageV3] extends Packer[T] { + override val packerType = PackerType.ZSTD + override protected def compressStream(stream: OutputStream): OutputStream = new ZstdOutputStream(stream) +} + +class GzipPacker[T <: GeneratedMessageV3] extends Packer[T] { + override val packerType = PackerType.GZIP + override protected def compressStream(stream: OutputStream): OutputStream = new GZIPOutputStream(stream) +} diff --git a/traces/commons/src/main/scala/com/expedia/www/haystack/trace/commons/packer/PackerFactory.scala b/traces/commons/src/main/scala/com/expedia/www/haystack/trace/commons/packer/PackerFactory.scala new file mode 100644 index 000000000..820e3e9a4 --- /dev/null +++ b/traces/commons/src/main/scala/com/expedia/www/haystack/trace/commons/packer/PackerFactory.scala @@ -0,0 +1,32 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trace.commons.packer + +import com.expedia.open.tracing.buffer.SpanBuffer +import com.expedia.www.haystack.trace.commons.packer.PackerType.PackerType + +object PackerFactory { + def spanBufferPacker(`type`: PackerType): Packer[SpanBuffer] = { + `type` match { + case PackerType.SNAPPY => new SnappyPacker[SpanBuffer] + case PackerType.GZIP => new GzipPacker[SpanBuffer] + case PackerType.ZSTD => new ZstdPacker[SpanBuffer] + case _ => new NoopPacker[SpanBuffer] + } + } +} diff --git a/traces/commons/src/main/scala/com/expedia/www/haystack/trace/commons/packer/Unpacker.scala b/traces/commons/src/main/scala/com/expedia/www/haystack/trace/commons/packer/Unpacker.scala new file mode 100644 index 000000000..8afe55857 --- /dev/null +++ b/traces/commons/src/main/scala/com/expedia/www/haystack/trace/commons/packer/Unpacker.scala @@ -0,0 +1,80 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trace.commons.packer + +import java.io.{ByteArrayInputStream, ByteArrayOutputStream, InputStream} +import java.nio.ByteBuffer +import java.util.zip.GZIPInputStream + +import com.expedia.open.tracing.buffer.SpanBuffer +import com.github.luben.zstd.ZstdInputStream +import org.apache.commons.io.IOUtils +import org.json4s.jackson.Serialization +import org.xerial.snappy.SnappyInputStream + +object Unpacker { + import PackedMessage._ + + private def readMetadata(packedDataBytes: Array[Byte]): Array[Byte] = { + val byteBuffer = ByteBuffer.wrap(packedDataBytes) + val magicBytesExist = MAGIC_BYTES.indices forall { idx => byteBuffer.get() == MAGIC_BYTES.apply(idx) } + if (magicBytesExist) { + val headerLength = byteBuffer.getInt + val metadataBytes = new Array[Byte](headerLength) + byteBuffer.get(metadataBytes, 0, headerLength) + metadataBytes + } else { + null + } + } + + private def unpack(compressedStream: InputStream) = { + val outputStream = new ByteArrayOutputStream() + IOUtils.copy(compressedStream, outputStream) + outputStream.toByteArray + } + + def readSpanBuffer(packedDataBytes: Array[Byte]): SpanBuffer = { + var parsedDataBytes: Array[Byte] = null + val metadataBytes = readMetadata(packedDataBytes) + if (metadataBytes != null) { + val packedMetadata = Serialization.read[PackedMetadata](new String(metadataBytes)) + val compressedDataOffset = MAGIC_BYTES.length + 4 + metadataBytes.length + packedMetadata.t match { + case PackerType.SNAPPY => + parsedDataBytes = unpack( + new SnappyInputStream( + new ByteArrayInputStream(packedDataBytes, compressedDataOffset, packedDataBytes.length - compressedDataOffset))) + case PackerType.GZIP => + parsedDataBytes = unpack( + new GZIPInputStream( + new ByteArrayInputStream(packedDataBytes, compressedDataOffset, packedDataBytes.length - compressedDataOffset))) + case PackerType.ZSTD => + parsedDataBytes = unpack( + new ZstdInputStream( + new ByteArrayInputStream(packedDataBytes, compressedDataOffset, packedDataBytes.length - compressedDataOffset))) + case _ => + return SpanBuffer.parseFrom( + new ByteArrayInputStream(packedDataBytes, compressedDataOffset, packedDataBytes.length - compressedDataOffset)) + } + } else { + parsedDataBytes = packedDataBytes + } + SpanBuffer.parseFrom(parsedDataBytes) + } +} diff --git a/traces/commons/src/main/scala/com/expedia/www/haystack/trace/commons/utils/SpanUtils.scala b/traces/commons/src/main/scala/com/expedia/www/haystack/trace/commons/utils/SpanUtils.scala new file mode 100644 index 000000000..2017b5765 --- /dev/null +++ b/traces/commons/src/main/scala/com/expedia/www/haystack/trace/commons/utils/SpanUtils.scala @@ -0,0 +1,159 @@ +/* + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expedia.www.haystack.trace.commons.utils + +import com.expedia.open.tracing.Log +import com.expedia.open.tracing.Span +import com.expedia.open.tracing.Tag +import com.expedia.www.haystack.trace.commons.utils.SpanMarkers._ + +import scala.collection.JavaConverters._ + +object SpanUtils { + val URL_TAG_KEY = "url" + + def getEventTimestamp(span: Span, event: String): Long = { + span.getLogsList.asScala.find(log => { + log.getFieldsList.asScala.exists(tag => { + tag.getKey.equalsIgnoreCase(LOG_EVENT_TAG_KEY) && tag.getVStr.equalsIgnoreCase(event) + }) + }).get.getTimestamp + } + + def getEndTime(span: Span): Long = { + span.getStartTime + span.getDuration + } + + def isMergedSpan(span: Span): Boolean = { + containsClientLogTag(span) && containsServerLogTag(span) + } + + def spanKind(span: Span): String = { + val kind = span.getTagsList.asScala.find(_.getKey == SpanMarkers.SPAN_KIND_TAG_KEY).map(_.getVStr).getOrElse("") + if (kind == "") { + if (containsServerLogTag(span)) { + return SERVER_SPAN_KIND + } else if (containsClientLogTag(span)) { + return CLIENT_SPAN_KIND + } + } + kind + } + + def containsServerLogTag(span: Span): Boolean = { + containsLogTag(span, SERVER_RECV_EVENT) && containsLogTag(span, SERVER_SEND_EVENT) + } + + def getServiceTag(span: Span): Option[Tag] = { + span.getTagsList.asScala.find(tag => { + tag.getKey.equalsIgnoreCase(SERVICE_TAG_KEY) + }) + } + + def containsClientLogTag(span: Span): Boolean = { + containsLogTag(span, CLIENT_RECV_EVENT) && containsLogTag(span, CLIENT_SEND_EVENT) + } + + def addServerLogTag(span: Span): Span = { + val receiveEventLog = Log.newBuilder() + .setTimestamp(span.getStartTime) + .addFields( + Tag.newBuilder().setKey(LOG_EVENT_TAG_KEY).setVStr(SERVER_RECV_EVENT)) + + val sendEventLog = Log.newBuilder() + .setTimestamp(span.getStartTime + span.getDuration) + .addFields( + Tag.newBuilder().setKey(LOG_EVENT_TAG_KEY).setVStr(SERVER_SEND_EVENT) ) + + span + .toBuilder + .addLogs(receiveEventLog) + .addLogs(sendEventLog) + .build() + } + + def addClientLogTag(span: Span): Span = { + val sendEventLog = Log.newBuilder() + .setTimestamp(span.getStartTime) + .addFields( + Tag.newBuilder().setType(Tag.TagType.STRING).setKey(LOG_EVENT_TAG_KEY).setVStr(CLIENT_SEND_EVENT)) + + val receiveEventLog = Log.newBuilder() + .setTimestamp(span.getStartTime + span.getDuration) + .addFields( + Tag.newBuilder().setType(Tag.TagType.STRING).setKey(LOG_EVENT_TAG_KEY).setVStr(CLIENT_RECV_EVENT)) + + span + .toBuilder + .addLogs(sendEventLog) + .addLogs(receiveEventLog) + .build() + } + + private def containsLogTag(span: Span, event: String) = { + span.getLogsList.asScala.exists(log => { + log.getFieldsList.asScala.exists(tag => { + tag.getKey.equalsIgnoreCase(LOG_EVENT_TAG_KEY) && tag.getVStr.equalsIgnoreCase(event) + }) + }) + } + + def createAutoGeneratedRootSpan(spans: Seq[Span], + reason: String, + rootSpanId: String): Span.Builder = { + val spanWithEarliestStartTime = spans.minBy(_.getStartTime) + val spanWithLatestEndTime = spans.maxBy(span => span.getStartTime + span.getDuration) + + val startTime = spanWithEarliestStartTime.getStartTime + val duration = (spanWithLatestEndTime.getStartTime + spanWithLatestEndTime.getDuration) - startTime + + val autoGenSpanBuilder = Span.newBuilder() + .setServiceName(spanWithEarliestStartTime.getServiceName) + .setOperationName(AUTOGEN_OPERATION_NAME) + .setTraceId(spanWithEarliestStartTime.getTraceId) + .setSpanId(rootSpanId) + .setParentSpanId("") + .setStartTime(startTime) + .setDuration(duration) + .addTags(Tag.newBuilder().setKey(AUTOGEN_REASON_TAG).setVStr(reason).setType(Tag.TagType.STRING)) + .addTags(Tag.newBuilder().setKey(AUTOGEN_SPAN_ID_TAG).setVStr(rootSpanId).setType(Tag.TagType.STRING)) + .addTags(Tag.newBuilder().setKey(AUTOGEN_FLAG_TAG).setVBool(true).setType(Tag.TagType.BOOL)) + + spanWithEarliestStartTime.getTagsList.asScala.find(_.getKey.equalsIgnoreCase(URL_TAG_KEY)) match { + case Some(urlTag) => autoGenSpanBuilder.addTags(urlTag) + case _ => autoGenSpanBuilder + } + } +} + +object SpanMarkers { + val AUTOGEN_OPERATION_NAME = "auto-generated" + val AUTOGEN_REASON_TAG = "X-HAYSTACK-AUTOGEN-REASON" + val AUTOGEN_SPAN_ID_TAG = "X-HAYSTACK-AUTOGEN-SPAN-ID" + val AUTOGEN_FLAG_TAG = "X-HAYSTACK-AUTOGEN" + + val LOG_EVENT_TAG_KEY = "event" + val SERVER_SEND_EVENT = "ss" + val SERVER_RECV_EVENT = "sr" + val CLIENT_SEND_EVENT = "cs" + val CLIENT_RECV_EVENT = "cr" + + val SPAN_KIND_TAG_KEY = "span.kind" + val SERVICE_TAG_KEY = "service" + val SERVER_SPAN_KIND = "server" + val CLIENT_SPAN_KIND = "client" +} diff --git a/traces/commons/src/test/resources/logback-test.xml b/traces/commons/src/test/resources/logback-test.xml new file mode 100644 index 000000000..adfa02c68 --- /dev/null +++ b/traces/commons/src/test/resources/logback-test.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/traces/commons/src/test/scala/com/expedia/www/haystack/trace/commons/unit/BaseUnitTestSpec.scala b/traces/commons/src/test/scala/com/expedia/www/haystack/trace/commons/unit/BaseUnitTestSpec.scala new file mode 100644 index 000000000..fc12a7a6b --- /dev/null +++ b/traces/commons/src/test/scala/com/expedia/www/haystack/trace/commons/unit/BaseUnitTestSpec.scala @@ -0,0 +1,24 @@ +/* + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expedia.www.haystack.trace.commons.unit + +import org.scalatest.easymock.EasyMockSugar +import org.scalatest.FunSpec +import org.scalatest.GivenWhenThen +import org.scalatest.Matchers + +trait BaseUnitTestSpec extends FunSpec with GivenWhenThen with Matchers with EasyMockSugar diff --git a/traces/commons/src/test/scala/com/expedia/www/haystack/trace/commons/unit/PackerSpec.scala b/traces/commons/src/test/scala/com/expedia/www/haystack/trace/commons/unit/PackerSpec.scala new file mode 100644 index 000000000..de2947f16 --- /dev/null +++ b/traces/commons/src/test/scala/com/expedia/www/haystack/trace/commons/unit/PackerSpec.scala @@ -0,0 +1,61 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trace.commons.unit + +import java.util.UUID + +import com.expedia.open.tracing.{Span, Tag} +import com.expedia.open.tracing.buffer.SpanBuffer +import com.expedia.www.haystack.trace.commons.packer.{PackerFactory, PackerType, Unpacker} +import org.scalatest.{FunSpec, Matchers} +import org.scalatest.easymock.EasyMockSugar + +class PackerSpec extends FunSpec with Matchers with EasyMockSugar { + describe("A Packer") { + it("should pack and unpack spanBuffer proto object for all packer types") { + PackerType.values.foreach(packerType => { + val packer = PackerFactory.spanBufferPacker(packerType) + val span_1 = Span.newBuilder() + .setTraceId(UUID.randomUUID().toString) + .setSpanId(UUID.randomUUID().toString) + .setServiceName("test_service") + .setOperationName("/foo") + .addTags(Tag.newBuilder().setKey("error").setVBool(false)) + .addTags(Tag.newBuilder().setKey("http.status_code").setVLong(200)) + .addTags(Tag.newBuilder().setKey("version").setVStr("1.1")) + .build() + val span_2 = Span.newBuilder() + .setTraceId(UUID.randomUUID().toString) + .setSpanId(UUID.randomUUID().toString) + .setParentSpanId(UUID.randomUUID().toString) + .setServiceName("another_test_service") + .setOperationName("/bar") + .addTags(Tag.newBuilder().setKey("error").setVBool(true)) + .addTags(Tag.newBuilder().setKey("http.status_code").setVLong(404)) + .addTags(Tag.newBuilder().setKey("version").setVStr("1.2")) + .build() + val spanBuffer = SpanBuffer.newBuilder().setTraceId("trace-1").addChildSpans(span_1).addChildSpans(span_2).build() + val packedMessage = packer.apply(spanBuffer) + val packedDataBytes = packedMessage.packedDataBytes + packedDataBytes should not be null + val spanBufferProto = Unpacker.readSpanBuffer(packedDataBytes) + spanBufferProto shouldEqual spanBuffer + }) + } + } +} diff --git a/traces/commons/src/test/scala/com/expedia/www/haystack/trace/commons/unit/TraceIndexDocSpec.scala b/traces/commons/src/test/scala/com/expedia/www/haystack/trace/commons/unit/TraceIndexDocSpec.scala new file mode 100644 index 000000000..37bbe53a5 --- /dev/null +++ b/traces/commons/src/test/scala/com/expedia/www/haystack/trace/commons/unit/TraceIndexDocSpec.scala @@ -0,0 +1,34 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trace.commons.unit + +import com.expedia.www.haystack.trace.commons.clients.es.document.TraceIndexDoc +import org.scalatest.{FunSpec, Matchers} + +import scala.collection.mutable + +class TraceIndexDocSpec extends FunSpec with Matchers { + describe("TraceIndex Document") { + it("should produce the valid json document for indexing") { + val startTime = 1528715319040L + val spanDoc = mutable.Map("spanid" -> "SPAN-1", "operatioName" -> "op1", "serviceName" -> "svc", "duration" -> 100, "starttime" -> startTime) + val indexDoc = TraceIndexDoc("trace-id", 100L, startTime, Seq(spanDoc)) + indexDoc.json shouldEqual "{\"traceid\":\"trace-id\",\"rootduration\":100,\"starttime\":1528715319040,\"spans\":[{\"spanid\":\"SPAN-1\",\"serviceName\":\"svc\",\"starttime\":1528715319040,\"operatioName\":\"op1\",\"duration\":100}]}" + } + } +} diff --git a/traces/commons/src/test/scala/com/expedia/www/haystack/trace/commons/unit/WhitelistIndexFieldConfigurationSpec.scala b/traces/commons/src/test/scala/com/expedia/www/haystack/trace/commons/unit/WhitelistIndexFieldConfigurationSpec.scala new file mode 100644 index 000000000..24febefb0 --- /dev/null +++ b/traces/commons/src/test/scala/com/expedia/www/haystack/trace/commons/unit/WhitelistIndexFieldConfigurationSpec.scala @@ -0,0 +1,79 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trace.commons.unit + +import com.expedia.www.haystack.trace.commons.config.entities.{IndexFieldType, WhiteListIndexFields, WhitelistIndexField, WhitelistIndexFieldConfiguration} +import org.json4s.ext.EnumNameSerializer +import org.json4s.jackson.Serialization +import org.json4s.{DefaultFormats, Formats} +import org.scalatest.{Entry, FunSpec, Matchers} + +import scala.collection.JavaConverters._ + +class WhitelistIndexFieldConfigurationSpec extends FunSpec with Matchers { + + protected implicit val formats: Formats = DefaultFormats + new EnumNameSerializer(IndexFieldType) + + describe("whitelist field configuration") { + it("an empty configuration should return whitelist fields as empty") { + val config = WhitelistIndexFieldConfiguration() + config.indexFieldMap shouldBe 'empty + config.whitelistIndexFields shouldBe 'empty + } + + it("a loaded configuration should return the non empty whitelist fields") { + val whitelistField_1 = WhitelistIndexField(name = "role", `type` = IndexFieldType.string, enableRangeQuery = true) + val whitelistField_2 = WhitelistIndexField(name = "Errorcode", `type` = IndexFieldType.long) + + val config = WhitelistIndexFieldConfiguration() + val cfgJsonData = Serialization.write(WhiteListIndexFields(List(whitelistField_1, whitelistField_2))) + + // reload + config.onReload(cfgJsonData) + + config.whitelistIndexFields.map(_.name) should contain allOf("role", "errorcode") + config.whitelistIndexFields.filter(r => r.name == "role").head.enableRangeQuery shouldBe true + config.indexFieldMap.size() shouldBe 2 + config.indexFieldMap.keys().asScala.toList should contain allOf("role", "errorcode") + config.globalTraceContextIndexFieldNames.size shouldBe 0 + + val whitelistField_3 = WhitelistIndexField(name = "status", `type` = IndexFieldType.string, aliases = Set("_status", "HTTP-STATUS")) + val whitelistField_4 = WhitelistIndexField(name = "something", `type` = IndexFieldType.long, searchContext = "trace") + + val newCfgJsonData = Serialization.write(WhiteListIndexFields(List(whitelistField_1, whitelistField_3, whitelistField_4))) + config.onReload(newCfgJsonData) + + config.whitelistIndexFields.size shouldBe 5 + config.whitelistIndexFields.map(_.name).toSet should contain allOf("status", "something", "role") + config.indexFieldMap.size shouldBe 5 + config.indexFieldMap.keys().asScala.toList should contain allOf("status", "something", "role", "http-status", "_status") + + config.onReload(newCfgJsonData) + config.whitelistIndexFields.size shouldBe 5 + config.whitelistIndexFields.map(_.name).toSet should contain allOf("status", "something", "role") + config.indexFieldMap.size() shouldBe 5 + config.indexFieldMap.keys().asScala.toList should contain allOf("status", "something", "role", "http-status", "_status") + + config.indexFieldMap.get("http-status").name shouldEqual "status" + config.indexFieldMap.get("_status").name shouldEqual "status" + + config.globalTraceContextIndexFieldNames.size shouldBe 1 + config.globalTraceContextIndexFieldNames.head shouldEqual "something" + } + } +} diff --git a/traces/deployment/scripts/publish-to-docker-hub.sh b/traces/deployment/scripts/publish-to-docker-hub.sh new file mode 100755 index 000000000..0ff8e3bf4 --- /dev/null +++ b/traces/deployment/scripts/publish-to-docker-hub.sh @@ -0,0 +1,43 @@ +#!/bin/bash + +set -e + +QUALIFIED_DOCKER_IMAGE_NAME=$DOCKER_ORG/$DOCKER_IMAGE_NAME +echo "DOCKER_ORG=$DOCKER_ORG, DOCKER_IMAGE_NAME=$DOCKER_IMAGE_NAME, QUALIFIED_DOCKER_IMAGE_NAME=$QUALIFIED_DOCKER_IMAGE_NAME" +echo "BRANCH=$BRANCH, TAG=$TAG, SHA=$SHA" + +# login +docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD + +# Add tags +if [[ $TAG =~ ([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then + echo "releasing semantic versions" + + unset MAJOR MINOR PATCH + MAJOR="${BASH_REMATCH[1]}" + MINOR="${BASH_REMATCH[2]}" + PATCH="${BASH_REMATCH[3]}" + + # for tag, add MAJOR, MAJOR.MINOR, MAJOR.MINOR.PATCH and latest as tag + # publish image with tags + docker tag $DOCKER_IMAGE_NAME $QUALIFIED_DOCKER_IMAGE_NAME:$MAJOR + docker push $QUALIFIED_DOCKER_IMAGE_NAME:$MAJOR + + docker tag $DOCKER_IMAGE_NAME $QUALIFIED_DOCKER_IMAGE_NAME:$MAJOR.$MINOR + docker push $QUALIFIED_DOCKER_IMAGE_NAME:$MAJOR.$MINOR + + docker tag $DOCKER_IMAGE_NAME $QUALIFIED_DOCKER_IMAGE_NAME:$MAJOR.$MINOR.$PATCH + docker push $QUALIFIED_DOCKER_IMAGE_NAME:$MAJOR.$MINOR.$PATCH + + docker tag $DOCKER_IMAGE_NAME $QUALIFIED_DOCKER_IMAGE_NAME:latest + docker push $QUALIFIED_DOCKER_IMAGE_NAME:latest + +elif [[ "$BRANCH" == "master" ]]; then + echo "releasing master branch" + + # for 'master' branch, add SHA as tags + docker tag $DOCKER_IMAGE_NAME $QUALIFIED_DOCKER_IMAGE_NAME:$SHA + + # publish image with tags + docker push $QUALIFIED_DOCKER_IMAGE_NAME +fi diff --git a/traces/deployment/terraform/es-indices/curator-service-metadata/main.tf b/traces/deployment/terraform/es-indices/curator-service-metadata/main.tf new file mode 100755 index 000000000..9b14010c0 --- /dev/null +++ b/traces/deployment/terraform/es-indices/curator-service-metadata/main.tf @@ -0,0 +1,29 @@ +locals { + count = "${var.enabled?1:0}" +} + + +data "template_file" "curator_cron_job" { + template = "${file("${path.module}/templates/curator-cron-job-yaml.tpl")}" + vars { + elasticsearch_host = "${var.elasticsearch_hostname}" + elasticsearch_port = "${var.elasticsearch_port}" + app_namespace = "${var.namespace}" + } +} +resource "null_resource" "curator_addons" { + triggers { + template = "${data.template_file.curator_cron_job.rendered}" + } + provisioner "local-exec" { + command = "echo '${data.template_file.curator_cron_job.rendered}' | ${var.kubectl_executable_name} apply -f - --context ${var.kubectl_context_name}" + } + + provisioner "local-exec" { + command = "echo '${data.template_file.curator_cron_job.rendered}' | ${var.kubectl_executable_name} delete -f - --context ${var.kubectl_context_name}" + when = "destroy" + } + count = "${local.count}" +} + + diff --git a/traces/deployment/terraform/es-indices/curator-service-metadata/outputs.tf b/traces/deployment/terraform/es-indices/curator-service-metadata/outputs.tf new file mode 100755 index 000000000..e69de29bb diff --git a/traces/deployment/terraform/es-indices/curator-service-metadata/templates/curator-cron-job-yaml.tpl b/traces/deployment/terraform/es-indices/curator-service-metadata/templates/curator-cron-job-yaml.tpl new file mode 100755 index 000000000..325452768 --- /dev/null +++ b/traces/deployment/terraform/es-indices/curator-service-metadata/templates/curator-cron-job-yaml.tpl @@ -0,0 +1,79 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: curator-es-service-metadata-index-store + namespace: ${app_namespace} + labels: + app: curator-es-service-metadata-index-store +data: + curator.yml: |- + client: + hosts: + - ${elasticsearch_host} + port: ${elasticsearch_port} + url_prefix: + use_ssl: False + certificate: + client_cert: + client_key: + aws_key: + aws_secret_key: + aws_region: + ssl_no_validate: False + http_auth: + timeout: 30 + master_only: False + logging: + loglevel: DEBUG + logfile: + logformat: default + blacklist: ['elasticsearch', 'urllib3'] + actions.yml: |- + actions: + 1: + action: delete_indices + options: + ignore_empty_list: True + timeout_override: + continue_if_exception: False + disable_action: False + filters: + - filtertype: pattern + kind: prefix + value: service-metadata- + exclude: + - filtertype: age + source: name + direction: older + timestring: "%Y-%m-%d" + unit: days + unit_count: 4 + exclude: +--- +apiVersion: batch/v1beta1 +kind: CronJob +metadata: + name: curator-es-service-metadata-index-store + namespace: ${app_namespace} + +spec: + schedule: "0 */4 * * *" + jobTemplate: + spec: + template: + spec: + containers: + - name: curator-es-service-metadata-index-store + image: bobrik/curator:5.4.0 + args: + - --config + - /config/curator.yml + - /config/actions.yml + volumeMounts: + - mountPath: /config + name: config + restartPolicy: OnFailure + volumes: + - name: config + configMap: + name: curator-es-service-metadata-index-store diff --git a/traces/deployment/terraform/es-indices/curator-service-metadata/variables.tf b/traces/deployment/terraform/es-indices/curator-service-metadata/variables.tf new file mode 100755 index 000000000..706d95c4f --- /dev/null +++ b/traces/deployment/terraform/es-indices/curator-service-metadata/variables.tf @@ -0,0 +1,7 @@ +variable "kubectl_executable_name" {} +variable "kubectl_context_name" {} +variable "enabled" {} +variable "elasticsearch_hostname" {} +variable "elasticsearch_port" {} +variable "namespace" {} + diff --git a/traces/deployment/terraform/es-indices/curator-trace-index/main.tf b/traces/deployment/terraform/es-indices/curator-trace-index/main.tf new file mode 100755 index 000000000..9b14010c0 --- /dev/null +++ b/traces/deployment/terraform/es-indices/curator-trace-index/main.tf @@ -0,0 +1,29 @@ +locals { + count = "${var.enabled?1:0}" +} + + +data "template_file" "curator_cron_job" { + template = "${file("${path.module}/templates/curator-cron-job-yaml.tpl")}" + vars { + elasticsearch_host = "${var.elasticsearch_hostname}" + elasticsearch_port = "${var.elasticsearch_port}" + app_namespace = "${var.namespace}" + } +} +resource "null_resource" "curator_addons" { + triggers { + template = "${data.template_file.curator_cron_job.rendered}" + } + provisioner "local-exec" { + command = "echo '${data.template_file.curator_cron_job.rendered}' | ${var.kubectl_executable_name} apply -f - --context ${var.kubectl_context_name}" + } + + provisioner "local-exec" { + command = "echo '${data.template_file.curator_cron_job.rendered}' | ${var.kubectl_executable_name} delete -f - --context ${var.kubectl_context_name}" + when = "destroy" + } + count = "${local.count}" +} + + diff --git a/traces/deployment/terraform/es-indices/curator-trace-index/outputs.tf b/traces/deployment/terraform/es-indices/curator-trace-index/outputs.tf new file mode 100755 index 000000000..e69de29bb diff --git a/traces/deployment/terraform/es-indices/curator-trace-index/templates/curator-cron-job-yaml.tpl b/traces/deployment/terraform/es-indices/curator-trace-index/templates/curator-cron-job-yaml.tpl new file mode 100755 index 000000000..45a0eb58e --- /dev/null +++ b/traces/deployment/terraform/es-indices/curator-trace-index/templates/curator-cron-job-yaml.tpl @@ -0,0 +1,79 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: curator-es-haystack-traces-index-store + namespace: ${app_namespace} + labels: + app: curator-es-haystack-traces-index-store +data: + curator.yml: |- + client: + hosts: + - ${elasticsearch_host} + port: ${elasticsearch_port} + url_prefix: + use_ssl: False + certificate: + client_cert: + client_key: + aws_key: + aws_secret_key: + aws_region: + ssl_no_validate: False + http_auth: + timeout: 30 + master_only: False + logging: + loglevel: DEBUG + logfile: + logformat: default + blacklist: ['elasticsearch', 'urllib3'] + actions.yml: |- + actions: + 1: + action: delete_indices + options: + ignore_empty_list: True + timeout_override: + continue_if_exception: False + disable_action: False + filters: + - filtertype: pattern + kind: prefix + value: haystack-traces- + exclude: + - filtertype: age + source: name + direction: older + timestring: "%Y-%m-%d" + unit: days + unit_count: 4 + exclude: +--- +apiVersion: batch/v1beta1 +kind: CronJob +metadata: + name: curator-es-haystack-traces-index-store + namespace: ${app_namespace} + +spec: + schedule: "0 */4 * * *" + jobTemplate: + spec: + template: + spec: + containers: + - name: curator-es-haystack-traces-index-store + image: bobrik/curator:5.4.0 + args: + - --config + - /config/curator.yml + - /config/actions.yml + volumeMounts: + - mountPath: /config + name: config + restartPolicy: OnFailure + volumes: + - name: config + configMap: + name: curator-es-haystack-traces-index-store diff --git a/traces/deployment/terraform/es-indices/curator-trace-index/variables.tf b/traces/deployment/terraform/es-indices/curator-trace-index/variables.tf new file mode 100755 index 000000000..706d95c4f --- /dev/null +++ b/traces/deployment/terraform/es-indices/curator-trace-index/variables.tf @@ -0,0 +1,7 @@ +variable "kubectl_executable_name" {} +variable "kubectl_context_name" {} +variable "enabled" {} +variable "elasticsearch_hostname" {} +variable "elasticsearch_port" {} +variable "namespace" {} + diff --git a/traces/deployment/terraform/es-indices/main.tf b/traces/deployment/terraform/es-indices/main.tf new file mode 100644 index 000000000..1036902e0 --- /dev/null +++ b/traces/deployment/terraform/es-indices/main.tf @@ -0,0 +1,29 @@ +module "curator_trace_index" { + source = "curator-trace-index" + kubectl_context_name = "${var.kubectl_context_name}" + enabled = "${var.enabled}" + elasticsearch_hostname = "${var.elasticsearch_hostname}" + elasticsearch_port = "${var.elasticsearch_port}" + kubectl_executable_name = "${var.kubectl_executable_name}" + namespace = "${var.namespace}" +} + +module "curator_service_metadata" { + source = "curator-service-metadata" + kubectl_context_name = "${var.kubectl_context_name}" + enabled = "${var.enabled}" + elasticsearch_hostname = "${var.elasticsearch_hostname}" + elasticsearch_port = "${var.elasticsearch_port}" + kubectl_executable_name = "${var.kubectl_executable_name}" + namespace = "${var.namespace}" +} + +module "whitelisted_fields" { + source = "whitelisted-fields" + kubectl_context_name = "${var.kubectl_context_name}" + enabled = "${var.enabled}" + elasticsearch_hostname = "${var.elasticsearch_hostname}" + elasticsearch_port = "${var.elasticsearch_port}" + kubectl_executable_name = "${var.kubectl_executable_name}" + namespace = "${var.namespace}" +} diff --git a/traces/deployment/terraform/es-indices/outputs.tf b/traces/deployment/terraform/es-indices/outputs.tf new file mode 100644 index 000000000..e69de29bb diff --git a/traces/deployment/terraform/es-indices/variables.tf b/traces/deployment/terraform/es-indices/variables.tf new file mode 100644 index 000000000..bacd8c4c8 --- /dev/null +++ b/traces/deployment/terraform/es-indices/variables.tf @@ -0,0 +1,7 @@ +variable "elasticsearch_hostname" {} +variable "elasticsearch_port" {} +variable "kubectl_context_name" {} +variable "kubectl_executable_name" {} +variable "namespace" {} +variable "node_selector_label"{} +variable "enabled"{} diff --git a/traces/deployment/terraform/es-indices/whitelisted-fields/main.tf b/traces/deployment/terraform/es-indices/whitelisted-fields/main.tf new file mode 100755 index 000000000..3fd7ff4c0 --- /dev/null +++ b/traces/deployment/terraform/es-indices/whitelisted-fields/main.tf @@ -0,0 +1,29 @@ +locals { + count = "${var.enabled?1:0}" +} + + +data "template_file" "whitelisted-fields-pod-yaml" { + template = "${file("${path.module}/templates/whitelisted-fields-pod-yaml.tpl")}" + vars { + elasticsearch_host = "${var.elasticsearch_hostname}" + elasticsearch_port = "${var.elasticsearch_port}" + app_namespace = "${var.namespace}" + } +} +resource "null_resource" "whitelisted-fields-pod" { + triggers { + template = "${data.template_file.whitelisted-fields-pod-yaml.rendered}" + } + provisioner "local-exec" { + command = "echo '${data.template_file.whitelisted-fields-pod-yaml.rendered}' | ${var.kubectl_executable_name} apply -f - --context ${var.kubectl_context_name}" + } + + provisioner "local-exec" { + command = "echo '${data.template_file.whitelisted-fields-pod-yaml.rendered}' | ${var.kubectl_executable_name} delete -f - --context ${var.kubectl_context_name}" + when = "destroy" + } + count = "${local.count}" +} + + diff --git a/traces/deployment/terraform/es-indices/whitelisted-fields/outputs.tf b/traces/deployment/terraform/es-indices/whitelisted-fields/outputs.tf new file mode 100755 index 000000000..e69de29bb diff --git a/traces/deployment/terraform/es-indices/whitelisted-fields/templates/whitelisted-fields-pod-yaml.tpl b/traces/deployment/terraform/es-indices/whitelisted-fields/templates/whitelisted-fields-pod-yaml.tpl new file mode 100755 index 000000000..94c66e15b --- /dev/null +++ b/traces/deployment/terraform/es-indices/whitelisted-fields/templates/whitelisted-fields-pod-yaml.tpl @@ -0,0 +1,44 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: whitelist-json + namespace: ${app_namespace} +data: + whitelist.json: |- + { + "fields": [{ + "name": "error", + "type": "string", + "enabled": true, + "searchContext": "trace" + }] + } +--- +apiVersion: batch/v1 +kind: Job +metadata: + name: es-whitelist + namespace: ${app_namespace} +spec: + template: + spec: + containers: + - name: es-whitelist + image: yauritux/busybox-curl + command: + - curl + args: + - -XPUT + - -H + - "Content-Type: application/json" + - -d + - "@/data/whitelist.json" + - "http://${elasticsearch_host}:${elasticsearch_port}/reload-configs/indexing-fields/1" + volumeMounts: + - mountPath: /data + name: data + restartPolicy: OnFailure + volumes: + - name: data + configMap: + name: whitelist-json \ No newline at end of file diff --git a/traces/deployment/terraform/es-indices/whitelisted-fields/variables.tf b/traces/deployment/terraform/es-indices/whitelisted-fields/variables.tf new file mode 100755 index 000000000..706d95c4f --- /dev/null +++ b/traces/deployment/terraform/es-indices/whitelisted-fields/variables.tf @@ -0,0 +1,7 @@ +variable "kubectl_executable_name" {} +variable "kubectl_context_name" {} +variable "enabled" {} +variable "elasticsearch_hostname" {} +variable "elasticsearch_port" {} +variable "namespace" {} + diff --git a/traces/deployment/terraform/main.tf b/traces/deployment/terraform/main.tf new file mode 100644 index 000000000..c208e9976 --- /dev/null +++ b/traces/deployment/terraform/main.tf @@ -0,0 +1,71 @@ +module "trace-indexer" { + source = "trace-indexer" + image = "expediadotcom/haystack-trace-indexer:${var.traces["version"]}" + storage_backend_image = "expediadotcom/haystack-trace-backend-cassandra:${var.traces["version"]}" + replicas = "${var.traces["indexer_instances"]}" + enabled = "${var.traces["enabled"]}" + cpu_limit = "${var.traces["indexer_cpu_limit"]}" + cpu_request = "${var.traces["indexer_cpu_request"]}" + memory_limit = "${var.traces["indexer_memory_limit"]}" + memory_request = "${var.traces["indexer_memory_request"]}" + jvm_memory_limit = "${var.traces["indexer_jvm_memory_limit"]}" + env_vars = "${var.traces["indexer_environment_overrides"]}" + elasticsearch_template = "${var.traces["indexer_elasticsearch_template"]}" + namespace = "${var.namespace}" + kafka_endpoint = "${var.kafka_hostname}:${var.kafka_port}" + elasticsearch_port = "${var.elasticsearch_port}" + elasticsearch_hostname = "${var.elasticsearch_hostname}" + cassandra_hostname = "${var.cassandra_hostname}" + graphite_hostname = "${var.graphite_hostname}" + graphite_port = "${var.graphite_port}" + graphite_enabled = "${var.graphite_enabled}" + node_selector_label = "${var.node_selector_label}" + kubectl_executable_name = "${var.kubectl_executable_name}" + kubectl_context_name = "${var.kubectl_context_name}" + backend_cpu_limit = "${var.traces["backend_cpu_limit"]}" + backend_cpu_request = "${var.traces["backend_cpu_request"]}" + backend_memory_limit = "${var.traces["backend_memory_limit"]}" + backend_memory_request = "${var.traces["backend_memory_request"]}" + backend_jvm_memory_limit = "${var.traces["backend_jvm_memory_limit"]}" + backend_env_vars = "${var.traces["backend_environment_overrides"]}" +} + +module "trace-reader" { + source = "trace-reader" + image = "expediadotcom/haystack-trace-reader:${var.traces["version"]}" + storage_backend_image = "expediadotcom/haystack-trace-backend-cassandra:${var.traces["version"]}" + replicas = "${var.traces["reader_instances"]}" + namespace = "${var.namespace}" + elasticsearch_endpoint = "${var.elasticsearch_hostname}:${var.elasticsearch_port}" + cassandra_hostname = "${var.cassandra_hostname}" + graphite_hostname = "${var.graphite_hostname}" + graphite_port = "${var.graphite_port}" + graphite_enabled = "${var.graphite_enabled}" + enabled = "${var.traces["enabled"]}" + node_selector_label = "${var.node_selector_label}" + kubectl_executable_name = "${var.kubectl_executable_name}" + kubectl_context_name = "${var.kubectl_context_name}" + cpu_limit = "${var.traces["reader_cpu_limit"]}" + cpu_request = "${var.traces["reader_cpu_request"]}" + memory_limit = "${var.traces["reader_memory_limit"]}" + memory_request = "${var.traces["reader_memory_request"]}" + jvm_memory_limit = "${var.traces["reader_jvm_memory_limit"]}" + backend_cpu_limit = "${var.traces["backend_cpu_limit"]}" + backend_cpu_request = "${var.traces["backend_cpu_request"]}" + backend_memory_limit = "${var.traces["backend_memory_limit"]}" + backend_memory_request = "${var.traces["backend_memory_request"]}" + backend_jvm_memory_limit = "${var.traces["backend_jvm_memory_limit"]}" + env_vars = "${var.traces["reader_environment_overrides"]}" + backend_env_vars = "${var.traces["backend_environment_overrides"]}" +} + +module "es-indices" { + source = "es-indices" + enabled = "${var.traces["enabled"]}" + namespace = "${var.namespace}" + node_selector_label = "${var.node_selector_label}" + kubectl_executable_name = "${var.kubectl_executable_name}" + kubectl_context_name = "${var.kubectl_context_name}" + elasticsearch_port = "${var.elasticsearch_port}" + elasticsearch_hostname = "${var.elasticsearch_hostname}" +} diff --git a/traces/deployment/terraform/outputs.tf b/traces/deployment/terraform/outputs.tf new file mode 100644 index 000000000..27ecf772c --- /dev/null +++ b/traces/deployment/terraform/outputs.tf @@ -0,0 +1,7 @@ +output "reader_hostname" { + value = "${module.trace-reader.hostname}" +} + +output "reader_port" { + value = "${module.trace-reader.service_port}" +} \ No newline at end of file diff --git a/traces/deployment/terraform/trace-indexer/main.tf b/traces/deployment/terraform/trace-indexer/main.tf new file mode 100644 index 000000000..5ea2c0f0a --- /dev/null +++ b/traces/deployment/terraform/trace-indexer/main.tf @@ -0,0 +1,82 @@ +locals { + app_name = "trace-indexer" + config_file_path = "${path.module}/templates/trace-indexer.conf" + deployment_yaml_file_path = "${path.module}/templates/deployment.yaml" + count = "${var.enabled?1:0}" + span_produce_topic = "${var.enable_kafka_sink?"span-buffer":""}" + elasticsearch_endpoint = "${var.elasticsearch_hostname}:${var.elasticsearch_port}" + checksum = "${sha1("${data.template_file.config_data.rendered}")}" + configmap_name = "indexer-${local.checksum}" +} + +resource "kubernetes_config_map" "haystack-config" { + metadata { + name = "${local.configmap_name}" + namespace = "${var.namespace}" + } + data { + "trace-indexer.conf" = "${data.template_file.config_data.rendered}" + } + count = "${local.count}" +} + +data "template_file" "config_data" { + template = "${file("${local.config_file_path}")}" + + vars { + kafka_endpoint = "${var.kafka_endpoint}" + elasticsearch_endpoint = "${local.elasticsearch_endpoint}" + elasticsearch_template = "${var.elasticsearch_template}" + span_produce_topic = "${local.span_produce_topic}" + } +} + +data "template_file" "deployment_yaml" { + template = "${file("${local.deployment_yaml_file_path}")}" + vars { + app_name = "${local.app_name}" + namespace = "${var.namespace}" + graphite_port = "${var.graphite_port}" + graphite_host = "${var.graphite_hostname}" + graphite_enabled = "${var.graphite_enabled}" + cassandra_hostname = "${var.cassandra_hostname}" + node_selecter_label = "${var.node_selector_label}" + storage_backend_image = "${var.storage_backend_image}" + image = "${var.image}" + replicas = "${var.replicas}" + memory_limit = "${var.memory_limit}" + memory_request = "${var.memory_request}" + jvm_memory_limit = "${var.jvm_memory_limit}" + cpu_limit = "${var.cpu_limit}" + cpu_request = "${var.cpu_request}" + backend_memory_limit = "${var.backend_memory_limit}" + backend_memory_request = "${var.backend_memory_request}" + backend_jvm_memory_limit = "${var.backend_jvm_memory_limit}" + backend_cpu_limit = "${var.backend_cpu_limit}" + backend_cpu_request = "${var.backend_cpu_request}" + + configmap_name = "${local.configmap_name}" + env_vars= "${indent(9,"${var.env_vars}")}" + backend_env_vars = "${indent(9,"${var.backend_env_vars}")}" + } +} + +resource "null_resource" "kubectl_apply" { + triggers { + template = "${data.template_file.deployment_yaml.rendered}" + } + provisioner "local-exec" { + command = "echo '${data.template_file.deployment_yaml.rendered}' | ${var.kubectl_executable_name} apply -f - --context ${var.kubectl_context_name}" + } + count = "${local.count}" +} + + +resource "null_resource" "kubectl_destroy" { + + provisioner "local-exec" { + command = "echo '${data.template_file.deployment_yaml.rendered}' | ${var.kubectl_executable_name} delete -f - --context ${var.kubectl_context_name}" + when = "destroy" + } + count = "${local.count}" +} diff --git a/traces/deployment/terraform/trace-indexer/outputs.tf b/traces/deployment/terraform/trace-indexer/outputs.tf new file mode 100644 index 000000000..e69de29bb diff --git a/traces/deployment/terraform/trace-indexer/templates/deployment.yaml b/traces/deployment/terraform/trace-indexer/templates/deployment.yaml new file mode 100644 index 000000000..f1d89090c --- /dev/null +++ b/traces/deployment/terraform/trace-indexer/templates/deployment.yaml @@ -0,0 +1,97 @@ +# ------------------- Deployment ------------------- # + +kind: Deployment +apiVersion: apps/v1beta2 +metadata: + labels: + k8s-app: ${app_name} + name: ${app_name} + namespace: ${namespace} +spec: + replicas: ${replicas} + revisionHistoryLimit: 10 + selector: + matchLabels: + k8s-app: ${app_name} + template: + metadata: + labels: + k8s-app: ${app_name} + spec: + containers: + - name: storage-backend-cassandra + image: ${storage_backend_image} + resources: + limits: + cpu: ${backend_cpu_limit} + memory: ${backend_memory_limit}Mi + requests: + cpu: ${backend_cpu_request} + memory: ${backend_memory_request}Mi + env: + - name: "HAYSTACK_PROP_CASSANDRA_ENDPOINTS" + value: "${cassandra_hostname}" + - name: "HAYSTACK_GRAPHITE_HOST" + value: "${graphite_host}" + - name: "HAYSTACK_GRAPHITE_PORT" + value: "${graphite_port}" + - name: "HAYSTACK_GRAPHITE_ENABLED" + value: "${graphite_enabled}" + - name: "JAVA_XMS" + value: "${backend_jvm_memory_limit}m" + - name: "JAVA_XMX" + value: "${backend_jvm_memory_limit}m" + ${backend_env_vars} + livenessProbe: + exec: + command: + - /bin/grpc_health_probe + - "-addr=:8090" + initialDelaySeconds: 30 + periodSeconds: 15 + timeoutSeconds: 5 + failureThreshold: 3 + - name: ${app_name} + image: ${image} + volumeMounts: + # Create on-disk volume to store exec logs + - mountPath: /config + name: config-volume + resources: + limits: + cpu: ${cpu_limit} + memory: ${memory_limit}Mi + requests: + cpu: ${cpu_request} + memory: ${memory_request}Mi + env: + - name: "HAYSTACK_OVERRIDES_CONFIG_PATH" + value: "/config/trace-indexer.conf" + - name: "HAYSTACK_GRAPHITE_HOST" + value: "${graphite_host}" + - name: "HAYSTACK_GRAPHITE_PORT" + value: "${graphite_port}" + - name: "HAYSTACK_GRAPHITE_ENABLED" + value: "${graphite_enabled}" + - name: "JAVA_XMS" + value: "${jvm_memory_limit}m" + - name: "JAVA_XMX" + value: "${jvm_memory_limit}m" + ${env_vars} + livenessProbe: + exec: + command: + - grep + - "true" + - /app/isHealthy + initialDelaySeconds: 30 + periodSeconds: 5 + timeoutSeconds: 5 + failureThreshold: 6 + nodeSelector: + ${node_selecter_label} + volumes: + - name: config-volume + configMap: + name: ${configmap_name} + diff --git a/traces/deployment/terraform/trace-indexer/templates/trace-indexer.conf b/traces/deployment/terraform/trace-indexer/templates/trace-indexer.conf new file mode 100644 index 000000000..19fa9f19f --- /dev/null +++ b/traces/deployment/terraform/trace-indexer/templates/trace-indexer.conf @@ -0,0 +1,157 @@ +health.status.path = "/app/isHealthy" + +span.accumulate { + store { + min.traces.per.cache = 1000 # this defines the minimum traces in each cache before eviction check is applied. This is also useful for testing the code + all.max.entries = 150000 # this is the maximum number of spans that can live across all the stores + } + window.ms = 10000 + poll.ms = 2000 + packer = zstd +} + +kafka { + close.stream.timeout.ms = 15000 + + topic.consume = "proto-spans" + topic.produce = "${span_produce_topic}" + + num.stream.threads = 2 + poll.timeout.ms = 100 + + # if consumer poll hangs, then wakeup it after after a timeout + # also set the maximum wakeups allowed, if max threshold is reached, then task will raise the shutdown request + max.wakeups = 10 + wakeup.timeout.ms = 3000 + + commit.offset { + retries = 3 + backoff.ms = 200 + } + + # consumer specific configurations + consumer { + group.id = "haystack-proto-trace-indexer" + bootstrap.servers = "${kafka_endpoint}" + auto.offset.reset = "latest" + + # disable auto commit as the app manages offset itself + enable.auto.commit = "false" + } + + # producer specific configurations + producer { + bootstrap.servers = "${kafka_endpoint}" + } +} + +backend { + + client { + host = "localhost" + port = 8090 + } + # defines the max inflight writes for backend client + max.inflight.requests = 100 +} +elasticsearch { + endpoint = "http://${elasticsearch_endpoint}" + + # defines settings for bulk operation like max inflight bulks, number of documents and the total size in a single bulk + bulk.max { + docs { + count = 200 + size.kb = 1000 + } + inflight = 25 + } + + conn.timeout.ms = 10000 + read.timeout.ms = 30000 + consistency.level = "one" + max.connections.per.route = 5 + + retries { + max = 10 + backoff { + initial.ms = 100 + factor = 2 + } + } + + index { + # apply the template before starting the client, if json is empty, no operation is performed + template.json = """${elasticsearch_template}""" + + name.prefix = "haystack-traces" + type = "spans" + hour.bucket = 6 + } + + # if enabled flag is true, es requests will be signed + signing.request.aws { + enabled = false + region = "us-west-2" + service.name = "es" + # if 'access.key' is not provided, will use DefaultAWSCredentialsProviderChain to resolve creds + access.key = "" + secret.key = "" + } +} + +service.metadata { + enabled = true + flush { + interval.sec = 60 + operation.count = 10000 + } + es { + endpoint = "http://${elasticsearch_endpoint}" + conn.timeout.ms = 10000 + read.timeout.ms = 5000 + consistency.level = "one" + index { + # apply the template before starting the client, if json is empty, no operation is performed + template.json = "{\"template\": \"service-metadata\", \"index_patterns\": [\"service-metadata*\"], \"aliases\": {\"service-metadata\":{}}, \"settings\": {\"number_of_shards\": 4, \"index.mapping.ignore_malformed\": true, \"analysis\": {\"normalizer\": {\"lowercase_normalizer\": {\"type\": \"custom\", \"filter\": [\"lowercase\"]}}}}, \"mappings\": {\"metadata\": {\"_field_names\": {\"enabled\": false}, \"_all\": {\"enabled\": false}, \"properties\": {\"servicename\": {\"type\": \"keyword\", \"norms\": false}, \"operationname\": {\"type\": \"keyword\", \"norms\": false}}}}}" + name = "service-metadata" + type = "metadata" + } + # defines settings for bulk operation like max inflight bulks, number of documents and the total size in a single bulk + bulk.max { + docs { + count = 100 + size.kb = 1000 + } + inflight = 10 + } + retries { + max = 10 + backoff { + initial.ms = 100 + factor = 2 + } + } + } +} + +reload { + tables { + index.fields.config = "indexing-fields" + } + config { + endpoint = "http://${elasticsearch_endpoint}" + database.name = "reload-configs" + } + startup.load = true + interval.ms = 60000 # -1 will imply 'no reload' + + # if enabled flag is true, es requests will be signed + signing.request.aws { + enabled = false + region = "us-west-2" + service.name = "es" + # if 'access.key' is not provided, will use DefaultAWSCredentialsProviderChain to resolve creds + access.key = "" + secret.key = "" + } +} diff --git a/traces/deployment/terraform/trace-indexer/variables.tf b/traces/deployment/terraform/trace-indexer/variables.tf new file mode 100644 index 000000000..52fb41b39 --- /dev/null +++ b/traces/deployment/terraform/trace-indexer/variables.tf @@ -0,0 +1,35 @@ +variable "storage_backend_image" {} +variable "image" {} +variable "replicas" {} +variable "namespace" {} +variable "kafka_endpoint" {} +variable "elasticsearch_hostname" {} +variable "elasticsearch_port" {} +variable "elasticsearch_template" {} +variable "cassandra_hostname" {} +variable "graphite_hostname" {} +variable "graphite_port" {} +variable "graphite_enabled" {} +variable "enabled"{} +variable "kubectl_executable_name" {} +variable "kubectl_context_name" {} +variable "node_selector_label"{} +variable "memory_request"{} +variable "memory_limit"{} +variable "jvm_memory_limit"{} +variable "cpu_request"{} +variable "cpu_limit"{} +variable "backend_memory_request"{} +variable "backend_memory_limit"{} +variable "backend_jvm_memory_limit"{} +variable "backend_cpu_request"{} +variable "backend_cpu_limit"{} +variable "env_vars" {} +variable "backend_env_vars" {} +variable "enable_kafka_sink" { + default = false +} + +variable "termination_grace_period" { + default = 30 +} diff --git a/traces/deployment/terraform/trace-reader/main.tf b/traces/deployment/terraform/trace-reader/main.tf new file mode 100644 index 000000000..83a4e76f5 --- /dev/null +++ b/traces/deployment/terraform/trace-reader/main.tf @@ -0,0 +1,79 @@ +locals { + app_name = "trace-reader" + config_file_path = "${path.module}/templates/trace-reader.conf" + deployment_yaml_file_path = "${path.module}/templates/deployment.yaml" + count = "${var.enabled?1:0}" + checksum = "${sha1("${data.template_file.config_data.rendered}")}" + configmap_name = "reader-${local.checksum}" +} + +resource "kubernetes_config_map" "haystack-config" { + metadata { + name = "${local.configmap_name}" + namespace = "${var.namespace}" + } + data { + "trace-reader.conf" = "${data.template_file.config_data.rendered}" + } + count = "${local.count}" + +} + +data "template_file" "config_data" { + template = "${file("${local.config_file_path}")}" + + vars { + elasticsearch_endpoint = "${var.elasticsearch_endpoint}" + } +} + +data "template_file" "deployment_yaml" { + template = "${file("${local.deployment_yaml_file_path}")}" + vars { + app_name = "${local.app_name}" + namespace = "${var.namespace}" + graphite_port = "${var.graphite_port}" + graphite_host = "${var.graphite_hostname}" + graphite_enabled = "${var.graphite_enabled}" + node_selecter_label = "${var.node_selector_label}" + image = "${var.image}" + storage_backend_image = "${var.storage_backend_image}" + cassandra_hostname = "${var.cassandra_hostname}" + replicas = "${var.replicas}" + memory_limit = "${var.memory_limit}" + memory_request = "${var.memory_request}" + jvm_memory_limit = "${var.jvm_memory_limit}" + cpu_limit = "${var.cpu_limit}" + cpu_request = "${var.cpu_request}" + backend_memory_limit = "${var.backend_memory_limit}" + backend_memory_request = "${var.backend_memory_request}" + backend_jvm_memory_limit = "${var.backend_jvm_memory_limit}" + backend_cpu_limit = "${var.backend_cpu_limit}" + backend_cpu_request = "${var.backend_cpu_request}" + service_port = "${var.service_port}" + container_port = "${var.container_port}" + configmap_name = "${local.configmap_name}" + env_vars= "${indent(9,"${var.env_vars}")}" + backend_env_vars = "${indent(9,"${var.backend_env_vars}")}" + } +} + +resource "null_resource" "kubectl_apply" { + triggers { + template = "${data.template_file.deployment_yaml.rendered}" + } + provisioner "local-exec" { + command = "echo '${data.template_file.deployment_yaml.rendered}' | ${var.kubectl_executable_name} apply -f - --context ${var.kubectl_context_name}" + } + count = "${local.count}" +} + + +resource "null_resource" "kubectl_destroy" { + + provisioner "local-exec" { + command = "echo '${data.template_file.deployment_yaml.rendered}' | ${var.kubectl_executable_name} delete -f - --context ${var.kubectl_context_name}" + when = "destroy" + } + count = "${local.count}" +} diff --git a/traces/deployment/terraform/trace-reader/outputs.tf b/traces/deployment/terraform/trace-reader/outputs.tf new file mode 100644 index 000000000..562aba81f --- /dev/null +++ b/traces/deployment/terraform/trace-reader/outputs.tf @@ -0,0 +1,7 @@ +output "hostname" { + value = "${local.app_name}" +} + +output "service_port" { + value = "${var.service_port}" +} \ No newline at end of file diff --git a/traces/deployment/terraform/trace-reader/templates/deployment.yaml b/traces/deployment/terraform/trace-reader/templates/deployment.yaml new file mode 100644 index 000000000..5bbf98543 --- /dev/null +++ b/traces/deployment/terraform/trace-reader/templates/deployment.yaml @@ -0,0 +1,111 @@ +# ------------------- Deployment ------------------- # + +kind: Deployment +apiVersion: apps/v1beta2 +metadata: + labels: + k8s-app: ${app_name} + name: ${app_name} + namespace: ${namespace} +spec: + replicas: ${replicas} + revisionHistoryLimit: 10 + selector: + matchLabels: + k8s-app: ${app_name} + template: + metadata: + labels: + k8s-app: ${app_name} + spec: + containers: + - name: storage-backend-cassandra-reader + image: ${storage_backend_image} + resources: + limits: + cpu: ${backend_cpu_limit} + memory: ${backend_memory_limit}Mi + requests: + cpu: ${backend_cpu_request} + memory: ${backend_memory_request}Mi + env: + - name: "HAYSTACK_PROP_CASSANDRA_ENDPOINTS" + value: "${cassandra_hostname}" + - name: "HAYSTACK_GRAPHITE_HOST" + value: "${graphite_host}" + - name: "HAYSTACK_GRAPHITE_PORT" + value: "${graphite_port}" + - name: "HAYSTACK_GRAPHITE_ENABLED" + value: "${graphite_enabled}" + - name: "JAVA_XMS" + value: "${backend_jvm_memory_limit}m" + - name: "JAVA_XMX" + value: "${backend_jvm_memory_limit}m" + ${backend_env_vars} + livenessProbe: + exec: + command: + - /bin/grpc_health_probe + - "-addr=:8090" + initialDelaySeconds: 30 + periodSeconds: 15 + timeoutSeconds: 5 + failureThreshold: 3 + - name: ${app_name} + image: ${image} + volumeMounts: + # Create on-disk volume to store exec logs + - mountPath: /config + name: config-volume + resources: + limits: + cpu: ${cpu_limit} + memory: ${memory_limit}Mi + requests: + cpu: ${cpu_request} + memory: ${memory_request}Mi + env: + - name: "HAYSTACK_OVERRIDES_CONFIG_PATH" + value: "/config/trace-reader.conf" + - name: "HAYSTACK_GRAPHITE_HOST" + value: "${graphite_host}" + - name: "HAYSTACK_GRAPHITE_PORT" + value: "${graphite_port}" + - name: "HAYSTACK_GRAPHITE_ENABLED" + value: "${graphite_enabled}" + - name: "JAVA_XMS" + value: "${jvm_memory_limit}m" + - name: "JAVA_XMX" + value: "${jvm_memory_limit}m" + ${env_vars} + livenessProbe: + exec: + command: + - /bin/grpc_health_probe + - "-addr=:${container_port}" + initialDelaySeconds: 30 + periodSeconds: 15 + timeoutSeconds: 5 + failureThreshold: 3 + nodeSelector: + ${node_selecter_label} + volumes: + - name: config-volume + configMap: + name: ${configmap_name} + +# ------------------- Service ------------------- # +--- +apiVersion: v1 +kind: Service +metadata: + labels: + k8s-app: ${app_name} + name: ${app_name} + namespace: ${namespace} +spec: + ports: + - port: ${service_port} + targetPort: ${container_port} + selector: + k8s-app: ${app_name} diff --git a/traces/deployment/terraform/trace-reader/templates/trace-reader.conf b/traces/deployment/terraform/trace-reader/templates/trace-reader.conf new file mode 100644 index 000000000..a196abb72 --- /dev/null +++ b/traces/deployment/terraform/trace-reader/templates/trace-reader.conf @@ -0,0 +1,97 @@ +service { + port = 8080 + ssl { + enabled = false + cert.path = "" + private.key.path = "" + } + max.message.size = 52428800 # 50MB in bytes +} + +backend { + client { + host = "localhost" + port = 8090 + } +} + +elasticsearch { + client { + endpoint = "http://${elasticsearch_endpoint}" + conn.timeout.ms = 10000 + read.timeout.ms = 30000 + } + index { + spans { + name.prefix = "haystack-traces" + type = "spans" + hour.bucket = 6 + hour.ttl = 72 // 3 * 24 hours + use.root.doc.starttime = true + } + service.metadata { + enabled = true + name = "service-metadata" + type = "metadata" + } + } + + # if enabled flag is true, es requests will be signed + signing.request.aws { + enabled = false + region = "us-west-2" + service.name = "es" + # if 'access.key' is not provided, will use DefaultAWSCredentialsProviderChain to resolve creds + access.key = "" + secret.key = "" + } +} + +trace { + validators { + sequence = [ + "com.expedia.www.haystack.trace.reader.readers.validators.TraceIdValidator" + ] + } + + transformers { + pre { + sequence = [ + "com.expedia.www.haystack.trace.reader.readers.transformers.DeDuplicateSpanTransformer" + "com.expedia.www.haystack.trace.reader.readers.transformers.ClientServerEventLogTransformer" + ] + } + post { + sequence = [ + "com.expedia.www.haystack.trace.reader.readers.transformers.PartialSpanTransformer" + "com.expedia.www.haystack.trace.reader.readers.transformers.ServerClientSpanMergeTransformer" + "com.expedia.www.haystack.trace.reader.readers.transformers.InvalidRootTransformer" + "com.expedia.www.haystack.trace.reader.readers.transformers.InvalidParentTransformer" + "com.expedia.www.haystack.trace.reader.readers.transformers.ClockSkewTransformer" + "com.expedia.www.haystack.trace.reader.readers.transformers.SortSpanTransformer" + ] + } + } +} + +reload { + tables { + index.fields.config = "indexing-fields" + } + config { + endpoint = "http://${elasticsearch_endpoint}" + database.name = "reload-configs" + } + startup.load = true + interval.ms = 60000 # -1 will imply 'no reload' + + # if enabled flag is true, es requests will be signed + signing.request.aws { + enabled = false + region = "us-west-2" + service.name = "es" + # if 'access.key' is not provided, will use DefaultAWSCredentialsProviderChain to resolve creds + access.key = "" + secret.key = "" + } +} diff --git a/traces/deployment/terraform/trace-reader/variables.tf b/traces/deployment/terraform/trace-reader/variables.tf new file mode 100644 index 000000000..0ceada02e --- /dev/null +++ b/traces/deployment/terraform/trace-reader/variables.tf @@ -0,0 +1,36 @@ +variable "image" {} +variable "storage_backend_image" {} +variable "replicas" {} +variable "namespace" {} +variable "elasticsearch_endpoint" {} +variable "cassandra_hostname" {} +variable "graphite_hostname" {} +variable "graphite_port" {} +variable "graphite_enabled" {} +variable "enabled"{} +variable "kubectl_executable_name" {} +variable "kubectl_context_name" {} +variable "node_selector_label"{} +variable "memory_request"{} +variable "memory_limit"{} +variable "cpu_request"{} +variable "cpu_limit"{} +variable "jvm_memory_limit"{} +variable "backend_memory_request"{} +variable "backend_memory_limit"{} +variable "backend_jvm_memory_limit"{} +variable "backend_cpu_request"{} +variable "backend_cpu_limit"{} +variable "env_vars" {} +variable "backend_env_vars" {} + +variable "termination_grace_period" { + default = 30 +} + +variable "service_port" { + default = 8080 +} +variable "container_port" { + default = 8080 +} diff --git a/traces/deployment/terraform/variables.tf b/traces/deployment/terraform/variables.tf new file mode 100644 index 000000000..020eecde4 --- /dev/null +++ b/traces/deployment/terraform/variables.tf @@ -0,0 +1,19 @@ +variable "elasticsearch_hostname" {} +variable "elasticsearch_port" {} +variable "kafka_hostname" {} +variable "kafka_port" {} +variable "cassandra_hostname" {} +variable "cassandra_port" {} +variable "graphite_hostname" {} +variable "graphite_port" {} +variable "graphite_enabled" {} +variable "kubectl_context_name" {} +variable "kubectl_executable_name" {} +variable "namespace" {} +variable "node_selector_label"{} + + +# traces config +variable "traces" { + type = "map" +} diff --git a/traces/indexer/Makefile b/traces/indexer/Makefile new file mode 100644 index 000000000..911775640 --- /dev/null +++ b/traces/indexer/Makefile @@ -0,0 +1,20 @@ +.PHONY: docker_build integration_test release + +export DOCKER_ORG := expediadotcom +export DOCKER_IMAGE_NAME := haystack-trace-indexer +PWD := $(shell pwd) + +docker_build: + docker build -t $(DOCKER_IMAGE_NAME) -f build/docker/Dockerfile . + +prepare_integration_test_env: docker_build + docker-compose -f build/integration-tests/docker-compose.yml -p sandbox up -d + +integration_test: prepare_integration_test_env + # run tests in a container so that we can join the docker-compose network and talk to kafka, elasticsearch and trace-backend + cd ../ && ./mvnw -q integration-test -pl indexer -am + # clean up the docker + docker-compose -f build/integration-tests/docker-compose.yml -p sandbox stop + docker rm $(shell docker ps -a -q) +release: + ../deployment/scripts/publish-to-docker-hub.sh diff --git a/traces/indexer/README.md b/traces/indexer/README.md new file mode 100644 index 000000000..07d0b616f --- /dev/null +++ b/traces/indexer/README.md @@ -0,0 +1,33 @@ +# haystack-trace-indexer + +This haystack component accumulates the spans associated with a TraceId in a given time window(configurable). +The time window for every unique traceId starts with the kafka-record's timestamp of the first observed child span. +These accumulated spans are written as single entity to external trace-backends for persistence and elastic search for indexing. We also output these +accumulated spans back to kafka for other consumers to consume. + +The buffering approach provides a performance optimization as it will potentially reduce the number of write calls to external stores. +Secondly, the output can also by used by dependency graph component to build the complete call graph for all the services. + +Note that the system can still emit partial spans for a given traceId, possible cases can be + * The time window is not configured correctly or doesn't match with the speed at which spans appear in kafka + * On redeployment of this component, we might spit out partially buffered spans. + +However, the partial buffered spans are ok to be written to the trace-backend and elastic search. In trace-backend, we use TraceId as the +primary key and store buffered-spans as a time series. + +In ElasticSearch, we use TraceId appended by a 4 character random ID with every document that we write. This ensures +that if the same TraceId reappears, we generate a new document. + +## Required Reading + +In order to understand the haystack, we recommend to read the details of [haystack](https://github.com/ExpediaDotCom/haystack) project. +Its written in kafka-streams(http://docs.confluent.io/current/streams/index.html) and hence some prior knowledge of kafka-streams would be useful. + + +## Technical Details + +Fill this as we go along.. + +## Building + +Check the details on [Build Section](../README.md) diff --git a/traces/indexer/build/docker/Dockerfile b/traces/indexer/build/docker/Dockerfile new file mode 100644 index 000000000..07f2bcba9 --- /dev/null +++ b/traces/indexer/build/docker/Dockerfile @@ -0,0 +1,24 @@ +FROM openjdk:8-jre +MAINTAINER Haystack + +ENV APP_NAME haystack-trace-indexer +ENV APP_HOME /app/bin +ENV JMXTRANS_AGENT jmxtrans-agent-1.2.6 +ENV DOCKERIZE_VERSION v0.6.1 + +ADD https://github.com/jwilder/dockerize/releases/download/${DOCKERIZE_VERSION}/dockerize-alpine-linux-amd64-${DOCKERIZE_VERSION}.tar.gz dockerize.tar.gz +RUN tar xzf dockerize.tar.gz +RUN chmod +x dockerize + +RUN mkdir -p ${APP_HOME} + +COPY target/${APP_NAME}.jar ${APP_HOME}/ +COPY build/docker/start-app.sh ${APP_HOME}/ +COPY build/docker/jmxtrans-agent.xml ${APP_HOME}/ + +RUN chmod +x ${APP_HOME}/start-app.sh +ADD https://github.com/jmxtrans/jmxtrans-agent/releases/download/${JMXTRANS_AGENT}/${JMXTRANS_AGENT}.jar ${APP_HOME}/ + +WORKDIR ${APP_HOME} + +ENTRYPOINT ["./start-app.sh"] diff --git a/traces/indexer/build/docker/jmxtrans-agent.xml b/traces/indexer/build/docker/jmxtrans-agent.xml new file mode 100644 index 000000000..0c33e7822 --- /dev/null +++ b/traces/indexer/build/docker/jmxtrans-agent.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ${HAYSTACK_GRAPHITE_HOST:monitoring-influxdb-graphite.kube-system.svc} + ${HAYSTACK_GRAPHITE_PORT:2003} + ${HAYSTACK_GRAPHITE_ENABLED:true} + + haystack.traces.indexer.#hostname#. + + 60 + diff --git a/traces/indexer/build/docker/start-app.sh b/traces/indexer/build/docker/start-app.sh new file mode 100755 index 000000000..ba2c65569 --- /dev/null +++ b/traces/indexer/build/docker/start-app.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +[ -z "$JAVA_XMS" ] && JAVA_XMS=1024m +[ -z "$JAVA_XMX" ] && JAVA_XMX=1024m +[ -z "$JAVA_GC_OPTS" ] && JAVA_GC_OPTS="-XX:+UseG1GC" + +set -e +JAVA_OPTS="${JAVA_OPTS} \ +-javaagent:${APP_HOME}/${JMXTRANS_AGENT}.jar=${APP_HOME}/jmxtrans-agent.xml \ +${JAVA_GC_OPTS} \ +-Xmx${JAVA_XMX} \ +-Xms${JAVA_XMS} \ +-XX:+ExitOnOutOfMemoryError \ +-Dapplication.name=${APP_NAME} \ +-Dapplication.home=${APP_HOME}" + +if [[ -n "$SERVICE_DEBUG_ON" ]] && [[ "$SERVICE_DEBUG_ON" == true ]]; then + JAVA_OPTS="$JAVA_OPTS -Xdebug -Xrunjdwp:transport=dt_socket,address=5005,server=y" +fi + +exec java ${JAVA_OPTS} -jar "${APP_HOME}/${APP_NAME}.jar" diff --git a/traces/indexer/build/integration-tests/docker-compose.yml b/traces/indexer/build/integration-tests/docker-compose.yml new file mode 100644 index 000000000..d07c15b26 --- /dev/null +++ b/traces/indexer/build/integration-tests/docker-compose.yml @@ -0,0 +1,8 @@ +version: '3' +services: + elasticsearch: + image: elastic/elasticsearch:6.0.1 + environment: + ES_JAVA_OPTS: "-Xms256m -Xmx256m" + ports: + - "9200:9200" \ No newline at end of file diff --git a/traces/indexer/pom.xml b/traces/indexer/pom.xml new file mode 100644 index 000000000..31879dd8f --- /dev/null +++ b/traces/indexer/pom.xml @@ -0,0 +1,236 @@ + + + + + haystack-traces + com.expedia.www + 1.0.9-SNAPSHOT + + + 4.0.0 + haystack-trace-indexer + jar + + + 0.11.0.0 + com.expedia.www.haystack.trace.indexer.App + ${project.artifactId}-${project.version} + false + + + + + com.expedia.www + haystack-trace-commons + ${project.version} + + + + com.google.protobuf + protobuf-java + + + + io.grpc + grpc-protobuf + + + + io.grpc + grpc-stub + + + + io.grpc + grpc-services + + + + io.grpc + grpc-netty + + + io.searchbox + jest + + + + org.elasticsearch + elasticsearch + + + + org.apache.commons + commons-lang3 + + + + org.apache.httpcomponents + httpclient + + + + com.amazonaws + aws-java-sdk-ec2 + + + + org.json4s + json4s-jackson_${scala.major.minor.version} + + + + + org.apache.kafka + kafka_${scala.major.minor.version} + ${kafka-version} + + + org.slf4j + slf4j-log4j12 + + + + + org.apache.kafka + kafka-clients + ${kafka-version} + + + + com.expedia.www + haystack-logback-metrics-appender + + + + + + org.apache.kafka + kafka-streams + ${kafka-version} + test + test + + + + com.expedia.www + haystack-trace-backend-memory + ${project.version} + test + + + org.apache.kafka + kafka-streams + ${kafka-version} + + + + org.apache.kafka + kafka_${scala.major.minor.version} + ${kafka-version} + + + org.slf4j + slf4j-log4j12 + + + test + test + + + + org.apache.kafka + kafka-clients + ${kafka-version} + test + test + + + + + ${finalName} + + + org.scalatest + scalatest-maven-plugin + + + test + + test + + + + earliest + haystack-test + + com.expedia.www.haystack.trace.indexer.unit + ${skip.unit.tests} + + + + integration-test + integration-test + + test + + + com.expedia.www.haystack.trace.indexer.integration + + + + + + + org.apache.maven.plugins + maven-shade-plugin + + true + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + + + + package + + shade + + + + + reference.conf + + + ${mainClass} + + + + + + + + + net.alchim31.maven + scala-maven-plugin + + + + org.scalastyle + scalastyle-maven-plugin + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + diff --git a/traces/indexer/src/main/resources/config/base.conf b/traces/indexer/src/main/resources/config/base.conf new file mode 100644 index 000000000..0df856db8 --- /dev/null +++ b/traces/indexer/src/main/resources/config/base.conf @@ -0,0 +1,160 @@ +health.status.path = "/app/isHealthy" + +haystack.graphite.host = "monitoring-influxdb-graphite.kube-system.svc" + +span.accumulate { + store { + min.traces.per.cache = 1000 # this defines the minimum traces in each cache before eviction check is applied. This is also useful for testing the code + all.max.entries = 20000 # this is the maximum number of spans that can live across all the stores + } + window.ms = 10000 + poll.ms = 2000 + packer = none +} + +kafka { + close.stream.timeout.ms = 15000 + + topic.consume = "proto-spans" + topic.produce = "span-buffer" + + num.stream.threads = 2 + poll.timeout.ms = 100 + + # if consumer poll hangs, then wakeup it after after a timeout + # also set the maximum wakeups allowed, if max threshold is reached, then task will raise the shutdown request + max.wakeups = 100 + wakeup.timeout.ms = 3000 + + commit.offset { + retries = 3 + backoff.ms = 200 + } + + # consumer specific configurations + consumer { + group.id = "haystack-trace-indexer" + bootstrap.servers = "kafkasvc:9092" + auto.offset.reset = "latest" + + # disable auto commit as the app manages offset itself + enable.auto.commit = "false" + } + +# producer specific configurations + producer { + bootstrap.servers = "kafkasvc:9092" + } +} + + +backend { + + client { + host = "localhost" + port = 8090 + } + # defines the max inflight writes for backend client + max.inflight.requests = 100 +} + +service.metadata { + enabled = true + flush { + interval.sec = 60 + operation.count = 10000 + } + es { + endpoint = "http://elasticsearch:9200" + conn.timeout.ms = 10000 + read.timeout.ms = 5000 + consistency.level = "one" + index { + # apply the template before starting the client, if json is empty, no operation is performed + template.json = "{\"template\": \"service-metadata\", \"index_patterns\": [\"service-metadata*\"], \"aliases\": {\"service-metadata\":{}}, \"settings\": {\"number_of_shards\": 4, \"index.mapping.ignore_malformed\": true, \"analysis\": {\"normalizer\": {\"lowercase_normalizer\": {\"type\": \"custom\", \"filter\": [\"lowercase\"]}}}}, \"mappings\": {\"metadata\": {\"_field_names\": {\"enabled\": false}, \"_all\": {\"enabled\": false}, \"properties\": {\"servicename\": {\"type\": \"keyword\", \"norms\": false}, \"operationname\": {\"type\": \"keyword\", \"norms\": false}}}}}" + name = "service-metadata" + type = "metadata" + } + # defines settings for bulk operation like max inflight bulks, number of documents and the total size in a single bulk + bulk.max { + docs { + count = 100 + size.kb = 1000 + } + inflight = 10 + } + retries { + max = 10 + backoff { + initial.ms = 100 + factor = 2 + } + } + } +} + +elasticsearch { + endpoint = "http://elasticsearch:9200" + + # defines settings for bulk operation like max inflight bulks, number of documents and the total size in a single bulk + bulk.max { + docs { + count = 100 + size.kb = 1000 + } + inflight = 10 + } + + conn.timeout.ms = 10000 + read.timeout.ms = 5000 + consistency.level = "one" + max.connections.per.route = 5 + + retries { + max = 10 + backoff { + initial.ms = 100 + factor = 2 + } + } + + index { + # apply the template before starting the client, if json is empty, no operation is performed + template.json = "{\"template\":\"haystack-traces*\",\"settings\":{\"number_of_shards\":16,\"index.mapping.ignore_malformed\":true,\"analysis\":{\"normalizer\":{\"lowercase_normalizer\":{\"type\":\"custom\",\"filter\":[\"lowercase\"]}}}},\"aliases\":{\"haystack-traces\":{}},\"mappings\":{\"spans\":{\"_field_names\":{\"enabled\":false},\"_all\":{\"enabled\":false},\"_source\":{\"includes\":[\"traceid\"]},\"properties\":{\"traceid\":{\"enabled\":false},\"starttime\":{\"type\":\"long\",\"doc_values\":true},\"spans\":{\"type\":\"nested\",\"properties\":{\"servicename\":{\"type\":\"keyword\",\"normalizer\":\"lowercase_normalizer\",\"doc_values\":true,\"norms\":false},\"operationname\":{\"type\":\"keyword\",\"normalizer\":\"lowercase_normalizer\",\"doc_values\":true,\"norms\":false},\"starttime\":{\"type\":\"long\",\"doc_values\":true},\"duration\":{\"type\":\"long\",\"doc_values\":true}}}},\"dynamic_templates\":[{\"strings_as_keywords_1\":{\"match_mapping_type\":\"string\",\"mapping\":{\"type\":\"keyword\",\"normalizer\":\"lowercase_normalizer\",\"doc_values\":false,\"norms\":false}}},{\"longs_disable_doc_norms\":{\"match_mapping_type\":\"long\",\"mapping\":{\"type\":\"long\",\"doc_values\":false,\"norms\":false}}}]}}}" + name.prefix = "haystack-traces" + hour.bucket = 6 + type = "spans" + } + + # if enabled flag is true, es requests will be signed + signing.request.aws { + enabled = false + region = "us-west-2" + service.name = "es" + # if 'access.key' is not provided, will use DefaultAWSCredentialsProviderChain to resolve creds + access.key = "" + secret.key = "" + } +} + +reload { + tables { + index.fields.config = "whitelist-index-fields" + } + config { + endpoint = "http://elasticsearch:9200" + database.name = "reload-configs" + } + startup.load = true + interval.ms = 60000 # -1 will imply 'no reload' + + # if enabled flag is true, es requests will be signed + signing.request.aws { + enabled = false + region = "us-west-2" + service.name = "es" + # if 'access.key' is not provided, will use DefaultAWSCredentialsProviderChain to resolve creds + access.key = "" + secret.key = "" + } +} diff --git a/traces/indexer/src/main/resources/logback.xml b/traces/indexer/src/main/resources/logback.xml new file mode 100644 index 000000000..ab4e25a63 --- /dev/null +++ b/traces/indexer/src/main/resources/logback.xml @@ -0,0 +1,27 @@ + + + + + + + true + + + + + + %d{yyyy-MM-dd HH:mm:ss:SSS} %thread, %level, %logger{70}, "%msg" %replace(%ex){'[\n]+', '\\n'}%nopex%n + + + + + + ${HAYSTACK_LOG_QUEUE_SIZE:-500} + ${HAYSTACK_LOG_DISCARD_THRESHOLD:-0} + + + + + + + diff --git a/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/App.scala b/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/App.scala new file mode 100644 index 000000000..2ca21d724 --- /dev/null +++ b/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/App.scala @@ -0,0 +1,78 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trace.indexer + +import com.codahale.metrics.JmxReporter +import com.expedia.www.haystack.commons.health.{HealthController, UpdateHealthStatusFile} +import com.expedia.www.haystack.commons.logger.LoggerUtils +import com.expedia.www.haystack.commons.metrics.MetricsSupport +import com.expedia.www.haystack.trace.indexer.config.ProjectConfiguration +import org.slf4j.LoggerFactory + +object App extends MetricsSupport { + private val LOGGER = LoggerFactory.getLogger(App.getClass) + + private var stream: StreamRunner = _ + private var appConfig: ProjectConfiguration = _ + + def main(args: Array[String]): Unit = { + startJmxReporter() + + try { + appConfig = new ProjectConfiguration + + HealthController.addListener(new UpdateHealthStatusFile(appConfig.healthStatusFilePath)) + + stream = new StreamRunner( + appConfig.kafkaConfig, + appConfig.spanAccumulateConfig, + appConfig.elasticSearchConfig, + appConfig.backendConfig, + appConfig.serviceMetadataWriteConfig, + appConfig.indexConfig) + + Runtime.getRuntime.addShutdownHook(new Thread { + override def run(): Unit = { + LOGGER.info("Shutdown hook is invoked, tearing down the application.") + shutdown() + } + }) + + stream.start() + + // mark the status of app as 'healthy' + HealthController.setHealthy() + } catch { + case ex: Exception => + LOGGER.error("Observed fatal exception while running the app", ex) + shutdown() + System.exit(1) + } + } + + private def shutdown(): Unit = { + if(stream != null) stream.close() + if(appConfig != null) appConfig.close() + LoggerUtils.shutdownLogger() + } + + private def startJmxReporter() = { + val jmxReporter = JmxReporter.forRegistry(metricRegistry).build() + jmxReporter.start() + } +} diff --git a/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/StreamRunner.scala b/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/StreamRunner.scala new file mode 100644 index 000000000..abd3860a0 --- /dev/null +++ b/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/StreamRunner.scala @@ -0,0 +1,132 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trace.indexer + +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.{Executors, TimeUnit} + +import com.expedia.www.haystack.commons.health.HealthController +import com.expedia.www.haystack.trace.commons.config.entities.WhitelistIndexFieldConfiguration +import com.expedia.www.haystack.trace.commons.packer.PackerFactory +import com.expedia.www.haystack.trace.indexer.config.entities._ +import com.expedia.www.haystack.trace.indexer.processors.StreamTaskState.StreamTaskState +import com.expedia.www.haystack.trace.indexer.processors._ +import com.expedia.www.haystack.trace.indexer.processors.supplier.SpanIndexProcessorSupplier +import com.expedia.www.haystack.trace.indexer.store.SpanBufferMemoryStoreSupplier +import com.expedia.www.haystack.trace.indexer.writers.TraceWriter +import com.expedia.www.haystack.trace.indexer.writers.es.{ElasticSearchWriter, ServiceMetadataWriter} +import com.expedia.www.haystack.trace.indexer.writers.grpc.GrpcTraceWriter +import com.expedia.www.haystack.trace.indexer.writers.kafka.KafkaWriter +import org.apache.commons.lang3.StringUtils +import org.slf4j.LoggerFactory + +import scala.collection.mutable +import scala.concurrent.ExecutionContextExecutor + +class StreamRunner(kafkaConfig: KafkaConfiguration, + accumulatorConfig: SpanAccumulatorConfiguration, + esConfig: ElasticSearchConfiguration, + traceWriteConfig: TraceBackendConfiguration, + serviceMetadataWriteConfig: ServiceMetadataWriteConfiguration, + indexConfig: WhitelistIndexFieldConfiguration) extends AutoCloseable with StateListener { + + implicit private val executor: ExecutionContextExecutor = scala.concurrent.ExecutionContext.global + + private val LOGGER = LoggerFactory.getLogger(classOf[StreamRunner]) + + private val isStarted = new AtomicBoolean(false) + private val streamThreadExecutor = Executors.newFixedThreadPool(kafkaConfig.numStreamThreads) + private val taskRunnables = mutable.ListBuffer[StreamTaskRunnable]() + + private val writers: Seq[TraceWriter] = { + val writers = mutable.ListBuffer[TraceWriter]() + writers += new GrpcTraceWriter(traceWriteConfig)(executor) + writers += new ElasticSearchWriter(esConfig, indexConfig) + + if (serviceMetadataWriteConfig.enabled) { + writers += new ServiceMetadataWriter(serviceMetadataWriteConfig, esConfig.awsRequestSigningConfiguration) + } + + if (StringUtils.isNotEmpty(kafkaConfig.produceTopic)) { + writers += new KafkaWriter(kafkaConfig.producerProps, kafkaConfig.produceTopic) + } + writers + } + + def start(): Unit = { + LOGGER.info("Starting the span indexing stream..") + + val storeSupplier = new SpanBufferMemoryStoreSupplier( + accumulatorConfig.minTracesPerCache, + accumulatorConfig.maxEntriesAllStores) + + val streamProcessSupplier = new SpanIndexProcessorSupplier( + accumulatorConfig, + storeSupplier, + writers, + PackerFactory.spanBufferPacker(accumulatorConfig.packerType)) + + for (streamId <- 0 until kafkaConfig.numStreamThreads) { + val task = new StreamTaskRunnable(streamId, kafkaConfig, streamProcessSupplier) + task.setStateListener(this) + taskRunnables += task + streamThreadExecutor.execute(task) + } + + isStarted.set(true) + } + + override def close(): Unit = { + if (isStarted.getAndSet(false)) { + val shutdownThread = new Thread() { + closeStreamTasks() + closeWriters() + waitAndTerminate() + } + shutdownThread.setDaemon(true) + shutdownThread.run() + } + } + + override def onTaskStateChange(state: StreamTaskState): Unit = { + if (state == StreamTaskState.FAILED) { + LOGGER.error("Thread state has changed to 'FAILED', so tearing down the app") + HealthController.setUnhealthy() + } + } + + private def closeStreamTasks(): Unit = { + LOGGER.info("Closing all the stream tasks..") + taskRunnables foreach { + _.close() + } + } + + private def closeWriters(): Unit = { + LOGGER.info("Closing all the writers now..") + writers foreach { + _.close + } + } + + private def waitAndTerminate(): Unit = { + LOGGER.info("Shutting down the stream executor service") + streamThreadExecutor.shutdown() + streamThreadExecutor.awaitTermination(kafkaConfig.consumerCloseTimeoutInMillis, TimeUnit.MILLISECONDS) + } +} diff --git a/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/config/ProjectConfiguration.scala b/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/config/ProjectConfiguration.scala new file mode 100644 index 000000000..9d276546e --- /dev/null +++ b/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/config/ProjectConfiguration.scala @@ -0,0 +1,282 @@ +/* + * Copyright 2019, Expedia Group. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trace.indexer.config + +import java.util.Properties + +import com.expedia.www.haystack.commons.config.ConfigurationLoader +import com.expedia.www.haystack.commons.retries.RetryOperation +import com.expedia.www.haystack.trace.commons.config.entities._ +import com.expedia.www.haystack.trace.commons.config.reload.{ConfigurationReloadElasticSearchProvider, Reloadable} +import com.expedia.www.haystack.trace.commons.packer.PackerType +import com.expedia.www.haystack.trace.indexer.config.entities._ +import com.expedia.www.haystack.trace.indexer.serde.SpanDeserializer +import com.typesafe.config.Config +import org.apache.commons.lang3.StringUtils +import org.apache.kafka.clients.consumer.ConsumerConfig +import org.apache.kafka.clients.producer.ProducerConfig +import org.apache.kafka.common.serialization.{ByteArraySerializer, StringDeserializer, StringSerializer} + +import scala.collection.JavaConverters._ +import scala.util.Try + +class ProjectConfiguration extends AutoCloseable { + private val config = ConfigurationLoader.loadConfigFileWithEnvOverrides() + + val healthStatusFilePath: String = config.getString("health.status.path") + + /** + * span accumulation related configuration like max buffered records, buffer window, poll interval + * + * @return a span config object + */ + val spanAccumulateConfig: SpanAccumulatorConfiguration = { + val cfg = config.getConfig("span.accumulate") + SpanAccumulatorConfiguration( + cfg.getInt("store.min.traces.per.cache"), + cfg.getInt("store.all.max.entries"), + cfg.getLong("poll.ms"), + cfg.getLong("window.ms"), + PackerType.withName(cfg.getString("packer").toUpperCase)) + } + + /** + * + * @return streams configuration object + */ + val kafkaConfig: KafkaConfiguration = { + // verify if the applicationId and bootstrap server config are non empty + def verifyAndUpdateConsumerProps(props: Properties): Unit = { + require(props.getProperty(ConsumerConfig.GROUP_ID_CONFIG).nonEmpty) + require(props.getProperty(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG).nonEmpty) + + // make sure auto commit is false + require(props.getProperty(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG) == "false") + + // set the deserializers explicitly + props.setProperty(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, classOf[StringDeserializer].getCanonicalName) + props.setProperty(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, new SpanDeserializer().getClass.getCanonicalName) + } + + def verifyAndUpdateProducerProps(props: Properties): Unit = { + require(props.getProperty(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG).nonEmpty) + props.setProperty(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, classOf[StringSerializer].getCanonicalName) + props.setProperty(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, classOf[ByteArraySerializer].getCanonicalName) + } + + def addProps(config: Config, props: Properties): Unit = { + if (config != null) { + config.entrySet().asScala foreach { + kv => { + props.setProperty(kv.getKey, kv.getValue.unwrapped().toString) + } + } + } + } + + val kafka = config.getConfig("kafka") + val producerConfig = if (kafka.hasPath("producer")) kafka.getConfig("producer") else null + val consumerConfig = kafka.getConfig("consumer") + + val consumerProps = new Properties + val producerProps = new Properties + + // producer specific properties + addProps(producerConfig, producerProps) + + // consumer specific properties + addProps(consumerConfig, consumerProps) + + // validate consumer props + verifyAndUpdateConsumerProps(consumerProps) + verifyAndUpdateProducerProps(producerProps) + + KafkaConfiguration( + numStreamThreads = kafka.getInt("num.stream.threads"), + pollTimeoutMs = kafka.getLong("poll.timeout.ms"), + consumerProps = consumerProps, + producerProps = producerProps, + produceTopic = if (kafka.hasPath("topic.produce")) kafka.getString("topic.produce") else "", + consumeTopic = kafka.getString("topic.consume"), + consumerCloseTimeoutInMillis = kafka.getInt("close.stream.timeout.ms"), + commitOffsetRetries = kafka.getInt("commit.offset.retries"), + commitBackoffInMillis = kafka.getLong("commit.offset.backoff.ms"), + maxWakeups = kafka.getInt("max.wakeups"), + wakeupTimeoutInMillis = kafka.getInt("wakeup.timeout.ms")) + } + + + /** + * + * trace backend configuration object + */ + val backendConfig: TraceBackendConfiguration = { + val traceBackendConfig = config.getConfig("backend") + + val grpcClients = traceBackendConfig.entrySet().asScala + .map(k => StringUtils.split(k.getKey, '.')(0)).toSeq + .map(cl => traceBackendConfig.getConfig(cl)) + .filter(cl => cl.hasPath("host") && cl.hasPath("port")) + .map(cl => GrpcClientConfig(cl.getString("host"), cl.getInt("port"))) + + // we dont support multiple backends for write operations + require(grpcClients.size == 1) + + TraceBackendConfiguration( + TraceStoreBackends(grpcClients), + maxInFlightRequests = traceBackendConfig.getInt("max.inflight.requests")) + + } + + /** + * service metadata write configuration + */ + val serviceMetadataWriteConfig: ServiceMetadataWriteConfiguration = { + val serviceMetadata = config.getConfig("service.metadata") + val es = serviceMetadata.getConfig("es") + val templateJsonConfigField = "index.template.json" + val indexTemplateJson = if (es.hasPath(templateJsonConfigField) + && StringUtils.isNotEmpty(es.getString(templateJsonConfigField))) { + Some(es.getString(templateJsonConfigField)) + } else { + None + } + val username = if (es.hasPath("username")) Option(es.getString("username")) else None + val password = if (es.hasPath("password")) Option(es.getString("password")) else None + ServiceMetadataWriteConfiguration( + enabled = serviceMetadata.getBoolean("enabled"), + flushIntervalInSec = serviceMetadata.getInt("flush.interval.sec"), + flushOnMaxOperationCount = serviceMetadata.getInt("flush.operation.count"), + esEndpoint = es.getString("endpoint"), + username = username, + password = password, + consistencyLevel = es.getString("consistency.level"), + indexName = es.getString("index.name"), + indexType = es.getString("index.type"), + indexTemplateJson = indexTemplateJson, + connectionTimeoutMillis = es.getInt("conn.timeout.ms"), + readTimeoutMillis = es.getInt("read.timeout.ms"), + maxInFlightBulkRequests = es.getInt("bulk.max.inflight"), + maxDocsInBulk = es.getInt("bulk.max.docs.count"), + maxBulkDocSizeInBytes = es.getInt("bulk.max.docs.size.kb") * 1000, + retryConfig = RetryOperation.Config( + es.getInt("retries.max"), + es.getLong("retries.backoff.initial.ms"), + es.getDouble("retries.backoff.factor")) + ) + } + + /** + * + * elastic search configuration object + */ + val elasticSearchConfig: ElasticSearchConfiguration = { + val es = config.getConfig("elasticsearch") + val indexConfig = es.getConfig("index") + + val templateJsonConfigField = "template.json" + val indexTemplateJson = if (indexConfig.hasPath(templateJsonConfigField) + && StringUtils.isNotEmpty(indexConfig.getString(templateJsonConfigField))) { + Some(indexConfig.getString(templateJsonConfigField)) + } else { + None + } + val ausername = if (es.hasPath("username")) Option(es.getString("username")) else None + val apassword = if (es.hasPath("password")) Option(es.getString("password")) else None + + ElasticSearchConfiguration( + endpoint = es.getString("endpoint"), + username = ausername, + password = apassword, + indexTemplateJson, + consistencyLevel = es.getString("consistency.level"), + indexNamePrefix = indexConfig.getString("name.prefix"), + indexHourBucket = indexConfig.getInt("hour.bucket"), + indexType = indexConfig.getString("type"), + connectionTimeoutMillis = es.getInt("conn.timeout.ms"), + readTimeoutMillis = es.getInt("read.timeout.ms"), + maxConnectionsPerRoute = es.getInt("max.connections.per.route"), + maxInFlightBulkRequests = es.getInt("bulk.max.inflight"), + maxDocsInBulk = es.getInt("bulk.max.docs.count"), + maxBulkDocSizeInBytes = es.getInt("bulk.max.docs.size.kb") * 1000, + retryConfig = RetryOperation.Config( + es.getInt("retries.max"), + es.getLong("retries.backoff.initial.ms"), + es.getDouble("retries.backoff.factor")), + awsRequestSigningConfig(config.getConfig("elasticsearch.signing.request.aws"))) + } + + private def awsRequestSigningConfig(awsESConfig: Config): AWSRequestSigningConfiguration = { + val accessKey: Option[String] = if (awsESConfig.hasPath("access.key") && awsESConfig.getString("access.key").nonEmpty) { + Some(awsESConfig.getString("access.key")) + } else + None + + val secretKey: Option[String] = if (awsESConfig.hasPath("secret.key") && awsESConfig.getString("secret.key").nonEmpty) { + Some(awsESConfig.getString("secret.key")) + } else + None + + AWSRequestSigningConfiguration( + awsESConfig.getBoolean("enabled"), + awsESConfig.getString("region"), + awsESConfig.getString("service.name"), + accessKey, + secretKey) + } + + /** + * configuration that contains list of tags that should be indexed for a span + */ + val indexConfig: WhitelistIndexFieldConfiguration = { + val indexConfig = WhitelistIndexFieldConfiguration() + indexConfig.reloadConfigTableName = Option(config.getConfig("reload.tables").getString("index.fields.config")) + indexConfig + } + + // configuration reloader + private val reloader = registerReloadableConfigurations(List(indexConfig)) + + /** + * registers a reloadable config object to reloader instance. + * The reloader registers them as observers and invokes them periodically when it re-reads the + * configuration from an external store + * + * @param observers list of reloadable configuration objects + * @return the reloader instance that uses ElasticSearch as an external database for storing the configs + */ + private def registerReloadableConfigurations(observers: Seq[Reloadable]): ConfigurationReloadElasticSearchProvider = { + val reload = config.getConfig("reload") + val reloadConfig = ReloadConfiguration( + reload.getString("config.endpoint"), + reload.getString("config.database.name"), + reload.getInt("interval.ms"), + if (reload.hasPath("config.username")) Option(reload.getString("config.username")) else None, + if (reload.hasPath("config.password")) Option(reload.getString("config.password")) else None, + observers, + loadOnStartup = reload.getBoolean("startup.load")) + + val loader = new ConfigurationReloadElasticSearchProvider(reloadConfig, awsRequestSigningConfig(config.getConfig("reload.signing.request.aws"))) + if (reloadConfig.loadOnStartup) loader.load() + loader + } + + override def close(): Unit = { + Try(reloader.close()) + } +} diff --git a/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/config/entities/ElasticSearchConfiguration.scala b/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/config/entities/ElasticSearchConfiguration.scala new file mode 100644 index 000000000..e92cd0531 --- /dev/null +++ b/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/config/entities/ElasticSearchConfiguration.scala @@ -0,0 +1,56 @@ +/* + * Copyright 2019, Expedia Group. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trace.indexer.config.entities + +import com.expedia.www.haystack.commons.retries.RetryOperation +import com.expedia.www.haystack.trace.commons.config.entities.AWSRequestSigningConfiguration + +/** + * defines the config parameters for elastic search writes + * + * @param endpoint: http endpoint to connect + * @param indexTemplateJson: template as json that will be applied when the app runs, this is optional + * @param consistencyLevel: consistency level of writes, for e.g. one, quoram + * @param indexNamePrefix: prefix for naming the elastic search index + * @param indexHourBucket: creates index for that hour duration. for e.g. for value 6, we create an index every 6 hours in a day so total 4 buckets + * @param indexType: elastic search index type + * @param connectionTimeoutMillis: connection timeout in millis + * @param readTimeoutMillis: read timeout in millis + * @param maxConnectionsPerRoute: max connections per http route + * @param maxInFlightBulkRequests: max bulk writes that can be run in parallel + * @param maxDocsInBulk maximum number of index documents in a single bulk + * @param maxBulkDocSizeInBytes maximum size (in bytes) of a single bulk request + * @param retryConfig retry max retries limit, initial backoff and exponential factor values + * @param awsRequestSigningConfiguration aws ES request signing config + */ +case class ElasticSearchConfiguration(endpoint: String, + username: Option[String], + password: Option[String], + indexTemplateJson: Option[String], + consistencyLevel: String, + indexNamePrefix: String, + indexHourBucket: Int, + indexType: String, + connectionTimeoutMillis: Int, + readTimeoutMillis: Int, + maxConnectionsPerRoute: Int, + maxInFlightBulkRequests: Int, + maxDocsInBulk: Int, + maxBulkDocSizeInBytes: Int, + retryConfig: RetryOperation.Config, + awsRequestSigningConfiguration: AWSRequestSigningConfiguration) diff --git a/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/config/entities/KafkaConfiguration.scala b/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/config/entities/KafkaConfiguration.scala new file mode 100644 index 000000000..8de15b8cd --- /dev/null +++ b/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/config/entities/KafkaConfiguration.scala @@ -0,0 +1,44 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trace.indexer.config.entities + +import java.util.Properties + +/** @param numStreamThreads num of stream threads + * @param pollTimeoutMs kafka consumer poll timeout + * @param consumerProps consumer config object + * @param producerProps producer config object + * @param produceTopic producer topic + * @param consumeTopic consumer topic + * @param consumerCloseTimeoutInMillis kafka consumer close timeout + * @param commitOffsetRetries retries of commit offset failed + * @param commitBackoffInMillis if commit operation fails, retry with backoff + * @param maxWakeups maximum wakeups allowed + * @param wakeupTimeoutInMillis wait timeout for consumer.poll() to return zero or more records + */ +case class KafkaConfiguration(numStreamThreads: Int, + pollTimeoutMs: Long, + consumerProps: Properties, + producerProps: Properties, + produceTopic: String, + consumeTopic: String, + consumerCloseTimeoutInMillis: Int, + commitOffsetRetries: Int, + commitBackoffInMillis: Long, + maxWakeups: Int, + wakeupTimeoutInMillis: Int) diff --git a/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/config/entities/ServiceMetadataWriteConfiguration.scala b/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/config/entities/ServiceMetadataWriteConfiguration.scala new file mode 100644 index 000000000..e99309241 --- /dev/null +++ b/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/config/entities/ServiceMetadataWriteConfiguration.scala @@ -0,0 +1,58 @@ +/* + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expedia.www.haystack.trace.indexer.config.entities + +import com.expedia.www.haystack.commons.retries.RetryOperation + +/** + * Configurations for writing service metadata to elastic search + * @param enabled: enable writing service metadata, if its set to false, list of service_names and operation names would be fetched from elastic search traces index, which is an expensive aggregation + * @param esEndpoint: http endpoint to connect + * @param indexTemplateJson: template as json that will be applied when the app runs, this is optional * @param username + * @param password: password for the es + * @param consistencyLevel: consistency level of writes, for e.g. one, quoram + * @param indexName: name of the elastic search index where the data is written + * @param indexType: elastic search index type + * @param connectionTimeoutMillis : connection timeout in millis + * @param readTimeoutMillis: read timeout in millis + * @param maxInFlightBulkRequests: max bulk writes that can be run in parallel + * @param maxDocsInBulk: maximum number of index documents in a single bulk + * @param maxBulkDocSizeInBytes maximum size (in bytes) of a single bulk request + * @param flushIntervalInSec: interval for collecting service name operation names in memory before flushing to es + * @param flushOnMaxOperationCount: maximum number of unique operations to force flushing to es + * @param retryConfig: retry max retries limit, initial backoff and exponential factor values + */ + +case class ServiceMetadataWriteConfiguration(enabled: Boolean, + esEndpoint: String, + username: Option[String], + password: Option[String], + consistencyLevel: String, + indexTemplateJson: Option[String], + indexName: String, + indexType: String, + connectionTimeoutMillis: Int, + readTimeoutMillis: Int, + maxInFlightBulkRequests: Int, + maxDocsInBulk: Int, + maxBulkDocSizeInBytes: Int, + flushIntervalInSec: Int, + flushOnMaxOperationCount: Int, + retryConfig: RetryOperation.Config + ) { + require(maxInFlightBulkRequests > 0) +} diff --git a/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/config/entities/SpanAccumulatorConfiguration.scala b/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/config/entities/SpanAccumulatorConfiguration.scala new file mode 100644 index 000000000..7f0f3490c --- /dev/null +++ b/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/config/entities/SpanAccumulatorConfiguration.scala @@ -0,0 +1,33 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trace.indexer.config.entities + +import com.expedia.www.haystack.trace.commons.packer.PackerType.PackerType + +/** + * @param minTracesPerCache minimum number of traces that will reside in each store. + * @param maxEntriesAllStores maximum number of records across all state stores, one record is one span buffer object + * @param pollIntervalMillis poll interval to gather the buffered-spans that are ready to emit out to sink + * @param bufferingWindowMillis time window for which unique traceId will be hold to gather its child spans + * @param packerType apply the compression on the spanbuffer before storing to trace-backend + */ +case class SpanAccumulatorConfiguration(minTracesPerCache: Int, + maxEntriesAllStores: Int, + pollIntervalMillis: Long, + bufferingWindowMillis: Long, + packerType: PackerType) diff --git a/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/config/entities/TraceBackendConfiguration.scala b/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/config/entities/TraceBackendConfiguration.scala new file mode 100644 index 000000000..929b83753 --- /dev/null +++ b/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/config/entities/TraceBackendConfiguration.scala @@ -0,0 +1,27 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trace.indexer.config.entities + +import com.expedia.www.haystack.trace.commons.config.entities.TraceStoreBackends + +/** + * @param clientConfig defines the grpc client configuration for connecting to the trace backend + * @param maxInFlightRequests defines the max parallel writes to trace-backend + */ +case class TraceBackendConfiguration(clientConfig: TraceStoreBackends, + maxInFlightRequests: Int) diff --git a/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/metrics/AppMetricNames.scala b/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/metrics/AppMetricNames.scala new file mode 100644 index 000000000..f260fca83 --- /dev/null +++ b/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/metrics/AppMetricNames.scala @@ -0,0 +1,42 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trace.indexer.metrics + +/** + * list all app metric names that are published on jmx + */ +object AppMetricNames { + val PROCESS_TIMER = "buffer.process" + val KAFKA_ITERATOR_AGE_MS = "kafka.iterator.age.ms" + + val BUFFERED_SPANS_COUNT = "buffered.spans.count" + val STATE_STORE_EVICTION = "state.store.eviction" + val SPAN_PROTO_DESER_FAILURE = "span.proto.deser.failure" + + val BACKEND_WRITE_TIME = "backend.write.time" + val BACKEND_WRITE_FAILURE = "backend.write.failure" + val BACKEND_WRITE_WARNINGS = "backend.write.warnings" + + val ES_WRITE_FAILURE = "es.write.failure" + val ES_WRITE_TIME = "es.writer.time" + + val METADATA_WRITE_TIME = "metadata.write.time" + val METADATA_WRITE_FAILURE = "metadata.write.failure" + + val KAFKA_PRODUCE_FAILURES = "kafka.produce.failure" +} diff --git a/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/processors/SpanIndexProcessor.scala b/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/processors/SpanIndexProcessor.scala new file mode 100644 index 000000000..c26f98214 --- /dev/null +++ b/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/processors/SpanIndexProcessor.scala @@ -0,0 +1,133 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trace.indexer.processors + +import com.codahale.metrics.{Histogram, Timer} +import com.expedia.open.tracing.Span +import com.expedia.open.tracing.buffer.SpanBuffer +import com.expedia.www.haystack.commons.metrics.MetricsSupport +import com.expedia.www.haystack.trace.commons.packer.Packer +import com.expedia.www.haystack.trace.indexer.config.entities.SpanAccumulatorConfiguration +import com.expedia.www.haystack.trace.indexer.metrics.AppMetricNames.{BUFFERED_SPANS_COUNT, KAFKA_ITERATOR_AGE_MS, PROCESS_TIMER} +import com.expedia.www.haystack.trace.indexer.store.SpanBufferMemoryStoreSupplier +import com.expedia.www.haystack.trace.indexer.store.data.model.SpanBufferWithMetadata +import com.expedia.www.haystack.trace.indexer.store.traits.{EldestBufferedSpanEvictionListener, SpanBufferKeyValueStore} +import com.expedia.www.haystack.trace.indexer.writers.TraceWriter +import org.apache.kafka.clients.consumer.{ConsumerRecord, OffsetAndMetadata} +import org.slf4j.{Logger, LoggerFactory} + +import scala.concurrent.ExecutionContextExecutor + +object SpanIndexProcessor extends MetricsSupport { + protected val LOGGER: Logger = LoggerFactory.getLogger(SpanIndexProcessor.getClass) + + protected val processTimer: Timer = metricRegistry.timer(PROCESS_TIMER) + protected val bufferedSpansHistogram: Histogram = metricRegistry.histogram(BUFFERED_SPANS_COUNT) + protected val iteratorAge: Histogram = metricRegistry.histogram(KAFKA_ITERATOR_AGE_MS) +} + +class SpanIndexProcessor(accumulatorConfig: SpanAccumulatorConfiguration, + storeSupplier: SpanBufferMemoryStoreSupplier, + writers: Seq[TraceWriter], + spanBufferPacker: Packer[SpanBuffer])(implicit val dispatcher: ExecutionContextExecutor) + extends StreamProcessor[String, Span] with EldestBufferedSpanEvictionListener { + + import com.expedia.www.haystack.trace.indexer.processors.SpanIndexProcessor._ + + private var spanBufferMemStore: SpanBufferKeyValueStore = _ + + // defines the last time we look into the store for emitting the traces + private var lastEmitTimestamp: Long = 0L + + override def init(): Unit = { + spanBufferMemStore = storeSupplier.get() + spanBufferMemStore.init() + spanBufferMemStore.addEvictionListener(this) + LOGGER.info("Span Index Processor has been initialized successfully!") + } + + override def close(): Unit = { + spanBufferMemStore.close() + LOGGER.info("Span Index Processor has been closed now!") + } + + override def process(records: Iterable[ConsumerRecord[String, Span]]): Option[OffsetAndMetadata] = { + val timer = processTimer.time() + try { + var currentTimestamp = 0L + var minEventTime = Long.MaxValue + + records + .filter(_ != null) + .foreach { + record => { + spanBufferMemStore.addOrUpdateSpanBuffer(record.key(), record.value(), record.timestamp(), record.offset()) + currentTimestamp = Math.max(record.timestamp(), currentTimestamp) + + // record the smallest event timestamp observed across the spans + if (record.value().getStartTime > 0) { + minEventTime = Math.min(record.value().getStartTime, minEventTime) // this is in micros + } + } + } + + iteratorAge.update(System.currentTimeMillis() - (minEventTime/1000l)) + mayBeEmit(currentTimestamp) + } finally { + timer.stop() + } + } + + private def writeTrace(spanBuffer: SpanBuffer, isLastSpanBuffer: Boolean) = { + // get a metric on spans that are buffered before we write them to external databases + bufferedSpansHistogram.update(spanBuffer.getChildSpansCount) + + val traceId = spanBuffer.getTraceId + val packedMessage = spanBufferPacker.apply(spanBuffer) + writers.foreach { + writer => + writer.writeAsync(traceId, packedMessage, isLastSpanBuffer) + } + } + + private def mayBeEmit(currentTimestamp: Long): Option[OffsetAndMetadata] = { + if ((currentTimestamp - accumulatorConfig.pollIntervalMillis) > lastEmitTimestamp) { + + var committableOffset = -1L + + val emittableSpanBuffers = spanBufferMemStore.getAndRemoveSpanBuffersOlderThan(currentTimestamp - accumulatorConfig.bufferingWindowMillis) + + emittableSpanBuffers.zipWithIndex foreach { + case (sb, idx) => + val spanBuffer = sb.builder.build() + writeTrace(spanBuffer, idx == emittableSpanBuffers.size - 1) + if (committableOffset < sb.firstSeenSpanKafkaOffset) committableOffset = sb.firstSeenSpanKafkaOffset + } + + lastEmitTimestamp = currentTimestamp + + if (committableOffset >= 0) Some(new OffsetAndMetadata(committableOffset)) else None + } else { + None + } + } + + // for now we set islastSpanBuffer as false. + // if too many eviction happens, then writer will flush it out eventually + override def onEvict(key: String, value: SpanBufferWithMetadata): Unit = writeTrace(value.builder.build(), isLastSpanBuffer = false) +} diff --git a/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/processors/StateListener.scala b/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/processors/StateListener.scala new file mode 100644 index 000000000..07ef22c95 --- /dev/null +++ b/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/processors/StateListener.scala @@ -0,0 +1,29 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trace.indexer.processors + +import com.expedia.www.haystack.trace.indexer.processors.StreamTaskState.StreamTaskState + +object StreamTaskState extends Enumeration { + type StreamTaskState = Value + val NOT_RUNNING, RUNNING, FAILED, CLOSED = Value +} + +trait StateListener { + def onTaskStateChange(state: StreamTaskState) +} diff --git a/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/processors/StreamProcessor.scala b/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/processors/StreamProcessor.scala new file mode 100644 index 000000000..e01d4e656 --- /dev/null +++ b/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/processors/StreamProcessor.scala @@ -0,0 +1,28 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trace.indexer.processors + +import org.apache.kafka.clients.consumer.{ConsumerRecord, OffsetAndMetadata} + +trait StreamProcessor[K, V] { + def process(record: Iterable[ConsumerRecord[K, V]]): Option[OffsetAndMetadata] + + def close(): Unit + + def init(): Unit +} diff --git a/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/processors/StreamTaskRunnable.scala b/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/processors/StreamTaskRunnable.scala new file mode 100644 index 000000000..8c8312e16 --- /dev/null +++ b/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/processors/StreamTaskRunnable.scala @@ -0,0 +1,265 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trace.indexer.processors + +import java.util +import java.util.Properties +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.{ConcurrentHashMap, Executors, TimeUnit} + +import com.expedia.open.tracing.Span +import com.expedia.www.haystack.trace.indexer.config.entities.KafkaConfiguration +import com.expedia.www.haystack.trace.indexer.processors.StreamTaskState.StreamTaskState +import com.expedia.www.haystack.trace.indexer.processors.supplier.StreamProcessorSupplier +import org.apache.kafka.clients.consumer._ +import org.apache.kafka.common.TopicPartition +import org.apache.kafka.common.errors.WakeupException +import org.slf4j.LoggerFactory + +import scala.annotation.tailrec +import scala.collection.JavaConverters._ +import scala.collection.mutable +import scala.util.Try + +class StreamTaskRunnable(taskId: Int, kafkaConfig: KafkaConfiguration, processorSupplier: StreamProcessorSupplier[String, Span]) + extends Runnable with AutoCloseable { + + private val LOGGER = LoggerFactory.getLogger(classOf[StreamTaskRunnable]) + + /** + * consumer rebalance listener + */ + private class RebalanceListener extends ConsumerRebalanceListener { + + /** + * close the running processors for the revoked partitions + * + * @param revokedPartitions revoked partitions + */ + override def onPartitionsRevoked(revokedPartitions: util.Collection[TopicPartition]): Unit = { + LOGGER.info("Partitions {} revoked at the beginning of consumer rebalance for taskId={}", revokedPartitions, taskId) + + revokedPartitions.asScala.foreach( + p => { + val processor = processors.remove(p) + if (processor != null) processor.close() + }) + } + + /** + * create processors for newly assigned partitions + * + * @param assignedPartitions newly assigned partitions + */ + override def onPartitionsAssigned(assignedPartitions: util.Collection[TopicPartition]): Unit = { + LOGGER.info("Partitions {} assigned at the beginning of consumer rebalance for taskId={}", assignedPartitions, taskId) + + assignedPartitions.asScala foreach { + partition => { + val processor = processorSupplier.get() + val previousProcessor = processors.putIfAbsent(partition, processor) + if (previousProcessor == null) processor.init() + } + } + } + } + + @volatile + private var state = StreamTaskState.NOT_RUNNING + private var wakeups: Int = 0 + + private val shutdownRequested = new AtomicBoolean(false) + private val wakeupScheduler = Executors.newScheduledThreadPool(1) + private val listeners = mutable.ListBuffer[StateListener]() + private val processors = new ConcurrentHashMap[TopicPartition, StreamProcessor[String, Span]]() + + private val consumer = { + val props = new Properties() + kafkaConfig.consumerProps.entrySet().asScala.foreach(entry => props.put(entry.getKey, entry.getValue)) + props.setProperty(ConsumerConfig.CLIENT_ID_CONFIG, taskId.toString) + new KafkaConsumer[String, Span](props) + } + + private val rebalanceListener = new RebalanceListener + + consumer.subscribe(util.Arrays.asList(kafkaConfig.consumeTopic), rebalanceListener) + + /** + * Execute the stream processors + * + */ + override def run(): Unit = { + LOGGER.info("Starting stream processing thread with id={}", taskId) + try { + updateStateAndNotify(StreamTaskState.RUNNING) + runLoop() + } catch { + case ie: InterruptedException => + LOGGER.error(s"This stream task with taskId=$taskId has been interrupted", ie) + case ex: Exception => + if (!shutdownRequested.get()) updateStateAndNotify(StreamTaskState.FAILED) + // may be logging the exception again for kafka specific exceptions, but it is ok. + LOGGER.error(s"Stream application faced an exception during processing for taskId=$taskId: ", ex) + } + finally { + consumer.close(kafkaConfig.consumerCloseTimeoutInMillis, TimeUnit.MILLISECONDS) + updateStateAndNotify(StreamTaskState.CLOSED) + } + } + + /** + * invoke the processor per partition for the records that are read from kafka. + * Update the offsets (if any) that need to be committed in the committableOffsets map + * + * @param partition kafka partition + * @param partitionRecords records of the given kafka partition + * @param committableOffsets offsets that need to be committed for the given topic partition + */ + private def invokeProcessor(partition: Int, + partitionRecords: Iterable[ConsumerRecord[String, Span]], + committableOffsets: util.HashMap[TopicPartition, OffsetAndMetadata]): Unit = { + val topicPartition = new TopicPartition(kafkaConfig.consumeTopic, partition) + val processor = processors.get(topicPartition) + + if (processor != null) { + processor.process(partitionRecords) match { + case Some(offsetMetadata) => committableOffsets.put(topicPartition, offsetMetadata) + case _ => /* the processor has nothing to commit for now */ + } + } + } + + /** + * run the consumer loop till the shutdown is requested or any exception is thrown + */ + private def runLoop(): Unit = { + while (!shutdownRequested.get()) { + poll() match { + case Some(records) if records != null && !records.isEmpty && !processors.isEmpty => + val committableOffsets = new util.HashMap[TopicPartition, OffsetAndMetadata]() + val groupedByPartition = records.asScala.groupBy(_.partition()) + + groupedByPartition foreach { + case (partition, partitionRecords) => invokeProcessor(partition, partitionRecords, committableOffsets) + } + + // commit offsets + commit(committableOffsets) + // if no records are returned in poll, then do nothing + case _ => + } + } + } + + /** + * before requesting consumer.poll(), schedule a wakeup call as poll() may hang due to network errors in kafka + * if the poll() doesnt return after a timeout, then wakeup the consumer. + * + * @return consumer records from kafka + */ + private def poll(): Option[ConsumerRecords[String, Span]] = { + + def scheduleWakeup() = wakeupScheduler.schedule(new Runnable { + override def run(): Unit = consumer.wakeup() + }, kafkaConfig.wakeupTimeoutInMillis, TimeUnit.MILLISECONDS) + + def handleWakeup(we: WakeupException): Unit = { + // if in shutdown phase, then do not swallow the exception, throw it to upstream + if (shutdownRequested.get()) throw we + + wakeups = wakeups + 1 + if (wakeups == kafkaConfig.maxWakeups) { + LOGGER.error(s"WakeupException limit exceeded, throwing up wakeup exception for taskId=$taskId.", we) + throw we + } else { + LOGGER.error(s"Consumer poll took more than ${kafkaConfig.wakeupTimeoutInMillis} ms for taskId=$taskId, wakeup attempt=$wakeups!. Will try poll again!") + } + } + + val wakeupCall = scheduleWakeup() + + try { + val records: ConsumerRecords[String, Span] = consumer.poll(kafkaConfig.pollTimeoutMs) + wakeups = 0 + Some(records) + } catch { + case we: WakeupException => + handleWakeup(we) + None + } finally { + Try(wakeupCall.cancel(true)) + } + } + + /** + * commit the offset to kafka with a retry logic + * + * @param offsets map of offsets for each topic partition + * @param retryAttempt current retry attempt + */ + @tailrec + private def commit(offsets: util.HashMap[TopicPartition, OffsetAndMetadata], retryAttempt: Int = 0): Unit = { + try { + if (!offsets.isEmpty && retryAttempt <= kafkaConfig.commitOffsetRetries) { + consumer.commitSync(offsets) + } + } catch { + case _: CommitFailedException => + Thread.sleep(kafkaConfig.commitBackoffInMillis) + // retry offset again + commit(offsets, retryAttempt + 1) + case ex: Exception => + LOGGER.error("Fail to commit the offsets with exception", ex) + } + } + + private def updateStateAndNotify(newState: StreamTaskState) = { + if (state != newState) { + state = newState + + // invoke listeners for any state change + listeners foreach (listener => listener.onTaskStateChange(state)) + } + } + + /** + * close the runnable. If still in running state, then wakeup the consumer + */ + override def close(): Unit = { + Try { + LOGGER.info(s"Close has been requested for taskId=$taskId") + shutdownRequested.set(true) + if (isStillRunning) consumer.wakeup() + wakeupScheduler.shutdown() + } + } + + /** + * if consumer is still in running state + * + * @return + */ + def isStillRunning: Boolean = state == StreamTaskState.RUNNING + + /** + * set the state change listener + * + * @param listener state change listener + */ + def setStateListener(listener: StateListener): Unit = listeners += listener +} diff --git a/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/processors/supplier/SpanIndexProcessorSupplier.scala b/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/processors/supplier/SpanIndexProcessorSupplier.scala new file mode 100644 index 000000000..64529f606 --- /dev/null +++ b/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/processors/supplier/SpanIndexProcessorSupplier.scala @@ -0,0 +1,39 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trace.indexer.processors.supplier + +import com.expedia.open.tracing.Span +import com.expedia.open.tracing.buffer.SpanBuffer +import com.expedia.www.haystack.trace.commons.packer.Packer +import com.expedia.www.haystack.trace.indexer.config.entities.SpanAccumulatorConfiguration +import com.expedia.www.haystack.trace.indexer.processors.{SpanIndexProcessor, StreamProcessor} +import com.expedia.www.haystack.trace.indexer.store.SpanBufferMemoryStoreSupplier +import com.expedia.www.haystack.trace.indexer.writers.TraceWriter + +import scala.concurrent.ExecutionContextExecutor + +class SpanIndexProcessorSupplier(accumulatorConfig: SpanAccumulatorConfiguration, + storeSupplier: SpanBufferMemoryStoreSupplier, + writers: Seq[TraceWriter], + spanBufferPacker: Packer[SpanBuffer])(implicit val dispatcher: ExecutionContextExecutor) + extends StreamProcessorSupplier[String, Span] { + + override def get(): StreamProcessor[String, Span] = { + new SpanIndexProcessor(accumulatorConfig, storeSupplier, writers, spanBufferPacker) + } +} diff --git a/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/processors/supplier/StreamProcessorSupplier.scala b/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/processors/supplier/StreamProcessorSupplier.scala new file mode 100644 index 000000000..ba7e7f6ff --- /dev/null +++ b/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/processors/supplier/StreamProcessorSupplier.scala @@ -0,0 +1,24 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trace.indexer.processors.supplier + +import com.expedia.www.haystack.trace.indexer.processors.StreamProcessor + +trait StreamProcessorSupplier[K, V] { + def get(): StreamProcessor[K, V] +} diff --git a/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/serde/SpanDeserializer.scala b/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/serde/SpanDeserializer.scala new file mode 100644 index 000000000..47b921231 --- /dev/null +++ b/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/serde/SpanDeserializer.scala @@ -0,0 +1,50 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trace.indexer.serde + +import java.util + +import com.expedia.open.tracing.Span +import com.expedia.www.haystack.commons.metrics.MetricsSupport +import com.expedia.www.haystack.trace.indexer.metrics.AppMetricNames +import org.apache.kafka.common.serialization.Deserializer + +class SpanDeserializer extends Deserializer[Span] with MetricsSupport { + + private val spanDeserMeter = metricRegistry.meter(AppMetricNames.SPAN_PROTO_DESER_FAILURE) + + override def configure(configs: util.Map[String, _], isKey: Boolean): Unit = () + + override def close(): Unit = () + + /** + * converts the binary protobuf bytes into Span object + * @param data serialized bytes of Span + * @return + */ + override def deserialize(topic: String, data: Array[Byte]): Span = { + try { + if(data == null || data.length == 0) null else Span.parseFrom(data) + } catch { + case _: Exception => + /* may be log and add metric */ + spanDeserMeter.mark() + null + } + } +} diff --git a/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/store/DynamicCacheSizer.scala b/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/store/DynamicCacheSizer.scala new file mode 100644 index 000000000..d969b2081 --- /dev/null +++ b/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/store/DynamicCacheSizer.scala @@ -0,0 +1,66 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trace.indexer.store + +import com.expedia.www.haystack.trace.indexer.store.traits.CacheSizeObserver + +import scala.collection.mutable + +class DynamicCacheSizer(val minTracesPerCache: Int, maxEntriesAcrossCaches: Int) { + + private val cacheObservers = mutable.HashSet[CacheSizeObserver]() + + /** + * adds cache observer + * + * @param observer state store acts as an observer + */ + def addCacheObserver(observer: CacheSizeObserver): Unit = { + this.synchronized { + cacheObservers.add(observer) + evaluateNewCacheSizeAndNotify(cacheObservers) + } + } + + /** + * removes cache observer + * @param observer state store acts as an observer + */ + def removeCacheObserver(observer: CacheSizeObserver): Unit = { + this.synchronized { + cacheObservers.remove(observer) + evaluateNewCacheSizeAndNotify(cacheObservers) + } + } + + /** + * Cache sizing strategy is simple, distribute the maxEntriesAcrossCaches across all observers + * @param observers list of changed observers + */ + private def evaluateNewCacheSizeAndNotify(observers: mutable.HashSet[CacheSizeObserver]): Unit = { + //notify the observers with a change in their cache size + def notifyObservers(newMaxEntriesPerCache: Int): Unit = { + observers.foreach(obs => obs.onCacheSizeChange(newMaxEntriesPerCache)) + } + + if(observers.nonEmpty) { + val newMaxEntriesPerCache = Math.floor(maxEntriesAcrossCaches / observers.size).toInt + notifyObservers(newMaxEntriesPerCache) + } + } +} diff --git a/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/store/SpanBufferMemoryStoreSupplier.scala b/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/store/SpanBufferMemoryStoreSupplier.scala new file mode 100644 index 000000000..19322e33b --- /dev/null +++ b/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/store/SpanBufferMemoryStoreSupplier.scala @@ -0,0 +1,34 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trace.indexer.store + +import com.expedia.www.haystack.trace.indexer.store.impl.SpanBufferMemoryStore +import com.expedia.www.haystack.trace.indexer.store.traits.SpanBufferKeyValueStore + +class SpanBufferMemoryStoreSupplier(minTracesPerCache: Int, + maxEntriesAcrossStores: Int) + extends StoreSupplier[SpanBufferKeyValueStore] { + + private val dynamicCacheSizer = new DynamicCacheSizer(minTracesPerCache, maxEntriesAcrossStores) + + /** + * @return kv store for maintaining buffered-spans. If logging is enabled, we persist the changelog to kafka topic + * else it is purely in-memory + */ + override def get(): SpanBufferKeyValueStore = new SpanBufferMemoryStore(dynamicCacheSizer) +} diff --git a/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/store/StoreSupplier.scala b/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/store/StoreSupplier.scala new file mode 100644 index 000000000..1111f0436 --- /dev/null +++ b/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/store/StoreSupplier.scala @@ -0,0 +1,22 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trace.indexer.store + +trait StoreSupplier[K] { + def get(): K +} diff --git a/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/store/data/model/SpanBufferWithMetadata.scala b/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/store/data/model/SpanBufferWithMetadata.scala new file mode 100644 index 000000000..5ec142db5 --- /dev/null +++ b/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/store/data/model/SpanBufferWithMetadata.scala @@ -0,0 +1,25 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ASpanGroupWithTimestampNY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.trace.indexer.store.data.model + +import com.expedia.open.tracing.buffer.SpanBuffer + +/** + * @param builder protobuf builder for building span buffer. + * @param firstSpanSeenAt timestamp when the first span of a given traceId is seen + */ +case class SpanBufferWithMetadata(builder: SpanBuffer.Builder, firstSpanSeenAt: Long, firstSeenSpanKafkaOffset: Long) diff --git a/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/store/impl/SpanBufferMemoryStore.scala b/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/store/impl/SpanBufferMemoryStore.scala new file mode 100644 index 000000000..a7bcf11d4 --- /dev/null +++ b/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/store/impl/SpanBufferMemoryStore.scala @@ -0,0 +1,129 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trace.indexer.store.impl + +import java.util +import java.util.concurrent.atomic.AtomicInteger + +import com.codahale.metrics.Meter +import com.expedia.open.tracing.Span +import com.expedia.open.tracing.buffer.SpanBuffer +import com.expedia.www.haystack.commons.metrics.MetricsSupport +import com.expedia.www.haystack.trace.indexer.metrics.AppMetricNames._ +import com.expedia.www.haystack.trace.indexer.store.DynamicCacheSizer +import com.expedia.www.haystack.trace.indexer.store.data.model.SpanBufferWithMetadata +import com.expedia.www.haystack.trace.indexer.store.traits.{CacheSizeObserver, EldestBufferedSpanEvictionListener, SpanBufferKeyValueStore} +import org.slf4j.{Logger, LoggerFactory} + +import scala.collection.mutable + +object SpanBufferMemoryStore extends MetricsSupport { + protected val LOGGER: Logger = LoggerFactory.getLogger(SpanBufferMemoryStore.getClass) + protected val evictionMeter: Meter = metricRegistry.meter(STATE_STORE_EVICTION) +} + +class SpanBufferMemoryStore(cacheSizer: DynamicCacheSizer) extends SpanBufferKeyValueStore with CacheSizeObserver { + import SpanBufferMemoryStore._ + + @volatile protected var open = false + + // This maxEntries will be adjusted by the dynamic cacheSizer, lets default it to a reasonable value 10000 + protected val maxEntries = new AtomicInteger(10000) + private val listeners: mutable.ListBuffer[EldestBufferedSpanEvictionListener] = mutable.ListBuffer() + private var totalSpansInMemStore: Int = 0 + private var map: util.LinkedHashMap[String, SpanBufferWithMetadata] = _ + + override def init() { + cacheSizer.addCacheObserver(this) + + // initialize the map + map = new util.LinkedHashMap[String, SpanBufferWithMetadata](cacheSizer.minTracesPerCache, 1.01f, false) { + override protected def removeEldestEntry(eldest: util.Map.Entry[String, SpanBufferWithMetadata]): Boolean = { + val evict = totalSpansInMemStore >= maxEntries.get() + if (evict) { + evictionMeter.mark() + totalSpansInMemStore -= eldest.getValue.builder.getChildSpansCount + listeners.foreach(listener => listener.onEvict(eldest.getKey, eldest.getValue)) + } + evict + } + } + + open = true + + LOGGER.info("Span buffer memory store has been initialized") + } + + /** + * removes and returns all the span buffers from the map that are recorded before the given timestamp + * + * @param timestamp timestamp before which all buffered spans should be read and removed + * @return + */ + override def getAndRemoveSpanBuffersOlderThan(timestamp: Long): mutable.ListBuffer[SpanBufferWithMetadata] = { + val result = mutable.ListBuffer[SpanBufferWithMetadata]() + + val iterator = this.map.entrySet().iterator() + var done = false + + while (!done && iterator.hasNext) { + val el = iterator.next() + if (el.getValue.firstSpanSeenAt <= timestamp) { + iterator.remove() + totalSpansInMemStore -= el.getValue.builder.getChildSpansCount + result += el.getValue + } else { + // here we apply a basic optimization and skip further iteration because all following records + // in this map will have higher recordTimestamp. When we insert the first span for a unique traceId + // in the map, we set the 'firstRecordTimestamp' attribute with record's timestamp + done = true + } + } + result + } + + override def addEvictionListener(l: EldestBufferedSpanEvictionListener): Unit = this.listeners += l + + override def close(): Unit = { + if(open) { + LOGGER.info("Closing the span buffer memory store") + cacheSizer.removeCacheObserver(this) + open = false + } + } + + def onCacheSizeChange(maxEntries: Int): Unit = { + LOGGER.info("Cache size has been changed to " + maxEntries) + this.maxEntries.set(maxEntries) + } + + override def addOrUpdateSpanBuffer(traceId: String, span: Span, spanRecordTimestamp: Long, offset: Long): SpanBufferWithMetadata = { + var value = this.map.get(traceId) + if (value == null) { + val spanBuffer = SpanBuffer.newBuilder().setTraceId(span.getTraceId).addChildSpans(span) + value = SpanBufferWithMetadata(spanBuffer, spanRecordTimestamp, offset) + this.map.put(traceId, value) + } else { + value.builder.addChildSpans(span) + } + totalSpansInMemStore += 1 + value + } + + def totalSpans: Int = totalSpansInMemStore +} diff --git a/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/store/traits/CacheSizeObserver.scala b/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/store/traits/CacheSizeObserver.scala new file mode 100644 index 000000000..69d1665f8 --- /dev/null +++ b/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/store/traits/CacheSizeObserver.scala @@ -0,0 +1,26 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trace.indexer.store.traits + +/** + * this is an observer that is called whenever maxSize of the cache is changed. This happens when kafka partitions + * are assigned or revoked resulting in a change of total number of state stores + */ +trait CacheSizeObserver { + def onCacheSizeChange(maxEntries: Int): Unit +} diff --git a/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/store/traits/EldestBufferedSpanEvictionListener.scala b/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/store/traits/EldestBufferedSpanEvictionListener.scala new file mode 100644 index 000000000..86a07b0fc --- /dev/null +++ b/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/store/traits/EldestBufferedSpanEvictionListener.scala @@ -0,0 +1,27 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trace.indexer.store.traits + +import com.expedia.www.haystack.trace.indexer.store.data.model.SpanBufferWithMetadata + +/** + * the listener is called when the eldest buffered span is evicted from the cache + */ +trait EldestBufferedSpanEvictionListener { + def onEvict(key: String, value: SpanBufferWithMetadata): Unit +} diff --git a/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/store/traits/SpanBufferKeyValueStore.scala b/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/store/traits/SpanBufferKeyValueStore.scala new file mode 100644 index 000000000..69d430370 --- /dev/null +++ b/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/store/traits/SpanBufferKeyValueStore.scala @@ -0,0 +1,62 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trace.indexer.store.traits + +import com.expedia.open.tracing.Span +import com.expedia.www.haystack.trace.indexer.store.data.model.SpanBufferWithMetadata + +import scala.collection.mutable + +/** + * this interface extends KeyValueStore to provide span buffering operations + */ +trait SpanBufferKeyValueStore { + + /** + * get all buffered span objects that are recorded before the given timestamp + * @param timestamp timestamp in millis + * @return + */ + def getAndRemoveSpanBuffersOlderThan(timestamp: Long): mutable.ListBuffer[SpanBufferWithMetadata] + + /** + * add a listener to the store, that gets called when the eldest spanBuffer is evicted + * due to constraints of maxEntries in the store cache + * @param l listener object that is called by the store + */ + def addEvictionListener(l: EldestBufferedSpanEvictionListener): Unit + + /** + * adds new spanBuffer for the traceId(if absent)in the store else add the spans + * @param traceId traceId + * @param span span object + * @param spanRecordTimestamp timestamp of the span record + * @param offset kafka offset of this span record + */ + def addOrUpdateSpanBuffer(traceId: String, span: Span, spanRecordTimestamp: Long, offset: Long): SpanBufferWithMetadata + + /** + * close the store + */ + def close() + + /** + * init the store + */ + def init() +} diff --git a/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/writers/TraceWriter.scala b/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/writers/TraceWriter.scala new file mode 100644 index 000000000..39ed338ea --- /dev/null +++ b/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/writers/TraceWriter.scala @@ -0,0 +1,32 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trace.indexer.writers + +import com.expedia.open.tracing.buffer.SpanBuffer +import com.expedia.www.haystack.trace.commons.packer.PackedMessage + +trait TraceWriter extends AutoCloseable { + + /** + * writes the span buffer to external store like grpc, elastic, or kafka + * @param traceId trace id + * @param packedSpanBuffer compressed serialized bytes of the span buffer object + * @param isLastSpanBuffer tells if this is the last record, so the writer can flush + */ + def writeAsync(traceId: String, packedSpanBuffer: PackedMessage[SpanBuffer], isLastSpanBuffer: Boolean) +} diff --git a/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/writers/es/ElasticSearchResultHandler.scala b/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/writers/es/ElasticSearchResultHandler.scala new file mode 100644 index 000000000..5f999b7fd --- /dev/null +++ b/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/writers/es/ElasticSearchResultHandler.scala @@ -0,0 +1,64 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trace.indexer.writers.es + +import com.codahale.metrics.{Meter, Timer} +import com.expedia.www.haystack.commons.retries.RetryOperation +import io.searchbox.client.JestResultHandler +import io.searchbox.core.BulkResult +import org.slf4j.{Logger, LoggerFactory} + +import scala.collection.JavaConverters._ + +class ElasticSearchResultHandler(timer: Timer.Context, failureMeter: Meter, retryOp: RetryOperation.Callback) + extends JestResultHandler[BulkResult] { + + protected val LOGGER: Logger = LoggerFactory.getLogger(classOf[ElasticSearchResultHandler]) + + /** + * this callback is invoked when the elastic search writes is completed with success or warnings + * + * @param result bulk result + */ + def completed(result: BulkResult): Unit = { + timer.close() + + // group the failed items as per status and log once for such a failed item + if (result.getFailedItems != null) { + result.getFailedItems.asScala.groupBy(_.status) foreach { + case (statusCode, failedItems) => + failureMeter.mark(failedItems.size) + LOGGER.error(s"Index operation has failed with status=$statusCode, totalFailedItems=${failedItems.size}, " + + s"errorReason=${failedItems.head.errorReason}, errorType=${failedItems.head.errorType}") + } + } + retryOp.onResult(result) + } + + /** + * this callback is invoked when the writes to elastic search fail completely + * + * @param ex the exception contains the reason of failure + */ + def failed(ex: Exception): Unit = { + timer.close() + failureMeter.mark() + LOGGER.error("Fail to write the documents in elastic search with reason:", ex) + retryOp.onError(ex, retry = true) + } +} diff --git a/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/writers/es/ElasticSearchWriter.scala b/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/writers/es/ElasticSearchWriter.scala new file mode 100644 index 000000000..162472d20 --- /dev/null +++ b/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/writers/es/ElasticSearchWriter.scala @@ -0,0 +1,165 @@ +/* + * Copyright 2019, Expedia Group. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trace.indexer.writers.es + +import java.util.concurrent.Semaphore + +import com.expedia.open.tracing.buffer.SpanBuffer +import com.expedia.www.haystack.commons.metrics.MetricsSupport +import com.expedia.www.haystack.commons.retries.RetryOperation._ +import com.expedia.www.haystack.trace.commons.clients.es.AWSSigningJestClientFactory +import com.expedia.www.haystack.trace.commons.config.entities.WhitelistIndexFieldConfiguration +import com.expedia.www.haystack.trace.commons.packer.PackedMessage +import com.expedia.www.haystack.trace.indexer.config.entities.ElasticSearchConfiguration +import com.expedia.www.haystack.trace.indexer.metrics.AppMetricNames +import com.expedia.www.haystack.trace.indexer.writers.TraceWriter +import io.searchbox.client.config.HttpClientConfig +import io.searchbox.client.{JestClient, JestClientFactory} +import io.searchbox.core._ +import io.searchbox.params.Parameters +import org.joda.time.format.DateTimeFormat +import org.joda.time.{DateTime, DateTimeZone} +import org.slf4j.LoggerFactory + +import scala.collection.JavaConverters._ +import scala.util.Try + +object ElasticSearchWriterUtils { + + // creates an index name based on current date. following example illustrates the naming convention of + // elastic search indices: + // haystack-span-2017-08-30-1 + def indexName(prefix: String, indexHourBucket: Int, eventTimeMicros: Long): String = { + val eventTime = new DateTime(eventTimeMicros / 1000, DateTimeZone.UTC) + val dataFormatter = DateTimeFormat.forPattern("yyyy-MM-dd") + val bucket: Int = eventTime.getHourOfDay / indexHourBucket + s"$prefix-${dataFormatter.print(eventTime)}-$bucket" + } +} + +class ElasticSearchWriter(esConfig: ElasticSearchConfiguration, whitelistFieldConfig: WhitelistIndexFieldConfiguration) + extends TraceWriter with MetricsSupport { + private val LOGGER = LoggerFactory.getLogger(classOf[ElasticSearchWriter]) + + // meter that measures the write failures + private val esWriteFailureMeter = metricRegistry.meter(AppMetricNames.ES_WRITE_FAILURE) + + // a timer that measures the amount of time it takes to complete one bulk write + private val esWriteTime = metricRegistry.timer(AppMetricNames.ES_WRITE_TIME) + + // converts a span into an indexable document + private val documentGenerator = new IndexDocumentGenerator(whitelistFieldConfig) + + // this semaphore controls the parallel writes to index store + private val inflightRequestsSemaphore = new Semaphore(esConfig.maxInFlightBulkRequests, true) + + // initialize the elastic search client + private lazy val esClient: JestClient = { + LOGGER.info("Initializing the http elastic search client with endpoint={}", esConfig.endpoint) + + val factory = { + if (esConfig.awsRequestSigningConfiguration.enabled) { + LOGGER.info("using AWSSigningJestClientFactory for es client") + new AWSSigningJestClientFactory(esConfig.awsRequestSigningConfiguration) + } else { + LOGGER.info("using JestClientFactory for es client") + new JestClientFactory() + } + } + val builder = new HttpClientConfig.Builder(esConfig.endpoint) + .multiThreaded(true) + .connTimeout(esConfig.connectionTimeoutMillis) + .readTimeout(esConfig.readTimeoutMillis) + .defaultMaxTotalConnectionPerRoute(esConfig.maxConnectionsPerRoute) + + if (esConfig.username.isDefined && esConfig.password.isDefined) { + builder.defaultCredentials(esConfig.username.get, esConfig.password.get) + } + + factory.setHttpClientConfig(builder.build()) + val client = factory.getObject + new IndexTemplateHandler(client, esConfig.indexTemplateJson, esConfig.indexType, whitelistFieldConfig).run() + client + } + + private val bulkBuilder = new ThreadSafeBulkBuilder(esConfig.maxDocsInBulk, esConfig.maxBulkDocSizeInBytes) + + override def close(): Unit = { + LOGGER.info("Closing the elastic search client now.") + Try(esClient.shutdownClient()) + } + + /** + * converts the spans to an index document and writes to elastic search. Also if the parallel writes + * exceed the max inflight requests, then we block and this puts backpressure on upstream + * + * @param traceId trace id + * @param packedSpanBuffer list of spans belonging to this traceId - packed bytes of span buffer + * @param isLastSpanBuffer tells if this is the last record, so the writer can flush + * @return + */ + override def writeAsync(traceId: String, packedSpanBuffer: PackedMessage[SpanBuffer], isLastSpanBuffer: Boolean): Unit = { + var isSemaphoreAcquired = false + + try { + val eventTimeInMicros = packedSpanBuffer.protoObj.getChildSpansList.asScala.head.getStartTime + val indexName = ElasticSearchWriterUtils.indexName(esConfig.indexNamePrefix, esConfig.indexHourBucket, eventTimeInMicros) + addIndexOperation(traceId, packedSpanBuffer.protoObj, indexName, isLastSpanBuffer) match { + case Some(bulkToDispatch) => + inflightRequestsSemaphore.acquire() + isSemaphoreAcquired = true + + // execute the request async with retry + withRetryBackoff((retryCallback) => { + esClient.executeAsync(bulkToDispatch, + new ElasticSearchResultHandler(esWriteTime.time(), esWriteFailureMeter, retryCallback)) + }, + esConfig.retryConfig, + onSuccess = (_: Any) => inflightRequestsSemaphore.release(), + onFailure = (ex) => { + inflightRequestsSemaphore.release() + LOGGER.error("Fail to write to ES after {} retry attempts", esConfig.retryConfig.maxRetries, ex) + }) + case _ => + } + } catch { + case ex: Exception => + if (isSemaphoreAcquired) inflightRequestsSemaphore.release() + esWriteFailureMeter.mark() + LOGGER.error("Failed to write spans to elastic search with exception", ex) + } + } + + private def addIndexOperation(traceId: String, spanBuffer: SpanBuffer, indexName: String, forceBulkCreate: Boolean): Option[Bulk] = { + // add all the spans as one document + val idxDocument = documentGenerator.createIndexDocument(traceId, spanBuffer) + + idxDocument match { + case Some(doc) => + val action: Index = new Index.Builder(doc.json) + .index(indexName) + .`type`(esConfig.indexType) + .setParameter(Parameters.CONSISTENCY, esConfig.consistencyLevel) + .build() + bulkBuilder.addAction(action, doc.json.getBytes("utf-8").length, forceBulkCreate) + case _ => + LOGGER.warn("Skipping the span buffer record for index operation for traceId={}!", traceId) + None + } + } +} diff --git a/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/writers/es/IndexDocumentGenerator.scala b/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/writers/es/IndexDocumentGenerator.scala new file mode 100644 index 000000000..ec53bffc1 --- /dev/null +++ b/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/writers/es/IndexDocumentGenerator.scala @@ -0,0 +1,158 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trace.indexer.writers.es + +import java.util.concurrent.TimeUnit + +import com.expedia.open.tracing.buffer.SpanBuffer +import com.expedia.open.tracing.{Span, Tag} +import com.expedia.www.haystack.commons.metrics.MetricsSupport +import com.expedia.www.haystack.trace.commons.clients.es.document.TraceIndexDoc +import com.expedia.www.haystack.trace.commons.clients.es.document.TraceIndexDoc.{OPERATION_KEY_NAME, SERVICE_KEY_NAME, TagValue} +import com.expedia.www.haystack.trace.commons.config.entities.IndexFieldType.IndexFieldType +import com.expedia.www.haystack.trace.commons.config.entities.{IndexFieldType, WhitelistIndexFieldConfiguration} +import org.apache.commons.lang3.StringUtils + +import scala.collection.JavaConverters._ +import scala.collection.mutable +import scala.util.{Failure, Success, Try} + +class IndexDocumentGenerator(config: WhitelistIndexFieldConfiguration) extends MetricsSupport { + + private val MIN_DURATION_FOR_TRUNCATION = TimeUnit.SECONDS.toMicros(20) + + /** + * @param spanBuffer a span buffer object + * @return index document that can be put in elastic search + */ + def createIndexDocument(traceId: String, spanBuffer: SpanBuffer): Option[TraceIndexDoc] = { + // We maintain a white list of tags that are to be indexed. The whitelist is maintained as a configuration + // in an external database (outside this app boundary). However, the app periodically reads this whitelist config + // and applies it to the new spans that are read. + val spanIndices = mutable.ListBuffer[mutable.Map[String, Any]]() + + var traceStartTime = Long.MaxValue + var rootDuration = 0l + + spanBuffer.getChildSpansList.asScala filter isValidForIndex foreach(span => { + + // calculate the trace starttime based on the minimum starttime observed across all child spans. + traceStartTime = Math.min(traceStartTime, truncateToSecondGranularity(span.getStartTime)) + if(span.getParentSpanId == null) rootDuration = span.getDuration + + val spanIndexDoc = spanIndices + .find(sp => sp(OPERATION_KEY_NAME).equals(span.getOperationName) && sp(SERVICE_KEY_NAME).equals(span.getServiceName)) + .getOrElse({ + val newSpanIndexDoc = mutable.Map[String, Any]( + SERVICE_KEY_NAME -> span.getServiceName, + OPERATION_KEY_NAME -> span.getOperationName) + spanIndices.append(newSpanIndexDoc) + newSpanIndexDoc + }) + updateSpanIndexDoc(spanIndexDoc, span) + }) + if (spanIndices.nonEmpty) Some(TraceIndexDoc(traceId, rootDuration, traceStartTime, spanIndices)) else None + } + + private def isValidForIndex(span: Span): Boolean = { + StringUtils.isNotEmpty(span.getServiceName) && StringUtils.isNotEmpty(span.getOperationName) + } + + /** + * transforms a span object into a index document. serviceName, operationName, duration and tags(depending upon the + * configuration) are used to create an index document. + * @param spanIndexDoc a span index document + * @param span a span object + * @return span index document as a map + */ + private def updateSpanIndexDoc(spanIndexDoc: mutable.Map[String, Any], span: Span): Unit = { + def append(key: String, value: Any): Unit = { + spanIndexDoc.getOrElseUpdate(key, mutable.Set[Any]()) + .asInstanceOf[mutable.Set[Any]] + .add(value) + } + + for (tag <- span.getTagsList.asScala; + normalizedTagKey = tag.getKey.toLowerCase; + indexField = config.indexFieldMap.get(normalizedTagKey); if indexField != null && indexField.enabled; + v = readTagValue(tag); + indexableValue = transformValueForIndexing(indexField.`type`, v); if indexableValue.isDefined) { + append(indexField.name, indexableValue) + } + + import com.expedia.www.haystack.trace.commons.clients.es.document.TraceIndexDoc._ + append(DURATION_KEY_NAME, adjustDurationForLowCardinality(span.getDuration)) + append(START_TIME_KEY_NAME, truncateToSecondGranularity(span.getStartTime)) + } + + + /** + * this method adjusts the tag's value to the indexing field type. Take an example of 'httpstatus' tag + * that we always want to index as a 'long' type in elastic search. Now services may send this tag value as string, + * hence in this method, we will transform the tag value to its expected type for e.g. long. + * In case we fail to adjust the type, we ignore the tag for indexing. + * @param fieldType expected field type that is valid for indexing + * @param value tag value + * @return tag value with adjusted(expected) type + */ + private def transformValueForIndexing(fieldType: IndexFieldType, value: TagValue): Option[TagValue] = { + Try (fieldType match { + case IndexFieldType.string => value.toString + case IndexFieldType.long | IndexFieldType.int => value.toString.toLong + case IndexFieldType.bool => value.toString.toBoolean + case IndexFieldType.double => value.toString.toDouble + case _ => value + }) match { + case Success(result) => Some(result) + case Failure(_) => + // TODO: should we also log the tag name etc? wondering if input is crazy, then we might end up logging too many errors + None + } + } + + /** + * converts the tag into key value pair + * @param tag span tag + * @return TagValue(Any) + */ + private def readTagValue(tag: Tag): TagValue = { + import com.expedia.open.tracing.Tag.TagType._ + + tag.getType match { + case BOOL => tag.getVBool + case STRING => tag.getVStr + case LONG => tag.getVLong + case DOUBLE => tag.getVDouble + case BINARY => tag.getVBytes.toStringUtf8 + case _ => throw new RuntimeException(s"Fail to understand the span tag type ${tag.getType} !!!") + } + } + + private def truncateToSecondGranularity(value: Long): Long = { + TimeUnit.SECONDS.toMicros(TimeUnit.MICROSECONDS.toSeconds(value)) + } + + private def adjustDurationForLowCardinality(value: Long): Long = { + // dont consider millis, if it accounts for less than 5% of the actual value + if (value > MIN_DURATION_FOR_TRUNCATION) { + truncateToSecondGranularity(value) + } else { + value + } + } +} diff --git a/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/writers/es/IndexTemplateHandler.scala b/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/writers/es/IndexTemplateHandler.scala new file mode 100644 index 000000000..dc6470e88 --- /dev/null +++ b/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/writers/es/IndexTemplateHandler.scala @@ -0,0 +1,106 @@ +/* + * Copyright 2019 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trace.indexer.writers.es + +import java.util + +import com.expedia.www.haystack.trace.commons.config.entities.IndexFieldType.IndexFieldType +import com.expedia.www.haystack.trace.commons.config.entities.{IndexFieldType, WhitelistIndexFieldConfiguration} +import com.fasterxml.jackson.core.`type`.TypeReference +import com.fasterxml.jackson.databind.ObjectMapper +import io.searchbox.client.JestClient +import io.searchbox.indices.template.{GetTemplate, PutTemplate} +import org.slf4j.LoggerFactory + +import scala.collection.JavaConverters._ + +class IndexTemplateHandler(client: JestClient, + applyTemplate: Option[String], + indexType: String, + whitelistFieldConfig: WhitelistIndexFieldConfiguration) { + + private val LOGGER = LoggerFactory.getLogger(classOf[IndexTemplateHandler]) + + private val ES_TEMPLATE_NAME = "spans-index-template" + private val mapper = new ObjectMapper() + + def run() { + applyTemplate match { + case Some(template) => updateESTemplate(template) + case _ => /* may be the template is set from outside the app */ + } + + whitelistFieldConfig.addOnChangeListener(() => { + LOGGER.info("applying the new elastic template as whitelist fields have changed from query perspective like enableRangeQuery") + readTemplate() match { + case Some(template) => updateESTemplate(template) + case _ => + } + }) + } + + private def esDataType(`type`: IndexFieldType): String = { + `type` match { + case IndexFieldType.int => "integer" + case IndexFieldType.string => "keyword" + case _ => `type`.toString + } + } + + private def updateESTemplate(templateJson: String): Unit = { + val esTemplate: util.HashMap[String, Object] = mapper.readValue(templateJson, new TypeReference[util.HashMap[String, Object]]() {}) + val mappings = esTemplate.get("mappings").asInstanceOf[util.HashMap[String, Object]] + val propertyMap = + mappings.get(indexType).asInstanceOf[util.HashMap[String, Object]] + .get("properties").asInstanceOf[util.HashMap[String, Object]] + .get(indexType).asInstanceOf[util.HashMap[String, Object]] + .get("properties").asInstanceOf[util.HashMap[String, Object]] + + whitelistFieldConfig.whitelistIndexFields.foreach(wf => { + val prop = propertyMap.get(wf.name) + if (prop != null) { + if (wf.enabled && wf.enableRangeQuery) { + propertyMap.put(wf.name, Map("type" -> esDataType(wf.`type`), "doc_values" -> true, "norms" -> false).asJava) + } else { + prop.asInstanceOf[util.HashMap[String, Object]].put("doc_values", Boolean.box(wf.enableRangeQuery)) + } + } + }) + + val newTemplateJson = mapper.writeValueAsString(esTemplate) + + LOGGER.info(s"setting the template with name $ES_TEMPLATE_NAME - $newTemplateJson") + + val putTemplateRequest = new PutTemplate.Builder(ES_TEMPLATE_NAME, newTemplateJson).build() + val result = client.execute(putTemplateRequest) + if (!result.isSucceeded) { + throw new RuntimeException(s"Fail to apply the following template to elastic search with reason=${result.getErrorMessage}") + } + } + + private def readTemplate(): Option[String] = { + val request = new GetTemplate.Builder(ES_TEMPLATE_NAME).build() + val result = client.execute(request) + if (result.isSucceeded) { + Some(result.getJsonObject.get(ES_TEMPLATE_NAME).toString) + } else { + LOGGER.error(s"Fail to read the template with name $ES_TEMPLATE_NAME for reason ${result.getErrorMessage}") + None + } + } +} diff --git a/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/writers/es/ServiceMetadataDocumentGenerator.scala b/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/writers/es/ServiceMetadataDocumentGenerator.scala new file mode 100644 index 000000000..f0e88269c --- /dev/null +++ b/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/writers/es/ServiceMetadataDocumentGenerator.scala @@ -0,0 +1,84 @@ +/* + * Copyright 2018 Expedia, Group. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trace.indexer.writers.es + +import java.time.Instant + +import com.expedia.open.tracing.Span +import com.expedia.www.haystack.commons.metrics.MetricsSupport +import com.expedia.www.haystack.trace.commons.clients.es.document.ServiceMetadataDoc +import com.expedia.www.haystack.trace.commons.utils.SpanUtils +import com.expedia.www.haystack.trace.indexer.config.entities.ServiceMetadataWriteConfiguration +import org.apache.commons.lang3.StringUtils + +import scala.collection.mutable + +class ServiceMetadataDocumentGenerator(config: ServiceMetadataWriteConfiguration) extends MetricsSupport { + + private var serviceMetadataMap = new mutable.HashMap[String, mutable.Set[String]]() + private var allOperationCount: Int = 0 + private var lastFlushInstant = Instant.MIN + + private def shouldFlush: Boolean = { + config.flushIntervalInSec == 0 || Instant.now().minusSeconds(config.flushIntervalInSec).isAfter(lastFlushInstant) + } + + private def areStatementsReadyToBeExecuted(): Seq[ServiceMetadataDoc] = { + if (serviceMetadataMap.nonEmpty && (shouldFlush || allOperationCount > config.flushOnMaxOperationCount)) { + val statements = serviceMetadataMap.flatMap { + case (serviceName, operationList) => + createServiceMetadataDoc(serviceName, operationList) + } + + lastFlushInstant = Instant.now() + serviceMetadataMap = new mutable.HashMap[String, mutable.Set[String]]() + allOperationCount = 0 + statements.toSeq + } else { + Nil + } + } + + /** + * get the list of unique service metadata documents contained in the list of spans + * + * @param spans : list of service metadata + * @return + */ + def getAndUpdateServiceMetadata(spans: Iterable[Span]): Seq[ServiceMetadataDoc] = { + this.synchronized { + spans.foreach(span => { + if (StringUtils.isNotEmpty(span.getServiceName) && StringUtils.isNotEmpty(span.getOperationName)) { + val operationsList = serviceMetadataMap.getOrElseUpdate(span.getServiceName, mutable.Set[String]()) + if (operationsList.add(span.getOperationName)) { + allOperationCount += 1 + } + } + }) + areStatementsReadyToBeExecuted() + } + } + + /** + * @return index document that can be put in elastic search + */ + def createServiceMetadataDoc(serviceName: String, operationList: mutable.Set[String]): List[ServiceMetadataDoc] = { + operationList.map(operationName => ServiceMetadataDoc(serviceName, operationName)).toList + + } +} diff --git a/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/writers/es/ServiceMetadataWriter.scala b/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/writers/es/ServiceMetadataWriter.scala new file mode 100644 index 000000000..df28e7217 --- /dev/null +++ b/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/writers/es/ServiceMetadataWriter.scala @@ -0,0 +1,167 @@ +/* + * Copyright 2019 Expedia, Group. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trace.indexer.writers.es + +import java.util.concurrent.{Semaphore, TimeUnit} + +import com.expedia.open.tracing.buffer.SpanBuffer +import com.expedia.www.haystack.commons.metrics.MetricsSupport +import com.expedia.www.haystack.commons.retries.RetryOperation.withRetryBackoff +import com.expedia.www.haystack.trace.commons.clients.es.AWSSigningJestClientFactory +import com.expedia.www.haystack.trace.commons.clients.es.document.ServiceMetadataDoc +import com.expedia.www.haystack.trace.commons.config.entities.AWSRequestSigningConfiguration +import com.expedia.www.haystack.trace.commons.packer.PackedMessage +import com.expedia.www.haystack.trace.indexer.config.entities.ServiceMetadataWriteConfiguration +import com.expedia.www.haystack.trace.indexer.metrics.AppMetricNames +import com.expedia.www.haystack.trace.indexer.writers.TraceWriter +import io.searchbox.client.config.HttpClientConfig +import io.searchbox.client.{JestClient, JestClientFactory} +import io.searchbox.core.{Bulk, Index} +import io.searchbox.indices.template.PutTemplate +import io.searchbox.params.Parameters +import org.joda.time.format.DateTimeFormat +import org.joda.time.{DateTime, DateTimeZone} +import org.slf4j.LoggerFactory + +import scala.collection.JavaConverters._ +import scala.util.Try + +object ServiceMetadataUtils { + + // creates an index name based on current date. following example illustrates the naming convention of + // elastic search indices for service metadata: + // service-metadata-2019-02-20 + def indexName(prefix: String): String = { + val eventTime = new DateTime(DateTimeZone.UTC) + val dataFormatter = DateTimeFormat.forPattern("yyyy-MM-dd") + s"$prefix-${dataFormatter.print(eventTime)}" + } +} + +class ServiceMetadataWriter(config: ServiceMetadataWriteConfiguration, awsRequestSigningConfig: AWSRequestSigningConfiguration) + extends TraceWriter with MetricsSupport { + + private val LOGGER = LoggerFactory.getLogger(classOf[ServiceMetadataWriter]) + + // a timer that measures the amount of time it takes to complete one bulk write + private val writeTimer = metricRegistry.timer(AppMetricNames.METADATA_WRITE_TIME) + + // meter that measures the write failures + private val failureMeter = metricRegistry.meter(AppMetricNames.METADATA_WRITE_FAILURE) + + // converts a serviceMetadata object into an indexable document + private val documentGenerator = new ServiceMetadataDocumentGenerator(config) + + + // this semaphore controls the parallel writes to service metadata index + private val inflightRequestsSemaphore = new Semaphore(config.maxInFlightBulkRequests, true) + + // initialize the elastic search client + private val esClient: JestClient = { + LOGGER.info("Initializing the http elastic search client with endpoint={}", config.esEndpoint) + + val factory = { + if (awsRequestSigningConfig.enabled) { + LOGGER.info("using AWSSigningJestClientFactory for es client") + new AWSSigningJestClientFactory(awsRequestSigningConfig) + } else { + LOGGER.info("using JestClientFactory for es client") + new JestClientFactory() + } + } + val builder = new HttpClientConfig.Builder(config.esEndpoint) + .multiThreaded(true) + .maxConnectionIdleTime(config.flushIntervalInSec + 10, TimeUnit.SECONDS) + .connTimeout(config.connectionTimeoutMillis) + .readTimeout(config.readTimeoutMillis) + + if (config.username.isDefined && config.password.isDefined) { + builder.defaultCredentials(config.username.get, config.password.get) + } + + factory.setHttpClientConfig(builder.build()) + factory.getObject + } + + private val bulkBuilder = new ThreadSafeBulkBuilder(config.maxDocsInBulk, config.maxBulkDocSizeInBytes) + + if (config.indexTemplateJson.isDefined) applyTemplate(config.indexTemplateJson.get) + + override def close(): Unit = { + LOGGER.info("Closing the elastic search client now.") + Try(esClient.shutdownClient()) + } + + /** + * converts the spans to an index document and writes to elastic search. Also if the parallel writes + * exceed the max inflight requests, then we block and this puts backpressure on upstream + * + * @param traceId trace id + * @param packedSpanBuffer list of spans belonging to this traceId - packed bytes of span buffer + * @param isLastSpanBuffer tells if this is the last record, so the writer can flush + * @return + */ + override def writeAsync(traceId: String, packedSpanBuffer: PackedMessage[SpanBuffer], isLastSpanBuffer: Boolean): Unit = { + var isSemaphoreAcquired = false + val idxDocument: Seq[ServiceMetadataDoc] = documentGenerator.getAndUpdateServiceMetadata(packedSpanBuffer.protoObj.getChildSpansList.asScala) + idxDocument.foreach(document => { + try { + addIndexOperation(traceId, document, ServiceMetadataUtils.indexName(config.indexName)) match { + case Some(bulkToDispatch) => + inflightRequestsSemaphore.acquire() + isSemaphoreAcquired = true + + // execute the request async with retry + withRetryBackoff(retryCallback => { + esClient.executeAsync(bulkToDispatch, new ElasticSearchResultHandler(writeTimer.time(), failureMeter, retryCallback)) + }, + config.retryConfig, + onSuccess = (_: Any) => inflightRequestsSemaphore.release(), + onFailure = ex => { + inflightRequestsSemaphore.release() + LOGGER.error("Fail to write to ES after {} retry attempts", config.retryConfig.maxRetries, ex) + }) + case _ => + } + } catch { + case ex: Exception => + if (isSemaphoreAcquired) inflightRequestsSemaphore.release() + failureMeter.mark() + LOGGER.error("Failed to write spans to elastic search with exception", ex) + } + }) + } + + private def addIndexOperation(traceId: String, document: ServiceMetadataDoc, indexName: String): Option[Bulk] = { // add all the service operation combinations in one bulk + val action: Index = new Index.Builder(document.json) + .index(indexName) + .`type`(config.indexType) + .setParameter(Parameters.CONSISTENCY, config.consistencyLevel) + .id(s"${document.servicename}_${document.operationname}") + .build() + bulkBuilder.addAction(action, document.json.getBytes("utf-8").length, forceBulkCreate = false) + } + + private def applyTemplate(templateJson: String) { + val putTemplateRequest = new PutTemplate.Builder("service-metadata-template", templateJson).build() + val result = esClient.execute(putTemplateRequest) + if (!result.isSucceeded) { + throw new RuntimeException(s"Fail to apply the following template to elastic search with reason=${result.getErrorMessage}") + } + } +} diff --git a/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/writers/es/ThreadSafeBulkBuilder.scala b/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/writers/es/ThreadSafeBulkBuilder.scala new file mode 100644 index 000000000..c8460bfd8 --- /dev/null +++ b/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/writers/es/ThreadSafeBulkBuilder.scala @@ -0,0 +1,79 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trace.indexer.writers.es + +import java.util + +import io.searchbox.action.BulkableAction +import io.searchbox.core.{Bulk, DocumentResult} + +/** + * this is a thread safe builder to build index actions + */ +class ThreadSafeBulkBuilder(maxDocuments: Int, maxDocSizeInBytes: Int) { + private var bulkActions = new util.LinkedList[BulkableAction[DocumentResult]] + private var docsCount = 0 + private var totalSizeInBytes = 0 + + /** + * add the action in the bulk builder, returns bulk if any of the following condition is true + * a) the total doc count in bulk is more than allowed setting + * b) total size of the docs in bulk is more than allowed setting + * c) force create the bulk + * + * @param action index action + * @param sizeInBytes total size of the json in the index action + * @param forceBulkCreate force to build the existing bulk + * @return + */ + def addAction(action: BulkableAction[DocumentResult], + sizeInBytes: Int, + forceBulkCreate: Boolean): Option[Bulk] = { + var dispatchActions: util.LinkedList[BulkableAction[DocumentResult]] = null + + this.synchronized { + bulkActions.add(action) + docsCount += 1 + totalSizeInBytes += sizeInBytes + + if (forceBulkCreate || + docsCount >= maxDocuments || + totalSizeInBytes >= maxDocSizeInBytes) { + dispatchActions = getAndResetBulkActions() + } + } + + if (dispatchActions == null) { + None + } else { + Some(new Bulk.Builder().addAction(dispatchActions).build()) + } + } + + private def getAndResetBulkActions(): util.LinkedList[BulkableAction[DocumentResult]] = { + val dispatchActions = bulkActions + bulkActions = new util.LinkedList[BulkableAction[DocumentResult]] + docsCount = 0 + totalSizeInBytes = 0 + dispatchActions + } + + def getDocsCount: Int = docsCount + + def getTotalSizeInBytes: Int = totalSizeInBytes +} diff --git a/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/writers/grpc/GrpcTraceWriter.scala b/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/writers/grpc/GrpcTraceWriter.scala new file mode 100644 index 000000000..965ff2675 --- /dev/null +++ b/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/writers/grpc/GrpcTraceWriter.scala @@ -0,0 +1,98 @@ +package com.expedia.www.haystack.trace.indexer.writers.grpc + +/* + * Copyright 2018 Expedia, Group. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + + +import java.util.concurrent.Semaphore + +import com.expedia.open.tracing.backend.{StorageBackendGrpc, TraceRecord, WriteSpansRequest} +import com.expedia.open.tracing.buffer.SpanBuffer +import com.expedia.www.haystack.commons.metrics.MetricsSupport +import com.expedia.www.haystack.trace.commons.packer.PackedMessage +import com.expedia.www.haystack.trace.indexer.config.entities.TraceBackendConfiguration +import com.expedia.www.haystack.trace.indexer.metrics.AppMetricNames +import com.expedia.www.haystack.trace.indexer.writers.TraceWriter +import com.google.protobuf.ByteString +import io.grpc.ManagedChannelBuilder +import org.slf4j.LoggerFactory + +import scala.concurrent.ExecutionContextExecutor +import scala.util.Try + +class GrpcTraceWriter(config: TraceBackendConfiguration)(implicit val dispatcher: ExecutionContextExecutor) + extends TraceWriter with MetricsSupport { + + private val LOGGER = LoggerFactory.getLogger(classOf[GrpcTraceWriter]) + private val writeTimer = metricRegistry.timer(AppMetricNames.BACKEND_WRITE_TIME) + private val writeFailures = metricRegistry.meter(AppMetricNames.BACKEND_WRITE_FAILURE) + + private val channel = { + val grpcConfig = config.clientConfig.backends.head + ManagedChannelBuilder.forAddress(grpcConfig.host, grpcConfig.port) + .usePlaintext(true) + .build() + } + private val client = StorageBackendGrpc.newStub(channel) + + // this semaphore controls the parallel writes to trace-backend + private val inflightRequestsSemaphore = new Semaphore(config.maxInFlightRequests, true) + + private def execute(traceId: String, packedSpanBuffer: PackedMessage[SpanBuffer]): Unit = { + val timer = writeTimer.time() + val singleRecord = TraceRecord + .newBuilder() + .setTraceId(traceId) + .setTimestamp(System.currentTimeMillis()) + .setSpans(ByteString.copyFrom(packedSpanBuffer.packedDataBytes)) + val writeSpansRequest = WriteSpansRequest.newBuilder().addRecords(singleRecord).build() + + // execute the request async with retry + client.writeSpans(writeSpansRequest, new WriteSpansResponseObserver(timer, inflightRequestsSemaphore)) + } + + /** + * writes the traceId and its spans to trace-backend. Use the current timestamp as the sort key for the writes to same + * TraceId. Also if the parallel writes exceed the max inflight requests, then we block and this puts backpressure on + * upstream + * + * @param traceId : trace id + * @param packedSpanBuffer : list of spans belonging to this traceId - span buffer + * @param isLastSpanBuffer tells if this is the last record, so the writer can flush`` + * @return + */ + override def writeAsync(traceId: String, packedSpanBuffer: PackedMessage[SpanBuffer], isLastSpanBuffer: Boolean): Unit = { + var isSemaphoreAcquired = false + + try { + inflightRequestsSemaphore.acquire() + isSemaphoreAcquired = true + /* write spanBuffer for a given traceId */ + execute(traceId, packedSpanBuffer) + } catch { + case ex: Exception => + LOGGER.error("Fail to write the spans to trace-backend with exception", ex) + writeFailures.mark() + if (isSemaphoreAcquired) inflightRequestsSemaphore.release() + } + } + + override def close(): Unit = { + LOGGER.info("Closing backend client now..") + Try(channel.shutdown()) + } +} diff --git a/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/writers/grpc/WriteSpansResponseObserver.scala b/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/writers/grpc/WriteSpansResponseObserver.scala new file mode 100644 index 000000000..44035258f --- /dev/null +++ b/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/writers/grpc/WriteSpansResponseObserver.scala @@ -0,0 +1,55 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trace.indexer.writers.grpc + +import java.util.concurrent.Semaphore + +import com.codahale.metrics.Timer +import com.expedia.open.tracing.backend.WriteSpansResponse +import com.expedia.www.haystack.commons.metrics.MetricsSupport +import com.expedia.www.haystack.trace.indexer.metrics.AppMetricNames +import io.grpc.stub.StreamObserver +import org.slf4j.LoggerFactory + + +class WriteSpansResponseObserver(timer:Timer.Context, inflightRequest: Semaphore) extends StreamObserver[WriteSpansResponse] with MetricsSupport { + + private val LOGGER = LoggerFactory.getLogger(classOf[WriteSpansResponseObserver]) + private val writeFailures = metricRegistry.meter(AppMetricNames.BACKEND_WRITE_FAILURE) + + /** + * this is invoked when the grpc aysnc write completes. + * We measure the time write operation takes and records any warnings or errors + */ + + override def onNext(writeSpanResponse: WriteSpansResponse): Unit = { + timer.close() + inflightRequest.release() + } + + override def onError(error: Throwable): Unit = { + timer.close() + inflightRequest.release() + writeFailures.mark() + LOGGER.error(s"Fail to write to trace-backend with exception ", error) + } + + override def onCompleted(): Unit = { + LOGGER.debug(s"Closing WriteSpans Trace Observer") + } +} diff --git a/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/writers/kafka/KafkaWriter.scala b/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/writers/kafka/KafkaWriter.scala new file mode 100644 index 000000000..fa8b0e8fe --- /dev/null +++ b/traces/indexer/src/main/scala/com/expedia/www/haystack/trace/indexer/writers/kafka/KafkaWriter.scala @@ -0,0 +1,53 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trace.indexer.writers.kafka + +import java.util.Properties + +import com.codahale.metrics.Meter +import com.expedia.open.tracing.buffer.SpanBuffer +import com.expedia.www.haystack.commons.metrics.MetricsSupport +import com.expedia.www.haystack.trace.commons.packer.PackedMessage +import com.expedia.www.haystack.trace.indexer.metrics.AppMetricNames +import com.expedia.www.haystack.trace.indexer.writers.TraceWriter +import org.apache.kafka.clients.producer._ +import org.slf4j.LoggerFactory + +import scala.util.Try + +object KafkaWriter extends MetricsSupport { + protected val kafkaProducerFailures: Meter = metricRegistry.meter(AppMetricNames.KAFKA_PRODUCE_FAILURES) +} + +class KafkaWriter(producerConfig: Properties, topic: String) extends TraceWriter { + private val LOGGER = LoggerFactory.getLogger(classOf[KafkaWriter]) + + private val producer = new KafkaProducer[String, Array[Byte]](producerConfig) + + override def writeAsync(traceId: String, packedSpanBuffer: PackedMessage[SpanBuffer], isLastSpanBuffer: Boolean): Unit = { + val record = new ProducerRecord[String, Array[Byte]](topic, traceId, packedSpanBuffer.packedDataBytes) + producer.send(record, (_: RecordMetadata, exception: Exception) => { + if (exception != null) { + LOGGER.error(s"Fail to write the span buffer record to kafka topic=$topic", exception) + KafkaWriter.kafkaProducerFailures.mark() + } + }) + } + + override def close(): Unit = Try(producer.close()) +} diff --git a/traces/indexer/src/test/resources/config/base.conf b/traces/indexer/src/test/resources/config/base.conf new file mode 100644 index 000000000..6d1619145 --- /dev/null +++ b/traces/indexer/src/test/resources/config/base.conf @@ -0,0 +1,155 @@ +health.status.path = "/app/isHealthy" + +span.accumulate { + store { + min.traces.per.cache = 1000 # this defines the minimum traces in each cache before eviction check is applied. This is also useful for testing the code + all.max.entries = 20000 # this is the maximum number of spans that can live across all the stores + } + window.ms = 10000 + poll.ms = 2000 + packer = snappy +} + +kafka { + close.stream.timeout.ms = 30000 + + topic.consume = "spans" + topic.produce = "span-buffer" + num.stream.threads = 2 + + max.wakeups = 5 + wakeup.timeout.ms = 5000 + + commit.offset { + retries = 3 + backoff.ms = 200 + } + + # consumer specific configurations + consumer { + group.id = "haystack-trace-indexer" + bootstrap.servers = "kafkasvc:9092" + auto.offset.reset = "latest" + + # disable auto commit as the app manages offset itself + enable.auto.commit = "false" + } + +# producer specific configurations + producer { + bootstrap.servers = "kafkasvc:9092" + } +} + + +backend { + + client { + host = "localhost" + port = 8090 + max.message.size = 52428800 # 50MB in bytes + } + # defines the max inflight writes for backend client + max.inflight.requests = 100 +} + +service.metadata { + enabled = true + flush { + interval.sec = 60 + operation.count = 10000 + } + es { + endpoint = "http://elasticsearch:9200" + conn.timeout.ms = 10000 + read.timeout.ms = 5000 + consistency.level = "one" + index { + # apply the template before starting the client, if json is empty, no operation is performed + template.json = "some_template_json" + name = "service-metadata" + type = "metadata" + } + # defines settings for bulk operation like max inflight bulks, number of documents and the total size in a single bulk + bulk.max { + docs { + count = 100 + size.kb = 1000 + } + inflight = 10 + } + retries { + max = 10 + backoff { + initial.ms = 100 + factor = 2 + } + } + } +} + +elasticsearch { + endpoint = "http://elasticsearch:9200" + max.inflight.requests = 50 + conn.timeout.ms = 10000 + read.timeout.ms = 5000 + max.connections.per.route = 10 + consistency.level = "one" + index { + template { + json = "some_template_json" + } + + name.prefix = "haystack-traces" + hour.bucket = 6 + type = "spans" + } + # defines settings for bulk operation like max inflight bulks, number of documents and the total size in a single bulk + bulk.max { + docs { + count = 100 + size.kb = 1000 + } + inflight = 10 + } + + retries { + max = 10 + backoff { + initial.ms = 1000 + factor = 2 + } + } + + # if enabled flag is true, es requests will be signed + signing.request.aws { + enabled = false + region = "us-west-2" + service.name = "es" + # if 'access.key' is not provided, will use DefaultAWSCredentialsProviderChain to resolve creds + access.key = "" + secret.key = "" + } +} + +reload { + tables { + index.fields.config = "whitelist-index-fields" + } + config { + endpoint = "http://elasticsearch:9200" + database.name = "reload-configs" + } + interval.ms = 600 + startup.load = false + + # if enabled flag is true, es requests will be signed + signing.request.aws { + enabled = false + region = "us-west-2" + service.name = "es" + # if 'access.key' is not provided, will use DefaultAWSCredentialsProviderChain to resolve creds + access.key = "" + secret.key = "" + } +} \ No newline at end of file diff --git a/traces/indexer/src/test/resources/logback-test.xml b/traces/indexer/src/test/resources/logback-test.xml new file mode 100644 index 000000000..adfa02c68 --- /dev/null +++ b/traces/indexer/src/test/resources/logback-test.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/traces/indexer/src/test/scala/com/expedia/www/haystack/trace/indexer/integration/BaseIntegrationTestSpec.scala b/traces/indexer/src/test/scala/com/expedia/www/haystack/trace/indexer/integration/BaseIntegrationTestSpec.scala new file mode 100644 index 000000000..51ef660e2 --- /dev/null +++ b/traces/indexer/src/test/scala/com/expedia/www/haystack/trace/indexer/integration/BaseIntegrationTestSpec.scala @@ -0,0 +1,327 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trace.indexer.integration + +import java.util.UUID +import java.util.concurrent.{Executors, ScheduledExecutorService, ScheduledFuture, TimeUnit} + +import com.expedia.open.tracing.Tag.TagType +import com.expedia.open.tracing.buffer.SpanBuffer +import com.expedia.open.tracing.{Log, Span, Tag} +import com.expedia.www.haystack.trace.commons.packer.{PackerType, Unpacker} +import com.expedia.www.haystack.trace.indexer.config.entities.SpanAccumulatorConfiguration +import com.expedia.www.haystack.trace.indexer.integration.clients.{ElasticSearchTestClient, GrpcTestClient, KafkaTestClient} +import org.apache.kafka.streams.KeyValue +import org.apache.kafka.streams.integration.utils.IntegrationTestUtils +import org.scalatest._ + +import scala.collection.JavaConverters._ +import scala.concurrent.duration.FiniteDuration + +case class TraceDescription(traceId: String, spanIdPrefix: String) + +abstract class BaseIntegrationTestSpec extends WordSpec with GivenWhenThen with Matchers with BeforeAndAfterAll with BeforeAndAfterEach { + protected val MAX_WAIT_FOR_OUTPUT_MS = 12000 + + protected val spanAccumulatorConfig = SpanAccumulatorConfiguration( + minTracesPerCache = 100, + maxEntriesAllStores = 500, + pollIntervalMillis = 2000L, + bufferingWindowMillis = 6000L, + PackerType.SNAPPY) + + protected var scheduler: ScheduledExecutorService = _ + + val kafka = new KafkaTestClient + val traceBackendClient = new GrpcTestClient + val elastic = new ElasticSearchTestClient + + override def beforeAll() { + scheduler = Executors.newSingleThreadScheduledExecutor() + kafka.prepare(getClass.getSimpleName) + traceBackendClient.prepare() + elastic.prepare() + } + + override def afterAll(): Unit = if (scheduler != null) scheduler.shutdownNow() + + protected def validateChildSpans(spanBuffer: SpanBuffer, + traceId: String, + spanIdPrefix: String, + childSpanCount: Int): Unit = { + spanBuffer.getTraceId shouldBe traceId + + withClue(s"the trace-id $traceId has lesser spans than expected"){ + spanBuffer.getChildSpansCount shouldBe childSpanCount + } + + (0 until spanBuffer.getChildSpansCount).toList foreach { idx => + spanBuffer.getChildSpans(idx).getSpanId shouldBe s"$spanIdPrefix-$idx" + spanBuffer.getChildSpans(idx).getTraceId shouldBe spanBuffer.getTraceId + spanBuffer.getChildSpans(idx).getServiceName shouldBe s"service$idx" + spanBuffer.getChildSpans(idx).getParentSpanId should not be null + spanBuffer.getChildSpans(idx).getOperationName shouldBe s"op$idx" + } + } + + private def randomSpan(traceId: String, spanId: String, serviceName: String, operationName: String): Span = { + Span.newBuilder() + .setTraceId(traceId) + .setParentSpanId(UUID.randomUUID().toString) + .setSpanId(spanId) + .setServiceName(serviceName) + .setOperationName(operationName) + .setStartTime(System.currentTimeMillis() * 1000) + .addTags(Tag.newBuilder().setKey("errorCode").setType(TagType.LONG).setVLong(404)) + .addTags(Tag.newBuilder().setKey("_role").setType(TagType.STRING).setVStr("haystack")) + .addLogs(Log.newBuilder().addFields(Tag.newBuilder().setKey("exceptiontype").setType(TagType.STRING).setVStr("external").build()).build()) + .build() + } + + protected def produceSpansAsync(maxSpansPerTrace: Int, + produceInterval: FiniteDuration, + traceDescription: List[TraceDescription], + startRecordTimestamp: Long, + maxRecordTimestamp: Long, + startSpanIdxFrom: Int = 0): ScheduledFuture[_] = { + var timestamp = startRecordTimestamp + var cnt = 0 + scheduler.scheduleWithFixedDelay(() => { + if (cnt < maxSpansPerTrace) { + val spans = traceDescription.map(sd => { + new KeyValue[String, Span](sd.traceId, randomSpan(sd.traceId, s"${sd.spanIdPrefix}-${startSpanIdxFrom + cnt}", s"service${startSpanIdxFrom + cnt}", s"op${startSpanIdxFrom + cnt}")) + }).asJava + IntegrationTestUtils.produceKeyValuesSynchronouslyWithTimestamp( + kafka.INPUT_TOPIC, + spans, + kafka.TEST_PRODUCER_CONFIG, + timestamp) + timestamp = timestamp + (maxRecordTimestamp / (maxSpansPerTrace - 1)) + } + cnt = cnt + 1 + }, 0, produceInterval.toMillis, TimeUnit.MILLISECONDS) + } + + def verifyBackendWrites(traceDescriptions: Seq[TraceDescription], minSpansPerTrace: Int, maxSpansPerTrace: Int): Unit = { + val traceRecords = traceBackendClient.queryTraces(traceDescriptions) + + traceRecords should have size traceDescriptions.size + + traceRecords.foreach(record => { + val spanBuffer = Unpacker.readSpanBuffer(record.getSpans.toByteArray) + val descr = traceDescriptions.find(_.traceId == record.getTraceId).get + record.getSpans should not be null + spanBuffer.getChildSpansCount should be >= minSpansPerTrace + spanBuffer.getChildSpansCount should be <= maxSpansPerTrace + + spanBuffer.getChildSpansList.asScala.zipWithIndex foreach { + case (sp, idx) => + sp.getSpanId shouldBe s"${descr.spanIdPrefix}-$idx" + sp.getServiceName shouldBe s"service$idx" + sp.getOperationName shouldBe s"op$idx" + } + }) + } + + def verifyOperationNames(): Unit = { + val operationNamesQuery = + """{ + | "query" : { + | "term" : { + | "servicename" : { + | "value" : "service0", + | "boost" : 1.0 + | } + | } + | }, + | "_source" : { + | "includes" : [ + | "operationname" + | ], + | "excludes" : [ + | "servicename" + | ] + | } + |}""".stripMargin + val docs = elastic.queryServiceMetadataIndex(operationNamesQuery) + docs.size shouldBe 1 + + } + + def verifyElasticSearchWrites(traceIds: Seq[String]): Unit = { + val matchAllQuery = + """{ + | "query": { + | "match_all": {} + | } + |}""".stripMargin + + var docs = elastic.querySpansIndex(matchAllQuery) + docs.size shouldBe traceIds.size + docs.indices.foreach { idx => + val traceId = docs.apply(idx).traceid + traceIds should contain(traceId) + } + + val spanSpecificQuery = + """ + |{ + | "query": { + | "bool": { + | "must": [ + | { + | "nested": { + | "path": "spans", + | "query": { + | "bool": { + | "must": [ + | { + | "match": { + | "spans.servicename": "service0" + | } + | }, + | { + | "match": { + | "spans.operationname": "op0" + | } + | } + | ] + | } + | } + | } + | } + | ] + |}}} + """.stripMargin + docs = elastic.querySpansIndex(spanSpecificQuery) + docs.size shouldBe traceIds.size + + val emptyResponseQuery = + """ + |{ + | "query": { + | "bool": { + | "must": [ + | { + | "nested": { + | "path": "spans", + | "query": { + | "bool": { + | "must": [ + | { + | "match": { + | "spans.servicename": "service0" + | } + | }, + | { + | "match": { + | "spans.operationname": "op1" + | } + | } + | ] + | } + | } + | } + | } + | ] + |}}} + """.stripMargin + docs = elastic.querySpansIndex(emptyResponseQuery) + docs.size shouldBe 0 + + val tagQuery = + """ + |{ + | "query": { + | "bool": { + | "must": [ + | { + | "nested": { + | "path": "spans", + | "query": { + | "bool": { + | "must": [ + | { + | "match": { + | "spans.servicename": "service2" + | } + | }, + | { + | "match": { + | "spans.operationname": "op2" + | } + | }, + | { + | "match": { + | "spans.errorcode": "404" + | } + | } + | ] + | } + | } + | } + | } + | ] + |}}} + """.stripMargin + docs = elastic.querySpansIndex(tagQuery) + docs.size shouldBe traceIds.size + docs.map(_.traceid) should contain theSameElementsAs traceIds + + + val roleTagQuery = + """ + |{ + | "query": { + | "bool": { + | "must": [ + | { + | "nested": { + | "path": "spans", + | "query": { + | "bool": { + | "must": [ + | { + | "match": { + | "spans.servicename": "service2" + | } + | }, + | { + | "match": { + | "spans.operationname": "op2" + | } + | }, + | { + | "match": { + | "spans.role": "haystack" + | } + | } + | ] + | } + | } + | } + | } + | ] + |}}} + """.stripMargin + docs = elastic.querySpansIndex(roleTagQuery) + docs.size shouldBe traceIds.size + docs.map(_.traceid) should contain theSameElementsAs traceIds + + } +} diff --git a/traces/indexer/src/test/scala/com/expedia/www/haystack/trace/indexer/integration/EvictedSpanBufferSpec.scala b/traces/indexer/src/test/scala/com/expedia/www/haystack/trace/indexer/integration/EvictedSpanBufferSpec.scala new file mode 100644 index 000000000..8596d25f2 --- /dev/null +++ b/traces/indexer/src/test/scala/com/expedia/www/haystack/trace/indexer/integration/EvictedSpanBufferSpec.scala @@ -0,0 +1,52 @@ +package com.expedia.www.haystack.trace.indexer.integration + +import java.util + +import com.expedia.open.tracing.buffer.SpanBuffer +import com.expedia.www.haystack.trace.indexer.StreamRunner +import org.apache.kafka.streams.KeyValue +import org.apache.kafka.streams.integration.utils.IntegrationTestUtils + +import scala.collection.JavaConverters._ +import scala.concurrent.duration._ + +class EvictedSpanBufferSpec extends BaseIntegrationTestSpec { + private val MAX_CHILD_SPANS = 5 + private val TRACE_ID_1 = "traceid-1" + private val TRACE_ID_2 = "traceid-2" + private val SPAN_ID_PREFIX = "span-id-" + + "Trace Indexing Topology" should { + s"consume spans from input '${kafka.INPUT_TOPIC}', buffer them together for a given traceId and write to trace-backend and elastic on eviction" in { + Given("a set of spans produced async with extremely extremely small store size configuration") + val kafkaConfig = kafka.buildConfig + val esConfig = elastic.buildConfig + val indexTagsConfig = elastic.indexingConfig + val backendConfig = traceBackendClient.buildConfig + val serviceMetadataConfig = elastic.buildServiceMetadataConfig + val accumulatorConfig = spanAccumulatorConfig.copy(minTracesPerCache = 1, maxEntriesAllStores = 1) + + produceSpansAsync(MAX_CHILD_SPANS, + produceInterval = 1.seconds, + List(TraceDescription(TRACE_ID_1, SPAN_ID_PREFIX), TraceDescription(TRACE_ID_2, SPAN_ID_PREFIX)), + 0L, accumulatorConfig.bufferingWindowMillis) + + When(s"kafka-streams topology is started") + val topology = new StreamRunner(kafkaConfig, accumulatorConfig, esConfig, backendConfig, serviceMetadataConfig, indexTagsConfig) + topology.start() + + Then(s"we should get multiple span-buffers bearing only 1 span due to early eviction from store") + val records: util.List[KeyValue[String, SpanBuffer]] = + IntegrationTestUtils.waitUntilMinKeyValueRecordsReceived(kafka.RESULT_CONSUMER_CONFIG, kafka.OUTPUT_TOPIC, 10, MAX_WAIT_FOR_OUTPUT_MS) + + validateKafkaOutput(records.asScala) + topology.close() + } + } + + // validate the kafka output + private def validateKafkaOutput(records: Seq[KeyValue[String, SpanBuffer]]): Unit = { + records.map(_.key).toSet should contain allOf (TRACE_ID_1, TRACE_ID_2) + records.foreach(rec => rec.value.getChildSpansCount shouldBe 1) + } +} diff --git a/traces/indexer/src/test/scala/com/expedia/www/haystack/trace/indexer/integration/FailedTopologyRecoverySpec.scala b/traces/indexer/src/test/scala/com/expedia/www/haystack/trace/indexer/integration/FailedTopologyRecoverySpec.scala new file mode 100644 index 000000000..fc07d76b7 --- /dev/null +++ b/traces/indexer/src/test/scala/com/expedia/www/haystack/trace/indexer/integration/FailedTopologyRecoverySpec.scala @@ -0,0 +1,97 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trace.indexer.integration + +import java.util + +import com.expedia.open.tracing.buffer.SpanBuffer +import com.expedia.www.haystack.trace.indexer.StreamRunner +import org.apache.kafka.streams.KeyValue +import org.apache.kafka.streams.integration.utils.IntegrationTestUtils + +import scala.collection.JavaConverters._ +import scala.concurrent.duration._ + +class FailedTopologyRecoverySpec extends BaseIntegrationTestSpec { + private val MAX_CHILD_SPANS_PER_TRACE = 5 + private val TRACE_ID_3 = "traceid-3" + private val SPAN_ID_PREFIX = "span-id-" + private val TRACE_DESCRIPTIONS = List(TraceDescription(TRACE_ID_3, SPAN_ID_PREFIX)) + + "Trace Indexing Topology" should { + s"consume spans from input '${kafka.INPUT_TOPIC}', buffer them together keyed by unique TraceId and write to trace-backend and elastic even if crashed in between" in { + Given("a set of spans produced async with spanBuffer+kafka configurations") + val kafkaConfig = kafka.buildConfig + val esConfig = elastic.buildConfig + val indexTagsConfig = elastic.indexingConfig + val backendConfig = traceBackendClient.buildConfig + val serviceMetadataConfig = elastic.buildServiceMetadataConfig + val accumulatorConfig = spanAccumulatorConfig.copy(pollIntervalMillis = spanAccumulatorConfig.pollIntervalMillis * 5) + val startTimestamp = System.currentTimeMillis() + produceSpansAsync( + MAX_CHILD_SPANS_PER_TRACE, + produceInterval = 1.seconds, + TRACE_DESCRIPTIONS, + startTimestamp, + spanAccumulatorConfig.bufferingWindowMillis) + + When(s"kafka-streams topology is started and then stopped forcefully after few sec") + var topology = new StreamRunner(kafkaConfig, accumulatorConfig, esConfig, backendConfig, serviceMetadataConfig, indexTagsConfig) + topology.start() + Thread.sleep(7000) + topology.close() + + // wait for few sec to close the stream threads + Thread.sleep(6000) + + Then(s"on restart of the topology, we should be able to read complete trace created in previous run from the '${kafka.OUTPUT_TOPIC}' topic in kafka, trace-backend and elasticsearch") + topology = new StreamRunner(kafkaConfig, accumulatorConfig, esConfig, backendConfig, serviceMetadataConfig, indexTagsConfig) + topology.start() + + // produce one more span record with same traceId to trigger punctuate + produceSpansAsync( + 1, + produceInterval = 1.seconds, + TRACE_DESCRIPTIONS, + startTimestamp + spanAccumulatorConfig.bufferingWindowMillis, + spanAccumulatorConfig.bufferingWindowMillis, + startSpanIdxFrom = MAX_CHILD_SPANS_PER_TRACE) + + try { + val records: util.List[KeyValue[String, SpanBuffer]] = + IntegrationTestUtils.waitUntilMinKeyValueRecordsReceived(kafka.RESULT_CONSUMER_CONFIG, kafka.OUTPUT_TOPIC, 1, MAX_WAIT_FOR_OUTPUT_MS) + + // wait for the elastic search writes to pass through, i guess refresh time has to be adjusted + Thread.sleep(5000) + validateKafkaOutput(records.asScala, MAX_CHILD_SPANS_PER_TRACE) + verifyBackendWrites(TRACE_DESCRIPTIONS, MAX_CHILD_SPANS_PER_TRACE, MAX_CHILD_SPANS_PER_TRACE + 1) // 1 extra record for trigger + verifyElasticSearchWrites(Seq(TRACE_ID_3)) + } finally { + topology.close() + } + } + } + + // validate the kafka output + private def validateKafkaOutput(records: Iterable[KeyValue[String, SpanBuffer]], minChildSpanCount: Int) = { + // expect only one span buffer + records.size shouldBe 1 + records.head.key shouldBe TRACE_ID_3 + records.head.value.getChildSpansCount should be >=minChildSpanCount + } +} diff --git a/traces/indexer/src/test/scala/com/expedia/www/haystack/trace/indexer/integration/MultipleTraceIndexingTopologySpec.scala b/traces/indexer/src/test/scala/com/expedia/www/haystack/trace/indexer/integration/MultipleTraceIndexingTopologySpec.scala new file mode 100644 index 000000000..a97166466 --- /dev/null +++ b/traces/indexer/src/test/scala/com/expedia/www/haystack/trace/indexer/integration/MultipleTraceIndexingTopologySpec.scala @@ -0,0 +1,88 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trace.indexer.integration + +import java.util + +import com.expedia.open.tracing.buffer.SpanBuffer +import com.expedia.www.haystack.trace.indexer.StreamRunner +import org.apache.kafka.streams.KeyValue +import org.apache.kafka.streams.integration.utils.IntegrationTestUtils + +import scala.collection.JavaConverters._ +import scala.concurrent.duration._ + +class MultipleTraceIndexingTopologySpec extends BaseIntegrationTestSpec { + private val MAX_CHILD_SPANS_PER_TRACE = 5 + private val TRACE_ID_9 = "traceid-9" + private val TRACE_ID_5 = "traceid-5" + private val SPAN_ID_PREFIX_1 = TRACE_ID_9 + "span-id-" + private val SPAN_ID_PREFIX_2 = TRACE_ID_5 + "span-id-" + + "Trace Indexing Topology" should { + s"consume spans from input '${kafka.INPUT_TOPIC}' and buffer them together for every unique traceId and write to trace-backend and elastic search" in { + Given("a set of spans with two different traceIds and project configurations") + val kafkaConfig = kafka.buildConfig + val esConfig = elastic.buildConfig + val indexTagsConfig = elastic.indexingConfig + val backendConfig = traceBackendClient.buildConfig + val serviceMetadataConfig = elastic.buildServiceMetadataConfig + + When(s"spans are produced in '${kafka.INPUT_TOPIC}' topic async, and kafka-streams topology is started") + val traceDescriptions = List(TraceDescription(TRACE_ID_5, SPAN_ID_PREFIX_2),TraceDescription(TRACE_ID_9, SPAN_ID_PREFIX_1)) + + produceSpansAsync(MAX_CHILD_SPANS_PER_TRACE, + 1.seconds, + traceDescriptions, + startRecordTimestamp = 0, + maxRecordTimestamp = spanAccumulatorConfig.bufferingWindowMillis) + + val topology = new StreamRunner(kafkaConfig, spanAccumulatorConfig, esConfig, backendConfig, serviceMetadataConfig, indexTagsConfig) + topology.start() + + Then(s"we should read two span buffers with different traceIds from '${kafka.OUTPUT_TOPIC}' topic and same should be read from trace-backend and elastic search") + try { + val result: util.List[KeyValue[String, SpanBuffer]] = + IntegrationTestUtils.waitUntilMinKeyValueRecordsReceived(kafka.RESULT_CONSUMER_CONFIG, kafka.OUTPUT_TOPIC, 2, MAX_WAIT_FOR_OUTPUT_MS) + + validateKafkaOutput(result.asScala,MAX_CHILD_SPANS_PER_TRACE) + + Thread.sleep(6000) + verifyBackendWrites(traceDescriptions, MAX_CHILD_SPANS_PER_TRACE, MAX_CHILD_SPANS_PER_TRACE) + verifyElasticSearchWrites(Seq(TRACE_ID_9, TRACE_ID_5)) + } finally { + topology.close() + } + } + } + + // validate the kafka output + private def validateKafkaOutput(records: Iterable[KeyValue[String, SpanBuffer]], childSpanCount: Int): Unit = { + records.size shouldBe 2 + + // both traceIds should be present as different span buffer objects + records.map(_.key) should contain allOf (TRACE_ID_9, TRACE_ID_5) + + records.foreach(record => { + record.key match { + case TRACE_ID_9 => validateChildSpans(record.value, TRACE_ID_9, SPAN_ID_PREFIX_1, childSpanCount) + case TRACE_ID_5 => validateChildSpans(record.value, TRACE_ID_5, SPAN_ID_PREFIX_2, childSpanCount) + } + }) + } +} diff --git a/traces/indexer/src/test/scala/com/expedia/www/haystack/trace/indexer/integration/PartialTraceIndexingTopologySpec.scala b/traces/indexer/src/test/scala/com/expedia/www/haystack/trace/indexer/integration/PartialTraceIndexingTopologySpec.scala new file mode 100644 index 000000000..ca484104c --- /dev/null +++ b/traces/indexer/src/test/scala/com/expedia/www/haystack/trace/indexer/integration/PartialTraceIndexingTopologySpec.scala @@ -0,0 +1,101 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trace.indexer.integration + +import java.util + +import com.expedia.open.tracing.buffer.SpanBuffer +import com.expedia.www.haystack.trace.indexer.StreamRunner +import org.apache.kafka.streams.KeyValue +import org.apache.kafka.streams.integration.utils.IntegrationTestUtils + +import scala.collection.JavaConverters._ +import scala.concurrent.duration._ + +class PartialTraceIndexingTopologySpec extends BaseIntegrationTestSpec { + private val MAX_CHILD_SPANS_PER_TRACE = 5 + private val TRACE_ID = "unique-trace-id" + + "Trace Indexing Topology" should { + s"consume spans from '${kafka.INPUT_TOPIC}' topic, buffer them together for every unique traceId and write to trace-backend and elastic search" in { + Given("a set of spans with all configurations") + val SPAN_ID_PREFIX = "span-id" + val kafkaConfig = kafka.buildConfig + val esConfig = elastic.buildConfig + val indexTagsConfig = elastic.indexingConfig + val backendConfig = traceBackendClient.buildConfig + val serviceMetadataConfig = elastic.buildServiceMetadataConfig + val traceDescription = List(TraceDescription(TRACE_ID, SPAN_ID_PREFIX)) + + When(s"spans are produced in '${kafka.INPUT_TOPIC}' topic async, and kafka-streams topology is started") + produceSpansAsync( + MAX_CHILD_SPANS_PER_TRACE, + 1.second, + traceDescription, + 0L, + spanAccumulatorConfig.bufferingWindowMillis) + + val topology = new StreamRunner(kafkaConfig, spanAccumulatorConfig, esConfig, backendConfig, serviceMetadataConfig, indexTagsConfig) + topology.start() + + Then(s"we should read one span buffer object from '${kafka.OUTPUT_TOPIC}' topic and the same should be searchable in trace-backend and elastic search") + try { + val result: util.List[KeyValue[String, SpanBuffer]] = + IntegrationTestUtils.waitUntilMinKeyValueRecordsReceived(kafka.RESULT_CONSUMER_CONFIG, kafka.OUTPUT_TOPIC, 1, MAX_WAIT_FOR_OUTPUT_MS) + validateKafkaOutput(result.asScala, MAX_CHILD_SPANS_PER_TRACE, SPAN_ID_PREFIX) + + // give a sleep to let elastic search results become searchable + Thread.sleep(6000) + verifyBackendWrites(traceDescription, MAX_CHILD_SPANS_PER_TRACE, MAX_CHILD_SPANS_PER_TRACE) + verifyElasticSearchWrites(Seq(TRACE_ID)) + + repeatTestWithNewerSpanIds() + } finally { + topology.close() + } + } + } + + // this test is useful to check if we are not emitting the old spans if the same traceId reappears later + private def repeatTestWithNewerSpanIds(): Unit = { + Given(s"a set of new span ids and same traceId '$TRACE_ID'") + val SPAN_ID_2_PREFIX = "span-id-2" + When(s"these spans are produced in '${kafka.INPUT_TOPIC}' topic on the currently running topology") + produceSpansAsync( + MAX_CHILD_SPANS_PER_TRACE, + 1.seconds, + List(TraceDescription(TRACE_ID, SPAN_ID_2_PREFIX)), + spanAccumulatorConfig.bufferingWindowMillis + 100L, + spanAccumulatorConfig.bufferingWindowMillis) + + Then(s"we should read see newer spans in the buffered object from '${kafka.OUTPUT_TOPIC}' topic") + val result: util.List[KeyValue[String, SpanBuffer]] = + IntegrationTestUtils.waitUntilMinKeyValueRecordsReceived(kafka.RESULT_CONSUMER_CONFIG, kafka.OUTPUT_TOPIC, 1, MAX_WAIT_FOR_OUTPUT_MS) + + validateKafkaOutput(result.asScala, MAX_CHILD_SPANS_PER_TRACE, SPAN_ID_2_PREFIX) + } + + // validate the kafka output + private def validateKafkaOutput(records: Iterable[KeyValue[String, SpanBuffer]], + childSpanCount: Int, + spanIdPrefix: String): Unit = { + // expect only one span buffer object + records.size shouldBe 1 + validateChildSpans(records.head.value, TRACE_ID, spanIdPrefix, MAX_CHILD_SPANS_PER_TRACE) + } +} \ No newline at end of file diff --git a/traces/indexer/src/test/scala/com/expedia/www/haystack/trace/indexer/integration/ServiceMetadataIndexingTopologySpec.scala b/traces/indexer/src/test/scala/com/expedia/www/haystack/trace/indexer/integration/ServiceMetadataIndexingTopologySpec.scala new file mode 100644 index 000000000..5e7ba6939 --- /dev/null +++ b/traces/indexer/src/test/scala/com/expedia/www/haystack/trace/indexer/integration/ServiceMetadataIndexingTopologySpec.scala @@ -0,0 +1,68 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trace.indexer.integration + +import java.util + +import com.expedia.open.tracing.buffer.SpanBuffer +import com.expedia.www.haystack.trace.indexer.StreamRunner +import org.apache.kafka.streams.KeyValue +import org.apache.kafka.streams.integration.utils.IntegrationTestUtils + +import scala.concurrent.duration._ + +class ServiceMetadataIndexingTopologySpec extends BaseIntegrationTestSpec { + private val MAX_CHILD_SPANS_PER_TRACE = 5 + private val TRACE_ID_6 = "traceid-6" + private val TRACE_ID_7 = "traceid-7" + private val SPAN_ID_PREFIX_1 = TRACE_ID_6 + "span-id-" + private val SPAN_ID_PREFIX_2 = TRACE_ID_7 + "span-id-" + + "Trace Indexing Topology" should { + s"consume spans from input '${kafka.INPUT_TOPIC}' and buffer them together for every service operation combination and write to elastic search elastic" in { + Given("a set of spans with different serviceNames and a project configurations") + val kafkaConfig = kafka.buildConfig + val esConfig = elastic.buildConfig + val indexTagsConfig = elastic.indexingConfig + val backendConfig = traceBackendClient.buildConfig + val serviceMetadataConfig = elastic.buildServiceMetadataConfig + + When(s"spans are produced in '${kafka.INPUT_TOPIC}' topic async, and kafka-streams topology is started") + val traceDescriptions = List(TraceDescription(TRACE_ID_6, SPAN_ID_PREFIX_1), TraceDescription(TRACE_ID_7, SPAN_ID_PREFIX_2)) + + produceSpansAsync(MAX_CHILD_SPANS_PER_TRACE, + 1.seconds, + traceDescriptions, + 0, + spanAccumulatorConfig.bufferingWindowMillis) + + val topology = new StreamRunner(kafkaConfig, spanAccumulatorConfig, esConfig, backendConfig, serviceMetadataConfig, indexTagsConfig) + topology.start() + + Then(s"we should read two multiple service operation combinations in elastic search") + try { + val result: util.List[KeyValue[String, SpanBuffer]] = + IntegrationTestUtils.waitUntilMinKeyValueRecordsReceived(kafka.RESULT_CONSUMER_CONFIG, kafka.OUTPUT_TOPIC, 2, MAX_WAIT_FOR_OUTPUT_MS) + Thread.sleep(6000) + verifyOperationNames() + } finally { + topology.close() + } + } + } +} diff --git a/traces/indexer/src/test/scala/com/expedia/www/haystack/trace/indexer/integration/clients/ElasticSearchTestClient.scala b/traces/indexer/src/test/scala/com/expedia/www/haystack/trace/indexer/integration/clients/ElasticSearchTestClient.scala new file mode 100644 index 000000000..5d554087a --- /dev/null +++ b/traces/indexer/src/test/scala/com/expedia/www/haystack/trace/indexer/integration/clients/ElasticSearchTestClient.scala @@ -0,0 +1,261 @@ +/* + * Copyright 2019, Expedia Group. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trace.indexer.integration.clients + +import java.text.SimpleDateFormat +import java.util.Date + +import com.expedia.www.haystack.commons.retries.RetryOperation +import com.expedia.www.haystack.trace.commons.config.entities._ +import com.expedia.www.haystack.trace.indexer.config.entities.{ElasticSearchConfiguration, ServiceMetadataWriteConfiguration} +import io.searchbox.client.config.HttpClientConfig +import io.searchbox.client.{JestClient, JestClientFactory} +import io.searchbox.core.Search +import io.searchbox.indices.DeleteIndex +import org.json4s.ext.EnumNameSerializer +import org.json4s.jackson.Serialization +import org.json4s.{DefaultFormats, Formats} + +case class EsSourceDocument(traceid: String) + +class ElasticSearchTestClient { + protected implicit val formats: Formats = DefaultFormats + new EnumNameSerializer(IndexFieldType) + + private val ELASTIC_SEARCH_ENDPOINT = "http://elasticsearch:9200" + private val SPANS_INDEX_NAME_PREFIX = "haystack-traces" + private val SPANS_INDEX_TYPE = "spans" + private val SPANS_INDEX_HOUR_BUCKET = 6 + + private val HAYSTACK_TRACES_INDEX = { + val formatter = new SimpleDateFormat("yyyy-MM-dd") + s"$SPANS_INDEX_NAME_PREFIX-${formatter.format(new Date())}" + } + + private val esClient: JestClient = { + val factory = new JestClientFactory() + factory.setHttpClientConfig(new HttpClientConfig.Builder(ELASTIC_SEARCH_ENDPOINT).build()) + factory.getObject + } + + def prepare(): Unit = { + // drop the haystack-traces- index + 0 until (24 / SPANS_INDEX_HOUR_BUCKET) foreach { + idx => { + esClient.execute(new DeleteIndex.Builder(s"$HAYSTACK_TRACES_INDEX-$idx").build()) + } + } + } + + def buildConfig = ElasticSearchConfiguration( + ELASTIC_SEARCH_ENDPOINT, + None, + None, + Some(INDEX_TEMPLATE), + "one", + SPANS_INDEX_NAME_PREFIX, + SPANS_INDEX_HOUR_BUCKET, + SPANS_INDEX_TYPE, + 3000, + 3000, + 5, + 10, + 10, + 10, + RetryOperation.Config(3, 2000, 2), + getAWSRequestSigningConfiguration) + + def getAWSRequestSigningConfiguration: AWSRequestSigningConfiguration = { + AWSRequestSigningConfiguration(enabled = false, "", "", None, None) + } + + def buildServiceMetadataConfig: ServiceMetadataWriteConfiguration = { + ServiceMetadataWriteConfiguration(enabled = true, + esEndpoint = ELASTIC_SEARCH_ENDPOINT, + username = None, + password = None, + consistencyLevel = "one", + indexTemplateJson = Some(SERVICE_METADATA_INDEX_TEMPLATE), + indexName = "service-metadata", + indexType = "metadata", + connectionTimeoutMillis = 3000, + readTimeoutMillis = 3000, + maxInFlightBulkRequests = 10, + maxDocsInBulk = 5, + maxBulkDocSizeInBytes = 50, + flushIntervalInSec = 10, + flushOnMaxOperationCount = 10, + retryConfig = RetryOperation.Config(10, 250, 2)) + } + + + def indexingConfig: WhitelistIndexFieldConfiguration = { + val cfg = WhitelistIndexFieldConfiguration() + val cfgJsonData = Serialization.write(WhiteListIndexFields( + List(WhitelistIndexField(name = "role", `type` = IndexFieldType.string, aliases = Set("_role")), WhitelistIndexField(name = "errorcode", `type` = IndexFieldType.long)))) + cfg.onReload(cfgJsonData) + cfg + } + + def querySpansIndex(query: String): List[EsSourceDocument] = { + import scala.collection.JavaConverters._ + val searchQuery = new Search.Builder(query) + .addIndex(SPANS_INDEX_NAME_PREFIX) + .addType(SPANS_INDEX_TYPE) + .build() + val result = esClient.execute(searchQuery) + if (result.getSourceAsStringList != null && result.getSourceAsStringList.size() > 0) { + result.getSourceAsStringList.asScala.map(Serialization.read[EsSourceDocument]).toList + } + else { + Nil + } + } + + def queryServiceMetadataIndex(query: String): List[String] = { + import scala.collection.JavaConverters._ + val SERVICE_METADATA_INDEX_NAME = "service-metadata" + val SERVICE_METADATA_INDEX_TYPE = "metadata" + val searchQuery = new Search.Builder(query) + .addIndex(SERVICE_METADATA_INDEX_NAME) + .addType(SERVICE_METADATA_INDEX_TYPE) + .build() + val result = esClient.execute(searchQuery) + if (result.getSourceAsStringList != null && result.getSourceAsStringList.size() > 0) { + result.getSourceAsStringList.asScala.toList + } + else { + Nil + } + } + + private val INDEX_TEMPLATE = + """{ + | "template": "haystack-traces*", + | "settings": { + | "number_of_shards": 1, + | "index.mapping.ignore_malformed": true, + | "analysis": { + | "normalizer": { + | "lowercase_normalizer": { + | "type": "custom", + | "filter": ["lowercase"] + | } + | } + | } + | }, + | "aliases": { + | "haystack-traces": {} + | }, + | "mappings": { + | "spans": { + | "_field_names": { + | "enabled": false + | }, + | "_all": { + | "enabled": false + | }, + | "_source": { + | "includes": ["traceid"] + | }, + | "properties": { + | "traceid": { + | "enabled": false + | }, + | "starttime": { + | "type": "long", + | "doc_values": true + | }, + | "spans": { + | "type": "nested", + | "properties": { + | "starttime": { + | "type": "long", + | "doc_values": true + | } + | } + | } + | }, + | "dynamic_templates": [{ + | "strings_as_keywords_1": { + | "match_mapping_type": "string", + | "mapping": { + | "type": "keyword", + | "normalizer": "lowercase_normalizer", + | "doc_values": false, + | "norms": false + | } + | } + | }, { + | "longs_disable_doc_norms": { + | "match_mapping_type": "long", + | "mapping": { + | "type": "long", + | "doc_values": false, + | "norms": false + | } + | } + | }] + | } + | } + |} + |""".stripMargin + + private val SERVICE_METADATA_INDEX_TEMPLATE = + """{ + | "template": "service-metadata*", + | "aliases": { + | "service-metadata": {} + | }, + | "settings": { + | "number_of_shards": 4, + | "index.mapping.ignore_malformed": true, + | "analysis": { + | "normalizer": { + | "lowercase_normalizer": { + | "type": "custom", + | "filter": [ + | "lowercase" + | ] + | } + | } + | } + | }, + | "mappings": { + | "metadata": { + | "_field_names": { + | "enabled": false + | }, + | "_all": { + | "enabled": false + | }, + | "properties": { + | "servicename": { + | "type": "keyword", + | "norms": false + | }, + | "operationname": { + | "type": "keyword", + | "doc_values": false, + | "norms": false + | } + | } + | } + | } + |} + |""".stripMargin +} diff --git a/traces/indexer/src/test/scala/com/expedia/www/haystack/trace/indexer/integration/clients/GrpcTestClient.scala b/traces/indexer/src/test/scala/com/expedia/www/haystack/trace/indexer/integration/clients/GrpcTestClient.scala new file mode 100644 index 000000000..342450061 --- /dev/null +++ b/traces/indexer/src/test/scala/com/expedia/www/haystack/trace/indexer/integration/clients/GrpcTestClient.scala @@ -0,0 +1,66 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trace.indexer.integration.clients + +import java.util.concurrent.Executors + +import com.expedia.open.tracing.backend.{ReadSpansRequest, StorageBackendGrpc, TraceRecord} +import com.expedia.www.haystack.trace.commons.config.entities.{GrpcClientConfig, TraceStoreBackends} +import com.expedia.www.haystack.trace.indexer.config.entities.TraceBackendConfiguration +import com.expedia.www.haystack.trace.indexer.integration.TraceDescription +import com.expedia.www.haystack.trace.storage.backends.memory.Service +import io.grpc.ManagedChannelBuilder + +import scala.collection.JavaConverters._ + +class GrpcTestClient { + + + var storageBackendClient: StorageBackendGrpc.StorageBackendBlockingStub = _ + + import GrpcTestClient._ + + + def prepare(): Unit = { + storageBackendClient = StorageBackendGrpc.newBlockingStub(ManagedChannelBuilder.forAddress("localhost", port) + .usePlaintext(true) + .build()) + } + + + def buildConfig = TraceBackendConfiguration( + TraceStoreBackends(Seq(GrpcClientConfig("localhost", port))), 10) + + def queryTraces(traceDescriptions: Seq[TraceDescription]): Seq[TraceRecord] = { + val traceIds = traceDescriptions.map(traceDescription => traceDescription.traceId).toList + storageBackendClient.readSpans(ReadSpansRequest.newBuilder().addAllTraceIds(traceIds.asJava).build()).getRecordsList.asScala + } + +} + +object GrpcTestClient { + + val port = 8090 + + private val executors = Executors.newSingleThreadExecutor() + executors.submit(new Runnable { + override def run(): Unit = Service.main(Array { + port.toString + }) + }) +} diff --git a/traces/indexer/src/test/scala/com/expedia/www/haystack/trace/indexer/integration/clients/KafkaTestClient.scala b/traces/indexer/src/test/scala/com/expedia/www/haystack/trace/indexer/integration/clients/KafkaTestClient.scala new file mode 100644 index 000000000..5f20ba7d4 --- /dev/null +++ b/traces/indexer/src/test/scala/com/expedia/www/haystack/trace/indexer/integration/clients/KafkaTestClient.scala @@ -0,0 +1,95 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trace.indexer.integration.clients + +import java.util.Properties + +import com.expedia.www.haystack.trace.indexer.config.entities.KafkaConfiguration +import com.expedia.www.haystack.trace.indexer.integration.serdes.{SnappyCompressedSpanBufferProtoDeserializer, SpanProtoSerializer} +import com.expedia.www.haystack.trace.indexer.serde.SpanDeserializer +import org.apache.kafka.clients.consumer.ConsumerConfig +import org.apache.kafka.clients.producer.ProducerConfig +import org.apache.kafka.common.serialization.{ByteArraySerializer, StringDeserializer, StringSerializer} +import org.apache.kafka.streams.integration.utils.EmbeddedKafkaCluster + +object KafkaTestClient { + val KAFKA_CLUSTER = new EmbeddedKafkaCluster(1) + KAFKA_CLUSTER.start() +} + +class KafkaTestClient { + import KafkaTestClient._ + + val INPUT_TOPIC = "spans" + val OUTPUT_TOPIC = "span-buffer" + + val APP_PRODUCER_CONFIG: Properties = { + val props = new Properties() + props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, KAFKA_CLUSTER.bootstrapServers) + props.put(ProducerConfig.ACKS_CONFIG, "1") + props.put(ProducerConfig.BATCH_SIZE_CONFIG, "20") + props.put(ProducerConfig.RETRIES_CONFIG, "0") + props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, classOf[StringSerializer]) + props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, classOf[ByteArraySerializer]) + props + } + + val APP_CONSUMER_CONFIG: Properties = new Properties() + + val TEST_PRODUCER_CONFIG: Properties = { + val props = new Properties() + props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, KAFKA_CLUSTER.bootstrapServers) + props.put(ProducerConfig.ACKS_CONFIG, "1") + props.put(ProducerConfig.BATCH_SIZE_CONFIG, "20") + props.put(ProducerConfig.RETRIES_CONFIG, "0") + props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, classOf[StringSerializer]) + props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, classOf[SpanProtoSerializer]) + props + } + + val RESULT_CONSUMER_CONFIG = new Properties() + + def buildConfig = KafkaConfiguration(numStreamThreads = 1, + pollTimeoutMs = 100, + APP_CONSUMER_CONFIG, APP_PRODUCER_CONFIG, OUTPUT_TOPIC, INPUT_TOPIC, + consumerCloseTimeoutInMillis = 3000, + commitOffsetRetries = 3, + commitBackoffInMillis = 250, + maxWakeups = 5, wakeupTimeoutInMillis = 3000) + + def prepare(appId: String): Unit = { + APP_CONSUMER_CONFIG.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, KAFKA_CLUSTER.bootstrapServers) + APP_CONSUMER_CONFIG.put(ConsumerConfig.GROUP_ID_CONFIG, appId + "-app-consumer") + APP_CONSUMER_CONFIG.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest") + APP_CONSUMER_CONFIG.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, classOf[StringDeserializer]) + APP_CONSUMER_CONFIG.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, classOf[SpanDeserializer]) + APP_CONSUMER_CONFIG.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "false") + + RESULT_CONSUMER_CONFIG.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, KAFKA_CLUSTER.bootstrapServers) + RESULT_CONSUMER_CONFIG.put(ConsumerConfig.GROUP_ID_CONFIG, appId + "-result-consumer") + RESULT_CONSUMER_CONFIG.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest") + RESULT_CONSUMER_CONFIG.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, classOf[StringDeserializer]) + RESULT_CONSUMER_CONFIG.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, classOf[SnappyCompressedSpanBufferProtoDeserializer]) + + deleteTopics(INPUT_TOPIC, OUTPUT_TOPIC) + KAFKA_CLUSTER.createTopic(INPUT_TOPIC, 2, 1) + KAFKA_CLUSTER.createTopic(OUTPUT_TOPIC) + } + + private def deleteTopics(topics: String*): Unit = KAFKA_CLUSTER.deleteTopicsAndWait(topics:_*) +} diff --git a/traces/indexer/src/test/scala/com/expedia/www/haystack/trace/indexer/integration/serdes/TestSerdes.scala b/traces/indexer/src/test/scala/com/expedia/www/haystack/trace/indexer/integration/serdes/TestSerdes.scala new file mode 100644 index 000000000..91c5244b4 --- /dev/null +++ b/traces/indexer/src/test/scala/com/expedia/www/haystack/trace/indexer/integration/serdes/TestSerdes.scala @@ -0,0 +1,46 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.trace.indexer.integration.serdes + +import java.util + +import com.expedia.open.tracing.Span +import com.expedia.open.tracing.buffer.SpanBuffer +import com.expedia.www.haystack.trace.commons.packer.Unpacker +import org.apache.kafka.common.serialization.{Deserializer, Serializer} + +class SpanProtoSerializer extends Serializer[Span] { + override def configure(configs: util.Map[String, _], isKey: Boolean): Unit = () + override def serialize(topic: String, data: Span): Array[Byte] = { + data.toByteArray + } + override def close(): Unit = () +} + +class SnappyCompressedSpanBufferProtoDeserializer extends Deserializer[SpanBuffer] { + override def configure(configs: util.Map[String, _], isKey: Boolean): Unit = () + + override def deserialize(topic: String, data: Array[Byte]): SpanBuffer = { + if(data == null) { + null + } else { + Unpacker.readSpanBuffer(data) + } + } + + override def close(): Unit = () +} diff --git a/traces/indexer/src/test/scala/com/expedia/www/haystack/trace/indexer/unit/ConfigurationLoaderSpec.scala b/traces/indexer/src/test/scala/com/expedia/www/haystack/trace/indexer/unit/ConfigurationLoaderSpec.scala new file mode 100644 index 000000000..ad56bdc0d --- /dev/null +++ b/traces/indexer/src/test/scala/com/expedia/www/haystack/trace/indexer/unit/ConfigurationLoaderSpec.scala @@ -0,0 +1,118 @@ +/* + * Copyright 2019, Expedia Group. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trace.indexer.unit + +import com.expedia.www.haystack.trace.commons.packer.PackerType +import com.expedia.www.haystack.trace.indexer.config.ProjectConfiguration +import org.apache.kafka.clients.consumer.ConsumerConfig +import org.apache.kafka.clients.producer.ProducerConfig +import org.scalatest.{FunSpec, Matchers} + +class ConfigurationLoaderSpec extends FunSpec with Matchers { + + val project = new ProjectConfiguration() + describe("Configuration loader") { + + it("should load the health status config from base.conf") { + project.healthStatusFilePath shouldEqual "/app/isHealthy" + } + + it("should load the span buffer config only from base.conf") { + val config = project.spanAccumulateConfig + config.pollIntervalMillis shouldBe 2000L + config.maxEntriesAllStores shouldBe 20000 + config.bufferingWindowMillis shouldBe 10000L + config.packerType shouldEqual PackerType.SNAPPY + } + + it("should load the kafka config from base.conf and one stream property from env variable") { + val kafkaConfig = project.kafkaConfig + kafkaConfig.produceTopic shouldBe "span-buffer" + kafkaConfig.consumeTopic shouldBe "spans" + kafkaConfig.numStreamThreads shouldBe 2 + kafkaConfig.commitOffsetRetries shouldBe 3 + kafkaConfig.commitBackoffInMillis shouldBe 200 + + kafkaConfig.maxWakeups shouldBe 5 + kafkaConfig.wakeupTimeoutInMillis shouldBe 5000 + + kafkaConfig.consumerProps.getProperty(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG) shouldBe "kafkasvc:9092" + kafkaConfig.consumerProps.getProperty(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG) shouldBe "earliest" + kafkaConfig.consumerProps.getProperty(ConsumerConfig.GROUP_ID_CONFIG) shouldBe "haystack-trace-indexer" + kafkaConfig.consumerProps.getProperty(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG) shouldBe "false" + kafkaConfig.consumerProps.getProperty(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG) shouldBe "org.apache.kafka.common.serialization.StringDeserializer" + kafkaConfig.consumerProps.getProperty(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG) shouldBe "com.expedia.www.haystack.trace.indexer.serde.SpanDeserializer" + + kafkaConfig.consumerCloseTimeoutInMillis shouldBe 30000 + + kafkaConfig.producerProps.getProperty(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG) shouldBe "kafkasvc:9092" + kafkaConfig.producerProps.getProperty(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG) shouldBe "org.apache.kafka.common.serialization.ByteArraySerializer" + kafkaConfig.producerProps.getProperty(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG) shouldBe "org.apache.kafka.common.serialization.StringSerializer" + } + + it("should load the service metadata config from base.conf") { + val config = project.serviceMetadataWriteConfig + config.flushIntervalInSec shouldBe 60 + config.flushOnMaxOperationCount shouldBe 10000 + config.esEndpoint shouldBe "http://elasticsearch:9200" + config.maxInFlightBulkRequests shouldBe 10 + config.maxDocsInBulk shouldBe 100 + config.maxBulkDocSizeInBytes shouldBe 1000000 + config.indexTemplateJson shouldBe Some("some_template_json") + config.consistencyLevel shouldBe "one" + config.readTimeoutMillis shouldBe 5000 + config.connectionTimeoutMillis shouldBe 10000 + config.indexName shouldBe "service-metadata" + config.indexType shouldBe "metadata" + config.retryConfig.maxRetries shouldBe 10 + config.retryConfig.backOffInMillis shouldBe 100 + config.retryConfig.backoffFactor shouldBe 2 + } + + it("should load the trace backend config from base.conf and few properties overridden from env variable") { + val backendConfiguration = project.backendConfig + + backendConfiguration.maxInFlightRequests shouldBe 100 + } + + it("should load the elastic search config from base.conf and one property overridden from env variable") { + val elastic = project.elasticSearchConfig + elastic.endpoint shouldBe "http://elasticsearch:9200" + elastic.maxInFlightBulkRequests shouldBe 10 + elastic.maxDocsInBulk shouldBe 100 + elastic.maxBulkDocSizeInBytes shouldBe 1000000 + elastic.indexTemplateJson shouldBe Some("some_template_json") + elastic.consistencyLevel shouldBe "one" + elastic.readTimeoutMillis shouldBe 5000 + elastic.connectionTimeoutMillis shouldBe 10000 + elastic.indexNamePrefix shouldBe "haystack-test" + elastic.indexType shouldBe "spans" + elastic.retryConfig.maxRetries shouldBe 10 + elastic.retryConfig.backOffInMillis shouldBe 1000 + elastic.retryConfig.backoffFactor shouldBe 2 + elastic.indexHourBucket shouldBe 6 + elastic.maxConnectionsPerRoute shouldBe 10 + + elastic.awsRequestSigningConfiguration.enabled shouldEqual false + elastic.awsRequestSigningConfiguration.region shouldEqual "us-west-2" + elastic.awsRequestSigningConfiguration.awsServiceName shouldEqual "es" + elastic.awsRequestSigningConfiguration.accessKey shouldBe None + elastic.awsRequestSigningConfiguration.secretKey shouldBe None + } + } +} diff --git a/traces/indexer/src/test/scala/com/expedia/www/haystack/trace/indexer/unit/DynamicCacheSizerSpec.scala b/traces/indexer/src/test/scala/com/expedia/www/haystack/trace/indexer/unit/DynamicCacheSizerSpec.scala new file mode 100644 index 000000000..340a1fb54 --- /dev/null +++ b/traces/indexer/src/test/scala/com/expedia/www/haystack/trace/indexer/unit/DynamicCacheSizerSpec.scala @@ -0,0 +1,74 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trace.indexer.unit + +import com.expedia.www.haystack.trace.indexer.store.traits.CacheSizeObserver +import com.expedia.www.haystack.trace.indexer.store.DynamicCacheSizer +import org.scalatest.easymock.EasyMockSugar +import org.scalatest.{FunSpec, Matchers} + +class DynamicCacheSizerSpec extends FunSpec with Matchers with EasyMockSugar { + private val MAX_CACHE_ENTRIES = 500 + + describe("dynamic cache sizer") { + it("should notify the cache observer with new cache size") { + val sizer = new DynamicCacheSizer(1, MAX_CACHE_ENTRIES) + val observer = mock[CacheSizeObserver] + expecting { + observer.onCacheSizeChange(MAX_CACHE_ENTRIES) + } + whenExecuting(observer) { + sizer.addCacheObserver(observer) + } + } + + it("should notify multiple cache observers with new cache size") { + val sizer = new DynamicCacheSizer(1, MAX_CACHE_ENTRIES) + val observer_1 = mock[CacheSizeObserver] + val observer_2 = mock[CacheSizeObserver] + + expecting { + observer_1.onCacheSizeChange(MAX_CACHE_ENTRIES) + observer_1.onCacheSizeChange(MAX_CACHE_ENTRIES / 2) + observer_2.onCacheSizeChange(MAX_CACHE_ENTRIES / 2) + } + whenExecuting(observer_1, observer_2) { + sizer.addCacheObserver(observer_1) + sizer.addCacheObserver(observer_2) + } + } + + it("should notify existing cache observers when an existing observer is removed with new cache size") { + val sizer = new DynamicCacheSizer(1, MAX_CACHE_ENTRIES) + val observer_1 = mock[CacheSizeObserver] + val observer_2 = mock[CacheSizeObserver] + + expecting { + observer_1.onCacheSizeChange(MAX_CACHE_ENTRIES) + observer_1.onCacheSizeChange(MAX_CACHE_ENTRIES / 2) + observer_2.onCacheSizeChange(MAX_CACHE_ENTRIES / 2) + observer_2.onCacheSizeChange(MAX_CACHE_ENTRIES) + } + whenExecuting(observer_1, observer_2) { + sizer.addCacheObserver(observer_1) + sizer.addCacheObserver(observer_2) + sizer.removeCacheObserver(observer_1) + } + } + } +} diff --git a/traces/indexer/src/test/scala/com/expedia/www/haystack/trace/indexer/unit/ElasticSearchResultHandlerSpec.scala b/traces/indexer/src/test/scala/com/expedia/www/haystack/trace/indexer/unit/ElasticSearchResultHandlerSpec.scala new file mode 100644 index 000000000..5822553f6 --- /dev/null +++ b/traces/indexer/src/test/scala/com/expedia/www/haystack/trace/indexer/unit/ElasticSearchResultHandlerSpec.scala @@ -0,0 +1,101 @@ +package com.expedia.www.haystack.trace.indexer.unit + +import java.util +import java.util.Collections + +import com.codahale.metrics.Timer +import com.expedia.www.haystack.commons.metrics.MetricsSupport +import com.expedia.www.haystack.commons.retries.RetryOperation +import com.expedia.www.haystack.trace.indexer.metrics.AppMetricNames +import com.expedia.www.haystack.trace.indexer.writers.es.ElasticSearchResultHandler +import com.google.gson.Gson +import io.searchbox.core.BulkResult +import org.elasticsearch.common.util.concurrent.EsRejectedExecutionException +import org.scalatest.easymock.EasyMockSugar +import org.scalatest.{FunSpec, Matchers} + +class ElasticSearchResultHandlerSpec extends FunSpec with Matchers with EasyMockSugar with MetricsSupport { + private val esWriteFailureMeter = metricRegistry.meter(AppMetricNames.ES_WRITE_FAILURE) + + describe("Trace Index Result Handler") { + + it("should complete with success if no failures reported") { + val retryCallback = mock[RetryOperation.Callback] + val timer = mock[Timer.Context] + val bulkResult = mock[BulkResult] + + expecting { + retryCallback.onResult(bulkResult) + timer.close() + bulkResult.getFailedItems.andReturn(Collections.emptyList()).anyTimes() + } + + whenExecuting(retryCallback, timer, bulkResult) { + val handler = new ElasticSearchResultHandler(timer, esWriteFailureMeter, retryCallback) + handler.completed(bulkResult) + esWriteFailureMeter.getCount shouldBe 0 + } + } + + it("should complete with success but mark the failures if happen") { + val retryCallback = mock[RetryOperation.Callback] + val timer = mock[Timer.Context] + val bulkResult = mock[BulkResult] + val outer = new BulkResult(new Gson()) + val resultItem = new outer.BulkResultItem("op", "index", "type", "1", 400, + "error", 1, "errorType", "errorReason") + + expecting { + retryCallback.onResult(bulkResult) + timer.close() + bulkResult.getFailedItems.andReturn(util.Arrays.asList(resultItem)).anyTimes() + } + + whenExecuting(retryCallback, timer, bulkResult) { + val handler = new ElasticSearchResultHandler(timer, esWriteFailureMeter, retryCallback) + val initialFailures = esWriteFailureMeter.getCount + handler.completed(bulkResult) + esWriteFailureMeter.getCount - initialFailures shouldBe 1 + } + } + + it("should report failure and mark the number of failures, and perform retry on any exception") { + val retryCallback = mock[RetryOperation.Callback] + val timer = mock[Timer.Context] + val bulkResult = mock[BulkResult] + + val error = new RuntimeException + expecting { + retryCallback.onError(error, retry = true) + timer.close() + } + + whenExecuting(retryCallback, timer, bulkResult) { + val handler = new ElasticSearchResultHandler(timer, esWriteFailureMeter, retryCallback) + val initialFailures = esWriteFailureMeter.getCount + handler.failed(error) + esWriteFailureMeter.getCount - initialFailures shouldBe 1 + } + } + + it("should report failure and mark the number of failures and perform function on elastic search specific exception") { + val retryCallback = mock[RetryOperation.Callback] + val timer = mock[Timer.Context] + val bulkResult = mock[BulkResult] + + val error = new EsRejectedExecutionException("too many requests") + + expecting { + retryCallback.onError(error, retry = true) + timer.close() + } + + whenExecuting(retryCallback, timer, bulkResult) { + val handler = new ElasticSearchResultHandler(timer, esWriteFailureMeter, retryCallback) + val initialFailures = esWriteFailureMeter.getCount + handler.failed(error) + esWriteFailureMeter.getCount - initialFailures shouldBe 1 + } + } + } +} diff --git a/traces/indexer/src/test/scala/com/expedia/www/haystack/trace/indexer/unit/ElasticSearchWriterUtilsSpec.scala b/traces/indexer/src/test/scala/com/expedia/www/haystack/trace/indexer/unit/ElasticSearchWriterUtilsSpec.scala new file mode 100644 index 000000000..b74d9826a --- /dev/null +++ b/traces/indexer/src/test/scala/com/expedia/www/haystack/trace/indexer/unit/ElasticSearchWriterUtilsSpec.scala @@ -0,0 +1,48 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.expedia.www.haystack.trace.indexer.unit + +import com.expedia.www.haystack.trace.indexer.writers.es.ElasticSearchWriterUtils +import org.scalatest.{BeforeAndAfterEach, FunSpec, GivenWhenThen, Matchers} + +class ElasticSearchWriterUtilsSpec extends FunSpec with Matchers with GivenWhenThen with BeforeAndAfterEach { + var timezone: String = _ + + override def beforeEach() { + timezone = System.getProperty("user.timezone") + System.setProperty("user.timezone", "CST") + } + + override def afterEach(): Unit = { + System.setProperty("user.timezone", timezone) + } + + describe("elastic search writer") { + it("should use UTC when generating ES indexes") { + Given("the system timezone is not UTC") + System.setProperty("user.timezone", "CST") + val eventTimeInMicros = System.currentTimeMillis() * 1000 + + When("the writer generates the ES indexes") + val cstName = ElasticSearchWriterUtils.indexName("haystack-traces", 6, eventTimeInMicros) + System.setProperty("user.timezone", "UTC") + val utcName = ElasticSearchWriterUtils.indexName("haystack-traces", 6, eventTimeInMicros) + + Then("it should use UTC to get those indexes") + cstName shouldBe utcName + } + } +} diff --git a/traces/indexer/src/test/scala/com/expedia/www/haystack/trace/indexer/unit/IndexTemplateHandlerSpec.scala b/traces/indexer/src/test/scala/com/expedia/www/haystack/trace/indexer/unit/IndexTemplateHandlerSpec.scala new file mode 100644 index 000000000..b108e54a4 --- /dev/null +++ b/traces/indexer/src/test/scala/com/expedia/www/haystack/trace/indexer/unit/IndexTemplateHandlerSpec.scala @@ -0,0 +1,173 @@ +/* + * Copyright 2019 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trace.indexer.unit + +import com.expedia.www.haystack.trace.commons.config.entities.WhitelistIndexFieldConfiguration +import com.expedia.www.haystack.trace.indexer.writers.es.IndexTemplateHandler +import com.google.gson.{Gson, JsonParser} +import io.searchbox.client.{JestClient, JestResult} +import io.searchbox.indices.template.{GetTemplate, PutTemplate} +import org.easymock.EasyMock +import org.scalatest.easymock.EasyMockSugar +import org.scalatest.{FunSpec, Matchers} + +class IndexTemplateHandlerSpec extends FunSpec with Matchers with EasyMockSugar { + + private val templateJson = + """ + |{ + | "spans-index-template": { + | "order": 0, + | "index_patterns": ["haystack-traces*"], + | "settings": { + | "index": { + | "analysis": { + | "normalizer": { + | "lowercase_normalizer": { + | "filter": ["lowercase"], + | "type": "custom" + | } + | } + | }, + | "number_of_shards": "8", + | "mapping": { + | "ignore_malformed": "true" + | } + | } + | }, + | "mappings": { + | "spans": { + | "_field_names": { + | "enabled": false + | }, + | "_all": { + | "enabled": false + | }, + | "_source": { + | "includes": ["traceid"] + | }, + | "properties": { + | "traceid": { + | "enabled": false + | }, + | "starttime": { + | "type": "long", + | "doc_values": true + | }, + | "spans": { + | "type": "nested", + | "properties": { + | "servicename": { + | "type": "keyword", + | "normalizer": "lowercase_normalizer", + | "doc_values": false, + | "norms": false + | }, + | "operationname": { + | "type": "keyword", + | "normalizer": "lowercase_normalizer", + | "doc_values": false, + | "norms": false + | }, + | "starttime": { + | "enabled": false + | }, + | "duration": { + | "type": "long", + | "doc_values": true + | }, + | "f1": { + | "type": "long", + | "doc_values": true + | } + | } + | } + | }, + | "dynamic_templates": [{ + | "strings_as_keywords_1": { + | "match_mapping_type": "string", + | "mapping": { + | "type": "keyword", + | "normalizer": "lowercase_normalizer", + | "doc_values": false, + | "norms": false + | } + | } + | }, { + | "longs_disable_doc_norms": { + | "match_mapping_type": "long", + | "mapping": { + | "type": "long", + | "doc_values": false, + | "norms": false + | } + | } + | }] + | } + | }, + | "aliases": { + | "haystack-traces": {} + | } + | } + |} + | + """.stripMargin + describe("Index Template Handler") { + it("should read the template and update it") { + val client = mock[JestClient] + val getTemplateResult = new JestResult(new Gson()) + val putTemplateResult = new JestResult(new Gson()) + val config = WhitelistIndexFieldConfiguration() + + val getTemplate = EasyMock.newCapture[GetTemplate]() + val putTemplate = EasyMock.newCapture[PutTemplate]() + + expecting { + getTemplateResult.setSucceeded(true) + putTemplateResult.setSucceeded(true) + getTemplateResult.setJsonObject(new JsonParser().parse(templateJson).getAsJsonObject) + client.execute(EasyMock.capture(getTemplate)).andReturn(getTemplateResult) + client.execute(EasyMock.capture(putTemplate)).andReturn(putTemplateResult) + } + + whenExecuting(client) { + new IndexTemplateHandler(client, None, "spans", config).run() + config.onReload( + """ + |{ + |"fields": [ + | { + | "name": "status_code", + | "type": "int", + | "enableRangeQuery": true + | }, + | { + | "name": "f1", + | "type": "long", + | "enableRangeQuery": false + | } + |]} + """. + stripMargin) + } + + putTemplate.getValue.getData(new Gson()) shouldEqual "{\"settings\":{\"index\":{\"analysis\":{\"normalizer\":{\"lowercase_normalizer\":{\"filter\":[\"lowercase\"],\"type\":\"custom\"}}},\"number_of_shards\":\"8\",\"mapping\":{\"ignore_malformed\":\"true\"}}},\"mappings\":{\"spans\":{\"_field_names\":{\"enabled\":false},\"_all\":{\"enabled\":false},\"_source\":{\"includes\":[\"traceid\"]},\"properties\":{\"traceid\":{\"enabled\":false},\"starttime\":{\"type\":\"long\",\"doc_values\":true},\"spans\":{\"type\":\"nested\",\"properties\":{\"servicename\":{\"type\":\"keyword\",\"normalizer\":\"lowercase_normalizer\",\"doc_values\":false,\"norms\":false},\"operationname\":{\"type\":\"keyword\",\"normalizer\":\"lowercase_normalizer\",\"doc_values\":false,\"norms\":false},\"starttime\":{\"enabled\":false},\"duration\":{\"type\":\"long\",\"doc_values\":true},\"f1\":{\"type\":\"long\",\"doc_values\":false}}}},\"dynamic_templates\":[{\"strings_as_keywords_1\":{\"match_mapping_type\":\"string\",\"mapping\":{\"type\":\"keyword\",\"normalizer\":\"lowercase_normalizer\",\"doc_values\":false,\"norms\":false}}},{\"longs_disable_doc_norms\":{\"match_mapping_type\":\"long\",\"mapping\":{\"type\":\"long\",\"doc_values\":false,\"norms\":false}}}]}},\"aliases\":{\"haystack-traces\":{}},\"index_patterns\":[\"haystack-traces*\"],\"order\":0}" + } + } +} + diff --git a/traces/indexer/src/test/scala/com/expedia/www/haystack/trace/indexer/unit/SpanBufferMemoryStoreSpec.scala b/traces/indexer/src/test/scala/com/expedia/www/haystack/trace/indexer/unit/SpanBufferMemoryStoreSpec.scala new file mode 100644 index 000000000..1c063b586 --- /dev/null +++ b/traces/indexer/src/test/scala/com/expedia/www/haystack/trace/indexer/unit/SpanBufferMemoryStoreSpec.scala @@ -0,0 +1,124 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trace.indexer.unit + +import com.expedia.open.tracing.Span +import com.expedia.www.haystack.trace.indexer.store.DynamicCacheSizer +import com.expedia.www.haystack.trace.indexer.store.impl.SpanBufferMemoryStore +import org.apache.kafka.streams.processor.internals.{ProcessorContextImpl, RecordCollector} +import org.apache.kafka.streams.processor.{StateRestoreCallback, StateStore, TaskId} +import org.easymock.EasyMock._ +import org.scalatest.easymock.EasyMockSugar +import org.scalatest.{FunSpec, Matchers} + +class SpanBufferMemoryStoreSpec extends FunSpec with Matchers with EasyMockSugar { + + private val TRACE_ID_1 = "TraceId_1" + private val TRACE_ID_2 = "TraceId_2" + + describe("SpanBuffer Memory Store") { + it("should create spanBuffer, add child spans and allow retrieving old spanBuffers from the store") { + val (context, rootStateStore, recordCollector, spanBufferStore) = createSpanBufferStore + + whenExecuting(context, recordCollector, rootStateStore) { + + val span1 = Span.newBuilder().setTraceId(TRACE_ID_1).setSpanId("SPAN_ID_1").build() + val span2 = Span.newBuilder().setTraceId(TRACE_ID_1).setSpanId("SPAN_ID_2").build() + + spanBufferStore.addOrUpdateSpanBuffer(TRACE_ID_1, span1, 11000L, 10) + spanBufferStore.addOrUpdateSpanBuffer(TRACE_ID_1, span2, 12000L, 11) + + spanBufferStore.totalSpans shouldBe 2 + + val result = spanBufferStore.getAndRemoveSpanBuffersOlderThan(13000L) + + result.size shouldBe 1 + result.foreach { + spanBufferWithMetadata => + spanBufferWithMetadata.builder.getTraceId shouldBe TRACE_ID_1 + spanBufferWithMetadata.builder.getChildSpansCount shouldBe 2 + spanBufferWithMetadata.builder.getChildSpans(0).getSpanId shouldBe "SPAN_ID_1" + spanBufferWithMetadata.builder.getChildSpans(1).getSpanId shouldBe "SPAN_ID_2" + } + spanBufferStore.totalSpans shouldBe 0 + } + } + + it("should create two spanBuffers for different traceIds, allow retrieving old spanBuffers from the store") { + val (context, rootStateStore, recordCollector, spanBufferStore) = createSpanBufferStore + + whenExecuting(context, recordCollector, rootStateStore) { + val span1 = Span.newBuilder().setTraceId(TRACE_ID_1).setSpanId("SPAN_ID_1").build() + val span2 = Span.newBuilder().setTraceId(TRACE_ID_2).setSpanId("SPAN_ID_2").build() + val span3 = Span.newBuilder().setTraceId(TRACE_ID_2).setSpanId("SPAN_ID_3").build() + + spanBufferStore.addOrUpdateSpanBuffer(TRACE_ID_1, span1, 11000L, 10) + spanBufferStore.addOrUpdateSpanBuffer(TRACE_ID_2, span2, 12000L, 11) + spanBufferStore.addOrUpdateSpanBuffer(TRACE_ID_2, span3, 12500L, 12) + + spanBufferStore.totalSpans shouldBe 3 + + var result = spanBufferStore.getAndRemoveSpanBuffersOlderThan(11500L) + + result.size shouldBe 1 + result.foreach { + spanBufferWithMetadata => + spanBufferWithMetadata.builder.getTraceId shouldBe TRACE_ID_1 + spanBufferWithMetadata.builder.getChildSpansCount shouldBe 1 + spanBufferWithMetadata.builder.getChildSpans(0).getSpanId shouldBe "SPAN_ID_1" + } + + spanBufferStore.totalSpans shouldBe 2 + + result = spanBufferStore.getAndRemoveSpanBuffersOlderThan(11500L) + result.size shouldBe 0 + + result = spanBufferStore.getAndRemoveSpanBuffersOlderThan(13000L) + + result.size shouldBe 1 + result.foreach { + spanBufferWithMetadata => + spanBufferWithMetadata.builder.getTraceId shouldBe TRACE_ID_2 + spanBufferWithMetadata.builder.getChildSpansCount shouldBe 2 + spanBufferWithMetadata.builder.getChildSpans(0).getSpanId shouldBe "SPAN_ID_2" + spanBufferWithMetadata.builder.getChildSpans(1).getSpanId shouldBe "SPAN_ID_3" + } + + spanBufferStore.totalSpans shouldBe 0 + } + } + } + + private def createSpanBufferStore = { + val cacheSizer = new DynamicCacheSizer(10, 1000) + val spanBufferStore = new SpanBufferMemoryStore(cacheSizer) + spanBufferStore.init() + + val context = mock[ProcessorContextImpl] + val rootStateStore = mock[StateStore] + val recordCollector: RecordCollector = mock[RecordCollector] + + expecting { + context.applicationId().andReturn("appId").anyTimes() + context.taskId().andReturn(new TaskId(1, 0)).anyTimes() + context.recordCollector().andReturn(recordCollector).anyTimes() + context.register(anyObject(classOf[StateStore]), anyBoolean(), anyObject(classOf[StateRestoreCallback])).anyTimes() + } + (context, rootStateStore, recordCollector, spanBufferStore) + } +} diff --git a/traces/indexer/src/test/scala/com/expedia/www/haystack/trace/indexer/unit/SpanIndexDocumentGeneratorSpec.scala b/traces/indexer/src/test/scala/com/expedia/www/haystack/trace/indexer/unit/SpanIndexDocumentGeneratorSpec.scala new file mode 100644 index 000000000..721a0405e --- /dev/null +++ b/traces/indexer/src/test/scala/com/expedia/www/haystack/trace/indexer/unit/SpanIndexDocumentGeneratorSpec.scala @@ -0,0 +1,296 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trace.indexer.unit + +import java.util.concurrent.TimeUnit + +import com.expedia.open.tracing.buffer.SpanBuffer +import com.expedia.open.tracing.{Log, Span, Tag} +import com.expedia.www.haystack.trace.commons.config.entities.{IndexFieldType, WhiteListIndexFields, WhitelistIndexField, WhitelistIndexFieldConfiguration} +import com.expedia.www.haystack.trace.indexer.writers.es.IndexDocumentGenerator +import com.google.protobuf.ByteString +import org.json4s.ext.EnumNameSerializer +import org.json4s.jackson.Serialization +import org.json4s.{DefaultFormats, Formats} +import org.scalatest.{FunSpec, Matchers} + +class SpanIndexDocumentGeneratorSpec extends FunSpec with Matchers { + protected implicit val formats: Formats = DefaultFormats + new EnumNameSerializer(IndexFieldType) + + private val TRACE_ID = "trace_id" + private val START_TIME_1 = 1529042838469123l + private val START_TIME_2 = 1529042848469000l + + private val LONG_DURATION = TimeUnit.SECONDS.toMicros(25) + TimeUnit.MICROSECONDS.toMicros(500) + + describe("Span to IndexDocument Generator") { + it ("should extract serviceName, operationName, duration and create json document for indexing") { + val generator = new IndexDocumentGenerator(WhitelistIndexFieldConfiguration()) + + val span_1 = Span.newBuilder().setTraceId(TRACE_ID) + .setSpanId("span-1") + .setServiceName("service1") + .setOperationName("op1") + .setStartTime(START_TIME_1) + .setDuration(610000L) + .build() + val span_2 = Span.newBuilder().setTraceId(TRACE_ID) + .setSpanId("span-2") + .setServiceName("service1") + .setOperationName("op1") + .setStartTime(START_TIME_1) + .setDuration(500000L) + .build() + val span_3 = Span.newBuilder().setTraceId(TRACE_ID) + .setSpanId("span-3") + .setServiceName("service2") + .setDuration(LONG_DURATION) + .setStartTime(START_TIME_2) + .setOperationName("op3").build() + + val spanBuffer = SpanBuffer.newBuilder().addChildSpans(span_1).addChildSpans(span_2).addChildSpans(span_3).setTraceId(TRACE_ID).build() + val doc = generator.createIndexDocument(TRACE_ID, spanBuffer).get + doc.json shouldBe "{\"traceid\":\"trace_id\",\"rootduration\":0,\"starttime\":1529042838000000,\"spans\":[{\"servicename\":\"service1\",\"starttime\":[1529042838000000],\"duration\":[500000,610000],\"operationname\":\"op1\"},{\"servicename\":\"service2\",\"starttime\":[1529042848000000],\"duration\":[25000000],\"operationname\":\"op3\"}]}" + } + + it ("should not create an index document if service name is absent") { + val generator = new IndexDocumentGenerator(WhitelistIndexFieldConfiguration()) + + val span_1 = Span.newBuilder().setTraceId(TRACE_ID) + .setOperationName("op1") + .build() + val span_2 = Span.newBuilder().setTraceId(TRACE_ID) + .setDuration(1000L) + .setOperationName("op2").build() + + val spanBuffer = SpanBuffer.newBuilder().addChildSpans(span_1).addChildSpans(span_2).setTraceId(TRACE_ID).build() + val doc = generator.createIndexDocument(TRACE_ID, spanBuffer) + doc shouldBe None + } + + it ("should extract tags along with serviceName, operationName and duration and create json document for indexing") { + val indexableTags = List( + WhitelistIndexField(name = "role", `type` = IndexFieldType.string), + WhitelistIndexField(name = "errorCode", `type` = IndexFieldType.long)) + + val whitelistConfig = WhitelistIndexFieldConfiguration() + whitelistConfig.onReload(Serialization.write(WhiteListIndexFields(indexableTags))) + val generator = new IndexDocumentGenerator(whitelistConfig) + + val tag_1 = Tag.newBuilder().setKey("role").setType(Tag.TagType.STRING).setVStr("haystack").build() + val tag_2 = Tag.newBuilder().setKey("errorCode").setType(Tag.TagType.LONG).setVLong(3).build() + + val span_1 = Span.newBuilder().setTraceId(TRACE_ID) + .setServiceName("service1") + .setSpanId("span-1") + .setOperationName("op1") + .setDuration(100L) + .setStartTime(START_TIME_1) + .addTags(tag_1) + .build() + val span_2 = Span.newBuilder().setTraceId(TRACE_ID) + .setServiceName("service1") + .setSpanId("span-2") + .setOperationName("op2") + .setDuration(200L) + .setStartTime(START_TIME_2) + .addTags(tag_2) + .addTags(tag_1) + .build() + val span_3 = Span.newBuilder().setTraceId(TRACE_ID) + .setSpanId("span-3") + .setServiceName("service2") + .setDuration(1000L) + .setStartTime(START_TIME_1) + .addTags(tag_2) + .setOperationName("op3").build() + + val spanBuffer = SpanBuffer.newBuilder().addChildSpans(span_1).addChildSpans(span_2).addChildSpans(span_3).setTraceId(TRACE_ID).build() + val doc = generator.createIndexDocument(TRACE_ID, spanBuffer).get + doc.json shouldBe "{\"traceid\":\"trace_id\",\"rootduration\":0,\"starttime\":1529042838000000,\"spans\":[{\"role\":[\"haystack\"],\"servicename\":\"service1\",\"starttime\":[1529042838000000],\"duration\":[100],\"operationname\":\"op1\"},{\"role\":[\"haystack\"],\"servicename\":\"service1\",\"starttime\":[1529042848000000],\"errorcode\":[3],\"duration\":[200],\"operationname\":\"op2\"},{\"servicename\":\"service2\",\"starttime\":[1529042838000000],\"errorcode\":[3],\"duration\":[1000],\"operationname\":\"op3\"}]}" + } + + it ("should respect enabled flag of tags create right json document for indexing") { + val indexableTags = List( + WhitelistIndexField(name = "role", IndexFieldType.string, enabled = false), + WhitelistIndexField(name = "errorCode", `type` = IndexFieldType.long)) + val whitelistConfig = WhitelistIndexFieldConfiguration() + whitelistConfig.onReload(Serialization.write(WhiteListIndexFields(indexableTags))) + val generator = new IndexDocumentGenerator(whitelistConfig) + + val tag_1 = Tag.newBuilder().setKey("role").setType(Tag.TagType.STRING).setVStr("haystack").build() + val tag_2 = Tag.newBuilder().setKey("errorCode").setType(Tag.TagType.LONG).setVLong(3).build() + + val span_1 = Span.newBuilder().setTraceId(TRACE_ID) + .setSpanId("span-1") + .setServiceName("service1") + .setOperationName("op1") + .setDuration(100L) + .setStartTime(START_TIME_1) + .addTags(tag_1) + .build() + val span_2 = Span.newBuilder().setTraceId(TRACE_ID) + .setSpanId("span-2") + .setServiceName("service1") + .setOperationName("op2") + .setDuration(200L) + .setStartTime(START_TIME_2) + .addTags(tag_2) + .build() + + val spanBuffer = SpanBuffer.newBuilder().addChildSpans(span_1).addChildSpans(span_2).setTraceId(TRACE_ID).build() + val doc = generator.createIndexDocument(TRACE_ID, spanBuffer).get + doc.json shouldBe "{\"traceid\":\"trace_id\",\"rootduration\":0,\"starttime\":1529042838000000,\"spans\":[{\"servicename\":\"service1\",\"starttime\":[1529042838000000],\"duration\":[100],\"operationname\":\"op1\"},{\"servicename\":\"service1\",\"starttime\":[1529042848000000],\"errorcode\":[3],\"duration\":[200],\"operationname\":\"op2\"}]}" + } + + it ("one more test to verify the tags are indexed") { + val indexableTags = List( + WhitelistIndexField(name = "errorCode", `type` = IndexFieldType.long)) + val whitelistConfig = WhitelistIndexFieldConfiguration() + whitelistConfig.onReload(Serialization.write(WhiteListIndexFields(indexableTags))) + val generator = new IndexDocumentGenerator(whitelistConfig) + + val tag_1 = Tag.newBuilder().setKey("errorCode").setType(Tag.TagType.LONG).setVLong(5).build() + val tag_2 = Tag.newBuilder().setKey("errorCode").setType(Tag.TagType.LONG).setVLong(3).build() + + val span_1 = Span.newBuilder().setTraceId(TRACE_ID) + .setServiceName("service1") + .setSpanId("span-1") + .setOperationName("op1") + .setDuration(100L) + .addTags(tag_1) + .build() + val span_2 = Span.newBuilder().setTraceId(TRACE_ID) + .setServiceName("service1") + .setSpanId("span-2") + .setOperationName("op2") + .setDuration(200L) + .addTags(tag_2) + .build() + + val spanBuffer = SpanBuffer.newBuilder().addChildSpans(span_1).addChildSpans(span_2).setTraceId(TRACE_ID).build() + val doc = generator.createIndexDocument(TRACE_ID, spanBuffer).get + doc.json shouldBe "{\"traceid\":\"trace_id\",\"rootduration\":0,\"starttime\":0,\"spans\":[{\"servicename\":\"service1\",\"starttime\":[0],\"errorcode\":[5],\"duration\":[100],\"operationname\":\"op1\"},{\"servicename\":\"service1\",\"starttime\":[0],\"errorcode\":[3],\"duration\":[200],\"operationname\":\"op2\"}]}" + } + + it ("should extract unique tag values along with serviceName, operationName and duration and create json document for indexing") { + val indexableTags = List( + WhitelistIndexField(name = "role", `type` = IndexFieldType.string), + WhitelistIndexField(name = "errorCode", `type` = IndexFieldType.long)) + val whitelistConfig = WhitelistIndexFieldConfiguration() + whitelistConfig.onReload(Serialization.write(WhiteListIndexFields(indexableTags))) + val generator = new IndexDocumentGenerator(whitelistConfig) + + val tag_1 = Tag.newBuilder().setKey("role").setType(Tag.TagType.STRING).setVStr("haystack").build() + val tag_2 = Tag.newBuilder().setKey("errorCode").setType(Tag.TagType.LONG).setVLong(3).build() + + val span_1 = Span.newBuilder().setTraceId(TRACE_ID) + .setServiceName("service1") + .setSpanId("span-1") + .setOperationName("op1") + .setDuration(100L) + .addTags(tag_1) + .build() + val span_2 = Span.newBuilder().setTraceId(TRACE_ID) + .setServiceName("service1") + .setSpanId("span-2") + .setOperationName("op2") + .setDuration(200L) + .addTags(tag_2) + .build() + + val spanBuffer = SpanBuffer.newBuilder().addChildSpans(span_1).addChildSpans(span_2).setTraceId(TRACE_ID).build() + val doc = generator.createIndexDocument(TRACE_ID, spanBuffer).get + doc.json shouldBe "{\"traceid\":\"trace_id\",\"rootduration\":0,\"starttime\":0,\"spans\":[{\"role\":[\"haystack\"],\"servicename\":\"service1\",\"starttime\":[0],\"duration\":[100],\"operationname\":\"op1\"},{\"servicename\":\"service1\",\"starttime\":[0],\"errorcode\":[3],\"duration\":[200],\"operationname\":\"op2\"}]}" + } + + it ("should extract tags, log values along with serviceName, operationName and duration and create json document for indexing") { + val indexableTags = List( + WhitelistIndexField(name = "role", `type` = IndexFieldType.string), + WhitelistIndexField(name = "errorCode", `type` = IndexFieldType.long), + WhitelistIndexField(name = "exception", `type` = IndexFieldType.string)) + + val whitelistConfig = WhitelistIndexFieldConfiguration() + whitelistConfig.onReload(Serialization.write(WhiteListIndexFields(indexableTags))) + val generator = new IndexDocumentGenerator(whitelistConfig) + + val tag_1 = Tag.newBuilder().setKey("role").setType(Tag.TagType.STRING).setVStr("haystack").build() + val tag_2 = Tag.newBuilder().setKey("errorCode").setType(Tag.TagType.LONG).setVLong(3).build() + val log_1 = Log.newBuilder() + .addFields(Tag.newBuilder().setKey("exception").setType(Tag.TagType.STRING).setVStr("xxx-yy-zzz").build()) + .setTimestamp(100L) + val log_2 = Log.newBuilder() + .addFields(Tag.newBuilder().setKey("exception").setType(Tag.TagType.STRING).setVStr("aaa-bb-cccc").build()) + .setTimestamp(200L) + + val span_1 = Span.newBuilder().setTraceId(TRACE_ID) + .setServiceName("service1") + .setSpanId("span-1") + .setOperationName("op1") + .setDuration(100L) + .setStartTime(START_TIME_1) + .addTags(tag_1) + .build() + val span_2 = Span.newBuilder().setTraceId("traceId") + .setServiceName("service1") + .setSpanId("span-2") + .setOperationName("op2") + .setDuration(200L) + .setStartTime(START_TIME_2) + .addTags(tag_2) + .addLogs(log_1) + .addLogs(log_2) + .build() + + val spanBuffer = SpanBuffer.newBuilder().addChildSpans(span_1).addChildSpans(span_2).setTraceId(TRACE_ID).build() + val doc = generator.createIndexDocument(TRACE_ID, spanBuffer).get + doc.json shouldBe "{\"traceid\":\"trace_id\",\"rootduration\":0,\"starttime\":1529042838000000,\"spans\":[{\"role\":[\"haystack\"],\"servicename\":\"service1\",\"starttime\":[1529042838000000],\"duration\":[100],\"operationname\":\"op1\"},{\"servicename\":\"service1\",\"starttime\":[1529042848000000],\"errorcode\":[3],\"duration\":[200],\"operationname\":\"op2\"}]}" + } + + it("should transform the tags for all data types like bool, long, double to string type") { + val indexableTags = List( + WhitelistIndexField(name = "errorCode", `type` = IndexFieldType.string), + WhitelistIndexField(name = "isErrored", `type` = IndexFieldType.string), + WhitelistIndexField(name = "exception", `type` = IndexFieldType.string)) + val whitelistConfig = WhitelistIndexFieldConfiguration() + whitelistConfig.onReload(Serialization.write(WhiteListIndexFields(indexableTags))) + val generator = new IndexDocumentGenerator(whitelistConfig) + + val tag_1 = Tag.newBuilder().setKey("isErrored").setType(Tag.TagType.BOOL).setVBool(true).build() + val tag_2 = Tag.newBuilder().setKey("errorCode").setType(Tag.TagType.LONG).setVLong(500).build() + val log_1 = Log.newBuilder() + .addFields(Tag.newBuilder().setKey("exception").setType(Tag.TagType.BINARY).setVBytes(ByteString.copyFromUtf8("xxx-yy-zzz")).build()) + .setTimestamp(100L) + val span_1 = Span.newBuilder().setTraceId(TRACE_ID) + .setServiceName("service1") + .setSpanId("span-1") + .setOperationName("op1") + .setDuration(100L) + .setStartTime(START_TIME_1) + .addTags(tag_1) + .addTags(tag_2) + .addLogs(log_1) + .build() + val spanBuffer = SpanBuffer.newBuilder().addChildSpans(span_1).setTraceId(TRACE_ID).build() + + val doc = generator.createIndexDocument(TRACE_ID, spanBuffer) + doc.get.json shouldEqual "{\"traceid\":\"trace_id\",\"rootduration\":0,\"starttime\":1529042838000000,\"spans\":[{\"servicename\":\"service1\",\"iserrored\":[\"true\"],\"starttime\":[1529042838000000],\"errorcode\":[\"500\"],\"duration\":[100],\"operationname\":\"op1\"}]}" + doc.get.json.contains("iserrored") shouldBe true + } + } +} \ No newline at end of file diff --git a/traces/indexer/src/test/scala/com/expedia/www/haystack/trace/indexer/unit/SpanIndexProcessorSpec.scala b/traces/indexer/src/test/scala/com/expedia/www/haystack/trace/indexer/unit/SpanIndexProcessorSpec.scala new file mode 100644 index 000000000..292a67682 --- /dev/null +++ b/traces/indexer/src/test/scala/com/expedia/www/haystack/trace/indexer/unit/SpanIndexProcessorSpec.scala @@ -0,0 +1,140 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expedia.www.haystack.trace.indexer.unit + +import com.expedia.open.tracing.Span +import com.expedia.open.tracing.buffer.SpanBuffer +import com.expedia.www.haystack.trace.commons.packer.{NoopPacker, PackedMessage, PackerType} +import com.expedia.www.haystack.trace.indexer.config.entities.SpanAccumulatorConfiguration +import com.expedia.www.haystack.trace.indexer.processors.SpanIndexProcessor +import com.expedia.www.haystack.trace.indexer.store.SpanBufferMemoryStoreSupplier +import com.expedia.www.haystack.trace.indexer.store.data.model.SpanBufferWithMetadata +import com.expedia.www.haystack.trace.indexer.store.traits.SpanBufferKeyValueStore +import com.expedia.www.haystack.trace.indexer.writers.TraceWriter +import org.apache.kafka.clients.consumer.ConsumerRecord +import org.apache.kafka.common.record.TimestampType +import org.easymock.EasyMock +import org.easymock.EasyMock._ +import org.scalatest.easymock.EasyMockSugar +import org.scalatest.{FunSpec, Matchers} + +import scala.collection.mutable + +class SpanIndexProcessorSpec extends FunSpec with Matchers with EasyMockSugar { + private implicit val executor = scala.concurrent.ExecutionContext.global + + private val TRACE_ID = "traceid" + private val startRecordTimestamp = System.currentTimeMillis() + private val timestampInterval = 100 + private val maxSpans = 10 + private val bufferingWindow = 10000 + private val startRecordOffset = 11 + private val accumulatorConfig = SpanAccumulatorConfiguration(10, 100, 2000, bufferingWindow, PackerType.NONE) + + describe("Span Index Processor") { + it("should process the records for a partition and return the offsets to commit") { + // mock entities + val mockStore = mock[SpanBufferKeyValueStore] + val storeSupplier = new SpanBufferMemoryStoreSupplier(10, 100) { + override def get(): SpanBufferKeyValueStore = mockStore + } + + val mockBackend = mock[TraceWriter] + + val processor = new SpanIndexProcessor(accumulatorConfig, storeSupplier, Seq(mockBackend), new NoopPacker[SpanBuffer])(executor) + val (spanBufferWithMetadata, records) = createConsumerRecordsAndSetStoreExpectation(maxSpans, startRecordTimestamp, timestampInterval, startRecordOffset, mockStore) + val finalStreamTimestamp = startRecordTimestamp + ((maxSpans - 1) * timestampInterval) + + val packedMessage = EasyMock.newCapture[PackedMessage[SpanBuffer]]() + val writeTraceIdCapture = EasyMock.newCapture[String]() + val writeLastRecordCapture = EasyMock.newCapture[Boolean]() + + expecting { + mockStore.addEvictionListener(processor) + mockStore.init() + mockStore.getAndRemoveSpanBuffersOlderThan(finalStreamTimestamp - bufferingWindow).andReturn(mutable.ListBuffer(spanBufferWithMetadata)) + mockBackend.writeAsync(capture(writeTraceIdCapture), capture(packedMessage), capture(writeLastRecordCapture)) + mockStore.close() + } + + whenExecuting(mockStore, mockBackend) { + processor.init() + val offsets = processor.process(records) + SpanBuffer.parseFrom(packedMessage.getValue.packedDataBytes).getChildSpansCount shouldBe maxSpans + writeTraceIdCapture.getValue shouldBe TRACE_ID + writeLastRecordCapture.getValue shouldBe true + offsets.get.offset() shouldBe startRecordOffset + + processor.close() + } + } + + it("should process the records for a partition, and if store does not emit any 'old' spanBuffers, then writers will not be called and no offsets will be committted") { + // mock entities + val mockStore = mock[SpanBufferKeyValueStore] + val storeSupplier = new SpanBufferMemoryStoreSupplier(10, 100) { + override def get(): SpanBufferKeyValueStore = mockStore + } + + val mockBackend = mock[TraceWriter] + + val processor = new SpanIndexProcessor(accumulatorConfig, storeSupplier, Seq(mockBackend), new NoopPacker)(executor) + val (_, records) = createConsumerRecordsAndSetStoreExpectation(maxSpans, startRecordTimestamp, timestampInterval, startRecordOffset, mockStore) + val finalStreamTimestamp = startRecordTimestamp + ((maxSpans - 1) * timestampInterval) + + expecting { + mockStore.addEvictionListener(processor) + mockStore.init() + mockStore.getAndRemoveSpanBuffersOlderThan(finalStreamTimestamp - bufferingWindow).andReturn(mutable.ListBuffer()) + } + + whenExecuting(mockStore, mockBackend) { + processor.init() + val offsets = processor.process(records) + offsets shouldBe 'empty + } + } + } + + private def createConsumerRecordsAndSetStoreExpectation(maxSpans: Int, + startRecordTimestamp: Long, + timestampInterval: Long, + startRecordOffset: Int, + mockStore: SpanBufferKeyValueStore): + (SpanBufferWithMetadata, Iterable[ConsumerRecord[String, Span]]) = { + + val builder = SpanBuffer.newBuilder().setTraceId(TRACE_ID) + val spanBufferWithMetadata = SpanBufferWithMetadata(builder, startRecordTimestamp, startRecordOffset) + + val consumerRecords = + for (idx <- 0 until maxSpans; + span = Span.newBuilder().setTraceId(TRACE_ID).setSpanId(idx.toString).build(); + timestamp = startRecordTimestamp + (idx * timestampInterval); + _ = builder.addChildSpans(span); + _ = mockStore.addOrUpdateSpanBuffer(TRACE_ID, span, timestamp, idx + startRecordOffset).andReturn(spanBufferWithMetadata)) + yield new ConsumerRecord[String, Span]("topic", + 0, + idx + startRecordOffset, + timestamp, + TimestampType.CREATE_TIME, + 0, 0, 0, + TRACE_ID, + span) + + (spanBufferWithMetadata, consumerRecords) + } +} diff --git a/traces/indexer/src/test/scala/com/expedia/www/haystack/trace/indexer/unit/SpanSerdeSpec.scala b/traces/indexer/src/test/scala/com/expedia/www/haystack/trace/indexer/unit/SpanSerdeSpec.scala new file mode 100644 index 000000000..c2b9efbc1 --- /dev/null +++ b/traces/indexer/src/test/scala/com/expedia/www/haystack/trace/indexer/unit/SpanSerdeSpec.scala @@ -0,0 +1,68 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trace.indexer.unit + +import com.expedia.open.tracing.{Span, Tag} +import com.expedia.www.haystack.trace.indexer.serde.SpanDeserializer +import org.scalatest.{FunSpec, Matchers} + +class SpanSerdeSpec extends FunSpec with Matchers { + + private val TRACE_ID = "unique-trace-id" + private val PARENT_SPAN_ID = "parent-span-id" + private val SPAN_ID = "spanId-1" + private val OP_NAME = "testOp" + private val TAG_KEY = "tag-key" + private val TAG_VALUE = "tag-value" + private val TOPIC = "topic" + + private val testSpan = { + val tag = Tag.newBuilder().setType(Tag.TagType.STRING).setKey(TAG_KEY).setVStr(TAG_VALUE).build() + Span.newBuilder() + .setTraceId(TRACE_ID) + .setParentSpanId(PARENT_SPAN_ID) + .setSpanId(SPAN_ID) + .setOperationName(OP_NAME) + .addTags(tag) + .build() + } + + describe("Span Serde") { + it("should serialize and deserialize a span object") { + val deser = new SpanDeserializer().deserialize(TOPIC, testSpan.toByteArray) + deser.getTraceId shouldEqual TRACE_ID + + deser.getParentSpanId shouldEqual PARENT_SPAN_ID + deser.getTraceId shouldEqual TRACE_ID + deser.getSpanId shouldEqual SPAN_ID + deser.getOperationName shouldEqual OP_NAME + deser.getTagsCount shouldBe 1 + + val tag = deser.getTags(0) + tag.getType shouldBe Tag.TagType.STRING + tag.getKey shouldBe TAG_KEY + tag.getVStr shouldBe TAG_VALUE + } + + it("should return null on serializing invalid span bytes") { + val data = "invalid span serialized bytes".getBytes() + val deser = new SpanDeserializer().deserialize(TOPIC, data) + deser shouldBe null + } + } +} diff --git a/traces/indexer/src/test/scala/com/expedia/www/haystack/trace/indexer/unit/ThreadSafeBulkBuilderSpec.scala b/traces/indexer/src/test/scala/com/expedia/www/haystack/trace/indexer/unit/ThreadSafeBulkBuilderSpec.scala new file mode 100644 index 000000000..412758ef5 --- /dev/null +++ b/traces/indexer/src/test/scala/com/expedia/www/haystack/trace/indexer/unit/ThreadSafeBulkBuilderSpec.scala @@ -0,0 +1,109 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trace.indexer.unit + +import com.expedia.www.haystack.trace.indexer.writers.es.ThreadSafeBulkBuilder +import com.google.gson.Gson +import io.searchbox.core.Index +import org.scalatest.{FunSpec, Matchers} + +class ThreadSafeBulkBuilderSpec extends FunSpec with Matchers { + private val gson = new Gson() + + describe("Thread safe bulk builder") { + it("should return the bulk object when index operations exceeds the configured maxDocument count") { + val builder = new ThreadSafeBulkBuilder(maxDocuments = 3, 1000) + var bulkOp = builder.addAction(new Index.Builder("source1").build(), 10, forceBulkCreate = false) + bulkOp shouldBe 'empty + + bulkOp = builder.addAction(new Index.Builder("source2").build(), 10, forceBulkCreate = false) + bulkOp shouldBe 'empty + + bulkOp = builder.addAction(new Index.Builder("source3").build(), 10, forceBulkCreate = false) + var bulkJson = bulkOp.get.getData(gson) + bulkJson shouldEqual + """{"index":{}} + |source1 + |{"index":{}} + |source2 + |{"index":{}} + |source3 + |""".stripMargin + + builder.getDocsCount shouldBe 0 + builder.getTotalSizeInBytes shouldBe 0 + + bulkOp = builder.addAction(new Index.Builder("source4").build(), 10, forceBulkCreate = true) + bulkJson = bulkOp.get. + getData(gson) + bulkJson shouldEqual + """{"index":{}} + |source4 + |""".stripMargin + } + + it("should return the bulk after size of the index operations exceed the configured threshold") { + val builder = new ThreadSafeBulkBuilder(maxDocuments = 10, 100) + var bulkOp = builder.addAction(new Index.Builder("source1").build(), 30, forceBulkCreate = false) + bulkOp shouldBe 'empty + + bulkOp = builder.addAction(new Index.Builder("source2").build(), 30, forceBulkCreate = false) + bulkOp shouldBe 'empty + + bulkOp = builder.addAction(new Index.Builder("source3").build(), 80, forceBulkCreate = false) + val bulkJson = bulkOp.get.getData(gson) + bulkJson shouldEqual """{"index":{}} + |source1 + |{"index":{}} + |source2 + |{"index":{}} + |source3 + |""".stripMargin + + builder.getDocsCount shouldBe 0 + builder.getTotalSizeInBytes shouldBe 0 + } + + it("should return the bulk if forceBulkCreate attribute is set") { + val builder = new ThreadSafeBulkBuilder(maxDocuments = 10, 1000) + var bulkOp = builder.addAction(new Index.Builder("source1").build(), 30, forceBulkCreate = false) + bulkOp shouldBe 'empty + + bulkOp = builder.addAction(new Index.Builder("source2").build(), 30, forceBulkCreate = false) + bulkOp shouldBe 'empty + + bulkOp = builder.addAction(new Index.Builder("source3").build(), 80, forceBulkCreate = false) + bulkOp shouldBe 'empty + + bulkOp = builder.addAction(new Index.Builder("source4").build(), 80, forceBulkCreate = true) + val bulkJson = bulkOp.get.getData(gson) + bulkJson shouldEqual """{"index":{}} + |source1 + |{"index":{}} + |source2 + |{"index":{}} + |source3 + |{"index":{}} + |source4 + |""".stripMargin + + builder.getDocsCount shouldBe 0 + builder.getTotalSizeInBytes shouldBe 0 + } + } +} diff --git a/traces/mvnw b/traces/mvnw new file mode 100755 index 000000000..5551fde8e --- /dev/null +++ b/traces/mvnw @@ -0,0 +1,286 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven2 Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + export JAVA_HOME="`/usr/libexec/java_home`" + else + export JAVA_HOME="/Library/Java/Home" + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" + # TODO classpath? +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="`which java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +BASE_DIR=`find_maven_basedir "$(pwd)"` +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found .mvn/wrapper/maven-wrapper.jar" + fi +else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." + fi + jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" + while IFS="=" read key value; do + case "$key" in (wrapperUrl) jarUrl="$value"; break ;; + esac + done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Downloading from: $jarUrl" + fi + wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + + if command -v wget > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + fi + wget "$jarUrl" -O "$wrapperJarPath" + elif command -v curl > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + fi + curl -o "$wrapperJarPath" "$jarUrl" + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + if [ -e "$javaClass" ]; then + if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Compiling MavenWrapperDownloader.java ..." + fi + # Compiling the Java class + ("$JAVA_HOME/bin/javac" "$javaClass") + fi + if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + # Running the downloader + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Running MavenWrapperDownloader.java ..." + fi + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +if [ "$MVNW_VERBOSE" = true ]; then + echo $MAVEN_PROJECTBASEDIR +fi +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +fi + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/traces/mvnw.cmd b/traces/mvnw.cmd new file mode 100755 index 000000000..e5cfb0ae9 --- /dev/null +++ b/traces/mvnw.cmd @@ -0,0 +1,161 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven2 Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" +if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" +FOR /F "tokens=1,2 delims==" %%A IN (%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties) DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + echo Found %WRAPPER_JAR% +) else ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + powershell -Command "(New-Object Net.WebClient).DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')" + echo Finished downloading %WRAPPER_JAR% +) +@REM End of extension + +%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" +if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%" == "on" pause + +if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% + +exit /B %ERROR_CODE% diff --git a/traces/pom.xml b/traces/pom.xml new file mode 100644 index 000000000..279b720ac --- /dev/null +++ b/traces/pom.xml @@ -0,0 +1,528 @@ + + + + 4.0.0 + com.expedia.www + haystack-traces + 1.0.9-SNAPSHOT + pom + + + commons + indexer + reader + backends + + + + scm:git:git://github.com/ExpediaDotCom/haystack-traces.git + scm:git:ssh://github.com/ExpediaDotCom/haystack-traces.git + http://github.com/ExpediaDotCom/haystack-traces + + + ${project.groupId}:${project.artifactId} + Code to build the haystack indexer and reader which move and read spans from the span stream into + elastic search + Cassandra, etc. + + https://github.com/ExpediaDotCom/haystack-traces/tree/master + + + + + Apache License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + + haystack + Haystack Team + haystack@expedia.com + https://github.com/ExpediaDotCom/haystack + + + + + 1.8 + 3.4.0 + 1.0.65 + + 1.2.3 + 1.7.25 + 3.4 + 2.6 + 3.5.3 + 1.3.1 + 1.1.7.1 + 1.3.7-2 + + 5.3.2 + 6.0.1 + 4.5.3 + 1.11.20 + 2 + 12 + 5 + 1.0.5 + ${scala.major.version}.${scala.minor.version} + ${scala.major.version}.${scala.minor.version}.${scala.tiny.version} + 6.8 + 1.6.0 + 3.0.3 + 1.7.1 + 4.1.16.Final + 1.3.0 + ${basedir}/../checkstyles/scalastyle_config.xml + 1.6 + 3.0.1 + 1.6.8 + true + + + + + + + com.google.protobuf + protobuf-java + ${protobuf.version} + + + io.grpc + grpc-protobuf + ${grpc.version} + + + io.grpc + grpc-stub + ${grpc.version} + + + io.grpc + grpc-netty + ${grpc.version} + + + io.grpc + grpc-services + ${grpc.version} + + + + io.netty + netty-handler + ${netty.handler.version} + + + + + org.scala-lang + scala-library + ${scala-library.version} + + + org.scala-lang + scala-reflect + ${scala-library.version} + + + + + com.typesafe + config + ${typesafe-config.version} + + + io.dropwizard.metrics + metrics-core + 3.1.2 + + + + + ch.qos.logback + logback-classic + ${logback.version} + + + ch.qos.logback + logback-core + ${logback.version} + + + org.slf4j + slf4j-api + ${slf4j-api.version} + + + + com.expedia.www + haystack-logback-metrics-appender + ${haystack.logback.metrics.appender.version} + + + + + io.searchbox + jest + ${jest.version} + + + + org.elasticsearch + elasticsearch + ${elasticsearch.version} + + + + org.json4s + json4s-jackson_${scala.major.minor.version} + ${json4s.version} + + + + org.json4s + json4s-ext_${scala.major.minor.version} + ${json4s.version} + + + + org.apache.commons + commons-lang3 + ${commons-lang.version} + + + + commons-io + commons-io + ${commons-io.version} + + + + + com.amazonaws + aws-java-sdk-sts + ${aws-sdk.version} + + + + com.amazonaws + aws-java-sdk-ec2 + ${aws-sdk.version} + + + + + vc.inreach.aws + aws-signing-request-interceptor + 0.0.22 + + + + org.apache.httpcomponents + httpclient + ${httpclient.version} + + + + + com.expedia.www + haystack-commons + ${haystack-commons.version} + + + + org.xerial.snappy + snappy-java + ${snappy.version} + + + com.github.luben + zstd-jni + ${zstd.version} + + + + + + + com.expedia.www + haystack-commons + + + + io.dropwizard.metrics + metrics-core + + + + com.typesafe + config + + + + org.scala-lang + scala-library + + + + org.scala-lang + scala-reflect + + + + ch.qos.logback + logback-classic + + + + ch.qos.logback + logback-core + + + + org.slf4j + slf4j-api + + + + commons-io + commons-io + + + + + org.scalatest + scalatest_${scala.major.minor.version} + ${scalatest.version} + test + + + org.pegdown + pegdown + ${pegdown.version} + test + + + junit + junit + 4.12 + test + + + org.easymock + easymock + 3.4 + test + + + + + ${basedir}/src/main/scala + + + ${basedir}/src/main/resources + true + + + + + + org.scalatest + scalatest-maven-plugin + 1.0 + + + test + + test + + + + + + + com.github.os72 + protoc-jar-maven-plugin + 3.3.0.1 + + + + org.apache.maven.plugins + maven-shade-plugin + 1.6 + + + + org.scalastyle + scalastyle-maven-plugin + 0.8.0 + + true + false + ${scalastyle.config.location} + ${basedir}/src/main/scala + ${basedir}/src/test/scala + ${project.build.directory}/scalastyle-output.xml + UTF-8 + + + + compile-scalastyle + + check + + compile + + + + + net.alchim31.maven + scala-maven-plugin + 3.2.1 + + + scala-compile-first + process-resources + + add-source + compile + + + + scala-test-compile + process-test-resources + + testCompile + + + + attach-javadocs + + doc-jar + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.6.1 + + ${project.jdk.version} + ${project.jdk.version} + + + + org.scoverage + scoverage-maven-plugin + ${scoverage.plugin.version} + + + 75 + false + true + ${scala-library.version} + true + + + + org.apache.maven.plugins + maven-source-plugin + ${maven-source-plugin.version} + + + attach-sources + + jar-no-fork + + + + + + org.apache.maven.plugins + maven-gpg-plugin + ${maven-gpg-plugin.version} + + + org.sonatype.plugins + nexus-staging-maven-plugin + ${nexus-staging-maven-plugin.version} + + + + + + org.apache.maven.plugins + maven-source-plugin + + + attach-sources + + jar-no-fork + + + + + + org.apache.maven.plugins + maven-gpg-plugin + + ${skipGpg} + + + + sign-artifacts + verify + + sign + + + + + + org.sonatype.plugins + nexus-staging-maven-plugin + true + + ossrh + https://oss.sonatype.org/ + true + + + + + + + + ossrh + https://oss.sonatype.org/content/repositories/snapshots + + + ossrh + http://oss.sonatype.org/service/local/staging/deploy/maven2/ + + + diff --git a/traces/reader/Makefile b/traces/reader/Makefile new file mode 100644 index 000000000..99fe50ba0 --- /dev/null +++ b/traces/reader/Makefile @@ -0,0 +1,23 @@ +.PHONY: docker_build prepare_integration_test_env integration_test release + +export DOCKER_ORG := expediadotcom +export DOCKER_IMAGE_NAME := haystack-trace-reader +PWD := $(shell pwd) +SERVICE_DEBUG_ON ?= false + +docker_build: + # build docker image using existing app jar + docker build -t $(DOCKER_IMAGE_NAME) -f build/docker/Dockerfile . + +prepare_integration_test_env: docker_build + # prepare environment to run integration tests against + docker-compose -f build/integration-tests/docker-compose.yml -p sandbox up -d + sleep 30 + +integration_test: prepare_integration_test_env + cd ../ && ./mvnw -q integration-test -pl reader -am + docker-compose -f build/integration-tests/docker-compose.yml -p sandbox stop + docker rm $(shell docker ps -a -q) + +release: + ../deployment/scripts/publish-to-docker-hub.sh diff --git a/traces/reader/README.md b/traces/reader/README.md new file mode 100644 index 000000000..a1fb06a98 --- /dev/null +++ b/traces/reader/README.md @@ -0,0 +1,14 @@ +# haystack-trace-reader + +Service for fetching traces and fields from persistent storages. + +## Technical Details + +In order to understand this service, we recommend to read the details of [haystack](https://github.com/ExpediaDotCom/haystack) project. +This service reads from [TraceBackend]() and [ElasticSearch](https://www.elastic.co/) stores. API endpoints are exposed as [GRPC](https://grpc.io/) endpoints. + +Will fill in more details as we go.. + +## Building + +Check the details on [Build Section](../README.md) diff --git a/traces/reader/build/docker/Dockerfile b/traces/reader/build/docker/Dockerfile new file mode 100644 index 000000000..276488ef2 --- /dev/null +++ b/traces/reader/build/docker/Dockerfile @@ -0,0 +1,30 @@ +FROM openjdk:8-jre +MAINTAINER Haystack + +ENV APP_NAME haystack-trace-reader +ENV APP_HOME /app/bin +ENV JMXTRANS_AGENT jmxtrans-agent-1.2.6 +ENV DOCKERIZE_VERSION v0.6.1 + +ADD https://github.com/jwilder/dockerize/releases/download/${DOCKERIZE_VERSION}/dockerize-alpine-linux-amd64-${DOCKERIZE_VERSION}.tar.gz dockerize.tar.gz +RUN tar xzf dockerize.tar.gz +RUN chmod +x dockerize + +RUN mkdir -p ${APP_HOME} + +COPY target/${APP_NAME}.jar ${APP_HOME}/ +COPY build/docker/start-app.sh ${APP_HOME}/ +RUN chmod +x ${APP_HOME}/start-app.sh + +RUN GRPC_HEALTH_PROBE_VERSION=v0.2.0 && \ + wget -qO/bin/grpc_health_probe https://github.com/grpc-ecosystem/grpc-health-probe/releases/download/${GRPC_HEALTH_PROBE_VERSION}/grpc_health_probe-linux-amd64 && \ + chmod +x /bin/grpc_health_probe + +COPY build/docker/jmxtrans-agent.xml ${APP_HOME}/ +ADD https://github.com/jmxtrans/jmxtrans-agent/releases/download/${JMXTRANS_AGENT}/${JMXTRANS_AGENT}.jar ${APP_HOME}/ + +WORKDIR ${APP_HOME} + +EXPOSE 8088 + +ENTRYPOINT ["./start-app.sh"] diff --git a/traces/reader/build/docker/jmxtrans-agent.xml b/traces/reader/build/docker/jmxtrans-agent.xml new file mode 100644 index 000000000..44fc677c7 --- /dev/null +++ b/traces/reader/build/docker/jmxtrans-agent.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + ${HAYSTACK_GRAPHITE_HOST:monitoring-influxdb-graphite.kube-system.svc} + ${HAYSTACK_GRAPHITE_PORT:2003} + ${HAYSTACK_GRAPHITE_ENABLED:true} + haystack.traces.reader.#hostname#. + + 30 + diff --git a/traces/reader/build/docker/start-app.sh b/traces/reader/build/docker/start-app.sh new file mode 100755 index 000000000..ba2c65569 --- /dev/null +++ b/traces/reader/build/docker/start-app.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +[ -z "$JAVA_XMS" ] && JAVA_XMS=1024m +[ -z "$JAVA_XMX" ] && JAVA_XMX=1024m +[ -z "$JAVA_GC_OPTS" ] && JAVA_GC_OPTS="-XX:+UseG1GC" + +set -e +JAVA_OPTS="${JAVA_OPTS} \ +-javaagent:${APP_HOME}/${JMXTRANS_AGENT}.jar=${APP_HOME}/jmxtrans-agent.xml \ +${JAVA_GC_OPTS} \ +-Xmx${JAVA_XMX} \ +-Xms${JAVA_XMS} \ +-XX:+ExitOnOutOfMemoryError \ +-Dapplication.name=${APP_NAME} \ +-Dapplication.home=${APP_HOME}" + +if [[ -n "$SERVICE_DEBUG_ON" ]] && [[ "$SERVICE_DEBUG_ON" == true ]]; then + JAVA_OPTS="$JAVA_OPTS -Xdebug -Xrunjdwp:transport=dt_socket,address=5005,server=y" +fi + +exec java ${JAVA_OPTS} -jar "${APP_HOME}/${APP_NAME}.jar" diff --git a/traces/reader/build/integration-tests/docker-app.conf b/traces/reader/build/integration-tests/docker-app.conf new file mode 100644 index 000000000..d058819a8 --- /dev/null +++ b/traces/reader/build/integration-tests/docker-app.conf @@ -0,0 +1,66 @@ +health.status.path = "isHealthy" + +service { + port = 8088 + ssl { + enabled = false + cert.path = "" + private.key.path = "" + } +} + +backend { + client { + host = "localhost" + port = 8090 + } +} + +elasticsearch { + endpoint = "http://elasticsearch:9200" + conn.timeout.ms = 10000 + read.timeout.ms = 5000 + + index { + name.prefix = "haystack-traces" + type = "spans" + } +} + +trace { + validators { + sequence = [ + "com.expedia.www.haystack.trace.reader.readers.validators.TraceIdValidator" + "com.expedia.www.haystack.trace.reader.readers.validators.ParentIdValidator" + "com.expedia.www.haystack.trace.reader.readers.validators.RootValidator" + ] + } + + transformers { + pre { + sequence = [ + "com.expedia.www.haystack.trace.reader.readers.transformers.DeDuplicateSpanTransformer" + ] + } + post { + sequence = [ + "com.expedia.www.haystack.trace.reader.readers.transformers.PartialSpanTransformer" + "com.expedia.www.haystack.trace.reader.readers.transformers.ClockSkewTransformer" + "com.expedia.www.haystack.trace.reader.readers.transformers.SortSpanTransformer" + ] + } + } +} + +reload { + tables { + index.fields.config = "whitelist-index-fields" + } + config { + endpoint = "http://elasticsearch:9200" + database.name = "reload-configs" + } + startup.load = true + interval.ms = 5000 # -1 will imply 'no reload' +} + diff --git a/traces/reader/build/integration-tests/docker-compose.yml b/traces/reader/build/integration-tests/docker-compose.yml new file mode 100644 index 000000000..bbea51ac6 --- /dev/null +++ b/traces/reader/build/integration-tests/docker-compose.yml @@ -0,0 +1,8 @@ +version: '3' +services: + elasticsearch: + image: elastic/elasticsearch:6.0.1 + environment: + ES_JAVA_OPTS: "-Xms256m -Xmx256m" + ports: + - "9200:9200" diff --git a/traces/reader/pom.xml b/traces/reader/pom.xml new file mode 100644 index 000000000..2d5c7bd9e --- /dev/null +++ b/traces/reader/pom.xml @@ -0,0 +1,181 @@ + + + + + haystack-traces + com.expedia.www + 1.0.9-SNAPSHOT + + + 4.0.0 + haystack-trace-reader + jar + + + com.expedia.www.haystack.trace.reader.Service + ${project.artifactId}-${project.version} + 3.3.0.1 + + + + + com.expedia.www + haystack-trace-commons + ${project.version} + + + + com.google.protobuf + protobuf-java + + + + io.grpc + grpc-protobuf + + + + io.grpc + grpc-stub + + + + io.grpc + grpc-services + + + + io.grpc + grpc-netty + + + + io.netty + netty-handler + + + + io.searchbox + jest + + + + org.elasticsearch + elasticsearch + + + + org.apache.commons + commons-lang3 + + + + org.apache.httpcomponents + httpclient + + + + com.amazonaws + aws-java-sdk-ec2 + + + + com.expedia.www + haystack-logback-metrics-appender + + + + + com.expedia.www + haystack-trace-backend-memory + ${project.version} + test + + + + + ${finalName} + + + org.scalatest + scalatest-maven-plugin + + + test + + test + + + com.expedia.www.haystack.trace.reader.unit + + + + integration-test + integration-test + + test + + + + /src/reader/build/integration-tests/docker-app.conf + + com.expedia.www.haystack.trace.reader.integration + + + + + + + org.apache.maven.plugins + maven-shade-plugin + + true + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + + + + package + + shade + + + + + reference.conf + + + ${mainClass} + + + + + + + + + net.alchim31.maven + scala-maven-plugin + + + + org.scalastyle + scalastyle-maven-plugin + + + org.apache.maven.plugins + maven-compiler-plugin + + + + + diff --git a/traces/reader/src/main/resources/config/base.conf b/traces/reader/src/main/resources/config/base.conf new file mode 100644 index 000000000..3b728bca8 --- /dev/null +++ b/traces/reader/src/main/resources/config/base.conf @@ -0,0 +1,109 @@ +haystack.graphite.host = "monitoring-influxdb-graphite.kube-system.svc" + +service { + port = 8088 + ssl { + enabled = false + cert.path = "" + private.key.path = "" + } + max.message.size = 52428800 # 50MB in bytes +} + +backend { + client { + host = "localhost" + port = 8090 + } + + # we support multiple grpc based backends, to provide another one use something like following. + # you are required to provide host and port + + # another_client { + # host = "localhost" + # port = 8092 + # } +} + +elasticsearch { + client { + endpoint = "http://elasticsearch:9200" + conn.timeout.ms = 10000 + read.timeout.ms = 5000 + } + index { + spans { + name.prefix = "haystack-traces" + type = "spans" + hour.bucket = 6 + hour.ttl = 72 // 3 * 24 hours + use.root.doc.starttime = true + } + service.metadata { + enabled = true + name = "service-metadata" + type = "metadata" + } + } + + # if enabled flag is true, es requests will be signed + signing.request.aws { + enabled = false + region = "us-west-2" + service.name = "es" + # if 'access.key' is not provided, will use DefaultAWSCredentialsProviderChain to resolve creds + access.key = "" + secret.key = "" + } +} + +trace { + validators { + sequence = [ + "com.expedia.www.haystack.trace.reader.readers.validators.TraceIdValidator" + "com.expedia.www.haystack.trace.reader.readers.validators.ParentIdValidator" + "com.expedia.www.haystack.trace.reader.readers.validators.RootValidator" + ] + } + + transformers { + pre { + sequence = [ + "com.expedia.www.haystack.trace.reader.readers.transformers.DeDuplicateSpanTransformer" + "com.expedia.www.haystack.trace.reader.readers.transformers.ClientServerEventLogTransformer" + "com.expedia.www.haystack.trace.reader.readers.transformers.InfrastructureTagTransformer" + ] + } + post { + sequence = [ + # "com.expedia.www.haystack.trace.reader.readers.transformers.OrphanedTraceTransformer" + "com.expedia.www.haystack.trace.reader.readers.transformers.PartialSpanTransformer" + "com.expedia.www.haystack.trace.reader.readers.transformers.ServerClientSpanMergeTransformer" + "com.expedia.www.haystack.trace.reader.readers.transformers.ClockSkewTransformer" + "com.expedia.www.haystack.trace.reader.readers.transformers.SortSpanTransformer" + ] + } + } +} + +reload { + tables { + index.fields.config = "whitelist-index-fields" + } + config { + endpoint = "http://elasticsearch:9200" + database.name = "reload-configs" + } + startup.load = true + interval.ms = 60000 # -1 will imply 'no reload' + + # if enabled flag is true, es requests will be signed + signing.request.aws { + enabled = false + region = "us-west-2" + service.name = "es" + # if 'access.key' is not provided, will use DefaultAWSCredentialsProviderChain to resolve creds + access.key = "" + secret.key = "" + } +} diff --git a/traces/reader/src/main/resources/logback.xml b/traces/reader/src/main/resources/logback.xml new file mode 100644 index 000000000..ab4e25a63 --- /dev/null +++ b/traces/reader/src/main/resources/logback.xml @@ -0,0 +1,27 @@ + + + + + + + true + + + + + + %d{yyyy-MM-dd HH:mm:ss:SSS} %thread, %level, %logger{70}, "%msg" %replace(%ex){'[\n]+', '\\n'}%nopex%n + + + + + + ${HAYSTACK_LOG_QUEUE_SIZE:-500} + ${HAYSTACK_LOG_DISCARD_THRESHOLD:-0} + + + + + + + diff --git a/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/Service.scala b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/Service.scala new file mode 100644 index 000000000..91b43988b --- /dev/null +++ b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/Service.scala @@ -0,0 +1,96 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.trace.reader + +import java.io.File + +import com.codahale.metrics.JmxReporter +import com.expedia.www.haystack.commons.logger.LoggerUtils +import com.expedia.www.haystack.commons.metrics.MetricsSupport +import com.expedia.www.haystack.trace.reader.config.ProviderConfiguration +import com.expedia.www.haystack.trace.reader.services.{GrpcHealthService, TraceService} +import com.expedia.www.haystack.trace.reader.stores.EsIndexedTraceStore +import io.grpc.netty.NettyServerBuilder +import org.slf4j.{Logger, LoggerFactory} + +object Service extends MetricsSupport { + private val LOGGER: Logger = LoggerFactory.getLogger("TraceReader") + + // primary executor for service's async tasks + implicit private val executor = scala.concurrent.ExecutionContext.global + + def main(args: Array[String]): Unit = { + startJmxReporter() + startService() + } + + private def startJmxReporter(): Unit = { + JmxReporter + .forRegistry(metricRegistry) + .build() + .start() + } + + private def startService(): Unit = { + try { + val config = new ProviderConfiguration + + val store = new EsIndexedTraceStore( + config.traceBackendConfiguration, + config.elasticSearchConfiguration, + config.whitelistedFieldsConfig)(executor) + + val serviceConfig = config.serviceConfig + + val serverBuilder = NettyServerBuilder + .forPort(serviceConfig.port) + .directExecutor() + .addService(new TraceService(store, config.traceValidatorConfig, config.traceTransformerConfig)(executor)) + .addService(new GrpcHealthService()) + + // enable ssl if enabled + if (serviceConfig.ssl.enabled) { + serverBuilder.useTransportSecurity(new File(serviceConfig.ssl.certChainFilePath), new File(serviceConfig.ssl.privateKeyPath)) + } + + // default max message size in grpc is 4MB. if our max message size is greater than 4MB then we should configure this + // limit in the netty based grpc server. + if (serviceConfig.maxSizeInBytes > 4 * 1024 * 1024) serverBuilder.maxMessageSize(serviceConfig.maxSizeInBytes) + + val server = serverBuilder.build().start() + + LOGGER.info(s"server started, listening on ${serviceConfig.port}") + + Runtime.getRuntime.addShutdownHook(new Thread() { + override def run(): Unit = { + LOGGER.info("shutting down gRPC server since JVM is shutting down") + server.shutdown() + store.close() + LOGGER.info("server has been shutdown now") + } + }) + + server.awaitTermination() + } catch { + case ex: Throwable => + ex.printStackTrace() + LOGGER.error("Fatal error observed while running the app", ex) + LoggerUtils.shutdownLogger() + System.exit(1) + } + } +} diff --git a/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/config/ProviderConfiguration.scala b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/config/ProviderConfiguration.scala new file mode 100644 index 000000000..5070b6403 --- /dev/null +++ b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/config/ProviderConfiguration.scala @@ -0,0 +1,219 @@ +/* + * Copyright 2019, Expedia Group. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expedia.www.haystack.trace.reader.config + +import java.util + +import com.expedia.www.haystack.commons.config.ConfigurationLoader +import com.expedia.www.haystack.trace.commons.config.entities._ +import com.expedia.www.haystack.trace.commons.config.reload.{ConfigurationReloadElasticSearchProvider, Reloadable} +import com.expedia.www.haystack.trace.reader.config.entities._ +import com.expedia.www.haystack.trace.reader.readers.transformers.{PartialSpanTransformer, SpanTreeTransformer, TraceTransformer} +import com.expedia.www.haystack.trace.reader.readers.validators.TraceValidator +import com.typesafe.config.Config +import org.apache.commons.lang3.StringUtils + +import scala.collection.JavaConverters._ +import scala.reflect.ClassTag + +class ProviderConfiguration { + private val config: Config = ConfigurationLoader.loadConfigFileWithEnvOverrides() + + val serviceConfig: ServiceConfiguration = { + val serviceConfig = config.getConfig("service") + + val ssl = serviceConfig.getConfig("ssl") + val sslConfig = SslConfiguration(ssl.getBoolean("enabled"), ssl.getString("cert.path"), ssl.getString("private.key.path")) + + ServiceConfiguration(serviceConfig.getInt("port"), sslConfig, serviceConfig.getInt("max.message.size")) + } + + /** + * trace backend configuration object + */ + val traceBackendConfiguration: TraceStoreBackends = { + val traceBackendConfig = config.getConfig("backend") + + val grpcClients = traceBackendConfig.entrySet().asScala + .map(k => StringUtils.split(k.getKey, '.')(0)).toSeq + .map(cl => traceBackendConfig.getConfig(cl)) + .filter(cl => cl.hasPath("host") && cl.hasPath("port")) + .map(cl => GrpcClientConfig(cl.getString("host"), cl.getInt("port"))) + + require(grpcClients.nonEmpty) + + TraceStoreBackends(grpcClients) + } + + + /** + * ElasticSearch configuration + */ + + + private val elasticSearchClientConfig: ElasticSearchClientConfiguration = { + val es = config.getConfig("elasticsearch.client") + + val username = if (es.hasPath("username")) { + Option(es.getString("username")) + } else None + val password = if (es.hasPath("password")) { + Option(es.getString("password")) + } else None + + ElasticSearchClientConfiguration( + endpoint = es.getString("endpoint"), + username = username, + password = password, + connectionTimeoutMillis = es.getInt("conn.timeout.ms"), + readTimeoutMillis = es.getInt("read.timeout.ms") + ) + } + private val spansIndexConfiguration: SpansIndexConfiguration = { + val indexConfig = config.getConfig("elasticsearch.index.spans") + SpansIndexConfiguration( + indexNamePrefix = indexConfig.getString("name.prefix"), + indexType = indexConfig.getString("type"), + indexHourBucket = indexConfig.getInt("hour.bucket"), + indexHourTtl = indexConfig.getInt("hour.ttl"), + useRootDocumentStartTime = indexConfig.getBoolean("use.root.doc.starttime") + ) + } + private val serviceMetadataIndexConfig: ServiceMetadataIndexConfiguration = { + val metadataCfg = config.getConfig("elasticsearch.index.service.metadata") + + ServiceMetadataIndexConfiguration( + metadataCfg.getBoolean("enabled"), + metadataCfg.getString("name"), + metadataCfg.getString("type")) + } + + private def awsRequestSigningConfig(awsESConfig: Config): AWSRequestSigningConfiguration = { + val accessKey: Option[String] = if (awsESConfig.hasPath("access.key") && awsESConfig.getString("access.key").nonEmpty) + Some(awsESConfig.getString("access.key")) + else + None + + val secretKey: Option[String] = if (awsESConfig.hasPath("secret.key") && awsESConfig.getString("secret.key").nonEmpty) { + Some(awsESConfig.getString("secret.key")) + } + else + None + + AWSRequestSigningConfiguration( + awsESConfig.getBoolean("enabled"), + awsESConfig.getString("region"), + awsESConfig.getString("service.name"), + accessKey, + secretKey) + } + + + val elasticSearchConfiguration: ElasticSearchConfiguration = { + ElasticSearchConfiguration( + clientConfiguration = elasticSearchClientConfig, + spansIndexConfiguration = spansIndexConfiguration, + serviceMetadataIndexConfiguration = serviceMetadataIndexConfig, + awsRequestSigningConfiguration = awsRequestSigningConfig(config.getConfig("elasticsearch.signing.request.aws")) + ) + } + + private def toInstances[T](classes: util.List[String])(implicit ct: ClassTag[T]): scala.Seq[T] = { + classes.asScala.map(className => { + val c = Class.forName(className) + + if (c == null) { + throw new RuntimeException(s"No class found with name $className") + } else { + val o = c.newInstance() + val baseClass = ct.runtimeClass + + if (!baseClass.isInstance(o)) { + throw new RuntimeException(s"${c.getName} is not an instance of ${baseClass.getName}") + } + o.asInstanceOf[T] + } + }) + } + + /** + * Configurations to specify what all transforms to apply on traces + */ + val traceTransformerConfig: TraceTransformersConfiguration = { + val preTransformers = config.getStringList("trace.transformers.pre.sequence") + val postTransformers = config.getStringList("trace.transformers.post.sequence") + + val preTransformerInstances = toInstances[TraceTransformer](preTransformers) + var postTransformerInstances = toInstances[SpanTreeTransformer](postTransformers).filterNot(_.isInstanceOf[PartialSpanTransformer]) + postTransformerInstances = new PartialSpanTransformer +: postTransformerInstances + + TraceTransformersConfiguration(preTransformerInstances, postTransformerInstances) + } + + /** + * Configurations to specify what all validations to apply on traces + */ + val traceValidatorConfig: TraceValidatorsConfiguration = { + val validatorConfig: Config = config.getConfig("trace.validators") + TraceValidatorsConfiguration(toInstances[TraceValidator](validatorConfig.getStringList("sequence"))) + } + + /** + * configuration that contains list of tags that should be indexed for a span + */ + val whitelistedFieldsConfig: WhitelistIndexFieldConfiguration = { + val whitelistedFieldsConfig = WhitelistIndexFieldConfiguration() + whitelistedFieldsConfig.reloadConfigTableName = Option(config.getConfig("reload.tables").getString("index.fields.config")) + whitelistedFieldsConfig + } + + // configuration reloader + registerReloadableConfigurations(List(whitelistedFieldsConfig)) + + /** + * registers a reloadable config object to reloader instance. + * The reloader registers them as observers and invokes them periodically when it re-reads the + * configuration from an external store + * + * @param observers list of reloadable configuration objects + * @return the reloader instance that uses ElasticSearch as an external database for storing the configs + */ + private def registerReloadableConfigurations(observers: Seq[Reloadable]): ConfigurationReloadElasticSearchProvider = { + val reload = config.getConfig("reload") + val reloadConfig = ReloadConfiguration( + reload.getString("config.endpoint"), + reload.getString("config.database.name"), + reload.getInt("interval.ms"), + if (reload.hasPath("config.username")) { + Option(reload.getString("config.username")) + } else { + None + }, + if (reload.hasPath("config.password")) { + Option(reload.getString("config.password")) + } else { + None + }, + observers, + loadOnStartup = reload.getBoolean("startup.load")) + + val awsConfig: AWSRequestSigningConfiguration = awsRequestSigningConfig(config.getConfig("reload.signing.request.aws")) + val loader = new ConfigurationReloadElasticSearchProvider(reloadConfig, awsConfig) + if (reloadConfig.loadOnStartup) loader.load() + loader + } +} diff --git a/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/config/entities/ElasticSearchConfiguration.scala b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/config/entities/ElasticSearchConfiguration.scala new file mode 100644 index 000000000..25f6928c9 --- /dev/null +++ b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/config/entities/ElasticSearchConfiguration.scala @@ -0,0 +1,40 @@ +/* + * Copyright 2019, Expedia Group. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.trace.reader.config.entities + +import com.expedia.www.haystack.trace.commons.config.entities.AWSRequestSigningConfiguration + +case class ElasticSearchClientConfiguration(endpoint: String, + username: Option[String], + password: Option[String], + connectionTimeoutMillis: Int, + readTimeoutMillis: Int) + +case class SpansIndexConfiguration(indexNamePrefix: String, + indexType: String, + indexHourTtl: Int, + indexHourBucket: Int, + useRootDocumentStartTime: Boolean) + +case class ServiceMetadataIndexConfiguration(enabled: Boolean, + indexName: String, + indexType: String) + +case class ElasticSearchConfiguration(clientConfiguration: ElasticSearchClientConfiguration, + spansIndexConfiguration: SpansIndexConfiguration, + serviceMetadataIndexConfiguration: ServiceMetadataIndexConfiguration, + awsRequestSigningConfiguration: AWSRequestSigningConfiguration) \ No newline at end of file diff --git a/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/config/entities/ServiceConfiguration.scala b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/config/entities/ServiceConfiguration.scala new file mode 100644 index 000000000..72693fcc1 --- /dev/null +++ b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/config/entities/ServiceConfiguration.scala @@ -0,0 +1,23 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expedia.www.haystack.trace.reader.config.entities + +/** + * @param port port to start grpc servicer on + */ +case class ServiceConfiguration(port: Int, ssl: SslConfiguration, maxSizeInBytes: Int) +case class SslConfiguration(enabled: Boolean, certChainFilePath: String, privateKeyPath: String) diff --git a/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/config/entities/TraceTransformersConfiguration.scala b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/config/entities/TraceTransformersConfiguration.scala new file mode 100644 index 000000000..611391ba3 --- /dev/null +++ b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/config/entities/TraceTransformersConfiguration.scala @@ -0,0 +1,20 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.expedia.www.haystack.trace.reader.config.entities + +import com.expedia.www.haystack.trace.reader.readers.transformers.{SpanTreeTransformer, TraceTransformer} + +case class TraceTransformersConfiguration(preTransformers: Seq[TraceTransformer], postTransformers: Seq[SpanTreeTransformer]) diff --git a/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/config/entities/TraceValidatorsConfiguration.scala b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/config/entities/TraceValidatorsConfiguration.scala new file mode 100644 index 000000000..6da0205f2 --- /dev/null +++ b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/config/entities/TraceValidatorsConfiguration.scala @@ -0,0 +1,20 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.expedia.www.haystack.trace.reader.config.entities + +import com.expedia.www.haystack.trace.reader.readers.validators.TraceValidator + +case class TraceValidatorsConfiguration(validators: Seq[TraceValidator]) diff --git a/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/exceptions/ElasticSearchClientError.scala b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/exceptions/ElasticSearchClientError.scala new file mode 100644 index 000000000..8d471165b --- /dev/null +++ b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/exceptions/ElasticSearchClientError.scala @@ -0,0 +1,22 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expedia.www.haystack.trace.reader.exceptions + +import io.grpc.{Status, StatusException} + +case class ElasticSearchClientError(status: Int, details: String) + extends StatusException(Status.INTERNAL.withDescription(s"es client returned status $status. $details")) diff --git a/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/exceptions/InvalidTraceException.scala b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/exceptions/InvalidTraceException.scala new file mode 100644 index 000000000..90dd3c414 --- /dev/null +++ b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/exceptions/InvalidTraceException.scala @@ -0,0 +1,22 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expedia.www.haystack.trace.reader.exceptions + +import io.grpc.{Status, StatusException} + +class InvalidTraceException(message: String) + extends StatusException(Status.FAILED_PRECONDITION.withDescription(s"Invalid Trace: $message")) diff --git a/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/exceptions/InvalidTraceIdInDocument.scala b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/exceptions/InvalidTraceIdInDocument.scala new file mode 100644 index 000000000..8c2a6f947 --- /dev/null +++ b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/exceptions/InvalidTraceIdInDocument.scala @@ -0,0 +1,22 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expedia.www.haystack.trace.reader.exceptions + +import io.grpc.{Status, StatusException} + +case class InvalidTraceIdInDocument(docId: String) + extends StatusException(Status.INTERNAL.withDescription(s"invalid traceId in doc: $docId")) diff --git a/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/exceptions/SpanNotFoundException.scala b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/exceptions/SpanNotFoundException.scala new file mode 100644 index 000000000..bf62a1c20 --- /dev/null +++ b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/exceptions/SpanNotFoundException.scala @@ -0,0 +1,21 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expedia.www.haystack.trace.reader.exceptions + +import io.grpc.{Status, StatusException} + +class SpanNotFoundException extends StatusException(Status.NOT_FOUND.withDescription("spanId not found in trace")) diff --git a/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/exceptions/TraceNotFoundException.scala b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/exceptions/TraceNotFoundException.scala new file mode 100644 index 000000000..df9cf3f6e --- /dev/null +++ b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/exceptions/TraceNotFoundException.scala @@ -0,0 +1,21 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expedia.www.haystack.trace.reader.exceptions + +import io.grpc.{Status, StatusException} + +class TraceNotFoundException extends StatusException(Status.NOT_FOUND.withDescription("traceId not found in data store")) diff --git a/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/metrics/AppMetricNames.scala b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/metrics/AppMetricNames.scala new file mode 100644 index 000000000..e20b124ec --- /dev/null +++ b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/metrics/AppMetricNames.scala @@ -0,0 +1,31 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trace.reader.metrics + +object AppMetricNames { + + val ELASTIC_SEARCH_READ_TIME = "elasticsearch.read.time" + val ELASTIC_SEARCH_READ_FAILURES = "elasticsearch.read.failures" + + val BACKEND_READ_TIME = "backend.read.time" + val BACKEND_READ_FAILURES = "backend.read.failures" + val BACKEND_TRACES_FAILURE = "backend.traces.failures" + + val SEARCH_TRACE_REJECTED = "search.trace.rejected" + val COUNT_BUCKET_REJECTED = "count.bucket.rejected" +} diff --git a/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/TraceProcessor.scala b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/TraceProcessor.scala new file mode 100644 index 000000000..b6d2941ae --- /dev/null +++ b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/TraceProcessor.scala @@ -0,0 +1,37 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expedia.www.haystack.trace.reader.readers + +import com.expedia.open.tracing.api.Trace +import com.expedia.www.haystack.trace.reader.readers.transformers.{PostTraceTransformationHandler, SpanTreeTransformer, TraceTransformationHandler, TraceTransformer} +import com.expedia.www.haystack.trace.reader.readers.validators.{TraceValidationHandler, TraceValidator} + +import scala.util.Try + +class TraceProcessor(validators: Seq[TraceValidator], + preValidationTransformers: Seq[TraceTransformer], + postValidationTransformers: Seq[SpanTreeTransformer]) { + + private val validationHandler: TraceValidationHandler = new TraceValidationHandler(validators) + private val postTransformers: PostTraceTransformationHandler = new PostTraceTransformationHandler(postValidationTransformers) + private val preTransformers: TraceTransformationHandler = new TraceTransformationHandler(preValidationTransformers) + + def process(trace: Trace): Try[Trace] = { + for (trace <- Try(preTransformers.transform(trace)); + validated <- validationHandler.validate(trace)) yield postTransformers.transform(validated) + } +} diff --git a/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/TraceReader.scala b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/TraceReader.scala new file mode 100644 index 000000000..00b96a7f2 --- /dev/null +++ b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/TraceReader.scala @@ -0,0 +1,156 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expedia.www.haystack.trace.reader.readers + +import com.codahale.metrics.Meter +import com.expedia.open.tracing.api.{FieldNames, _} +import com.expedia.www.haystack.commons.metrics.MetricsSupport +import com.expedia.www.haystack.trace.reader.config.entities.{TraceTransformersConfiguration, TraceValidatorsConfiguration} +import com.expedia.www.haystack.trace.reader.exceptions.SpanNotFoundException +import com.expedia.www.haystack.trace.reader.readers.utils.AuxiliaryTags +import com.expedia.www.haystack.trace.reader.readers.utils.TagExtractors._ +import com.expedia.www.haystack.trace.reader.stores.TraceStore +import org.slf4j.{Logger, LoggerFactory} + +import scala.collection.JavaConverters._ +import scala.concurrent.{ExecutionContextExecutor, Future} +import scala.util.{Failure, Success} + +object TraceReader extends MetricsSupport { + private val LOGGER: Logger = LoggerFactory.getLogger(s"${classOf[TraceReader]}.search.trace.rejection") + private val traceRejectedCounter: Meter = metricRegistry.meter("search.trace.rejection") +} + +class TraceReader(traceStore: TraceStore, + validatorsConfig: TraceValidatorsConfiguration, + transformersConfig: TraceTransformersConfiguration) + (implicit val executor: ExecutionContextExecutor) + extends TraceProcessor(validatorsConfig.validators, transformersConfig.preTransformers, transformersConfig.postTransformers) { + + def getTrace(request: TraceRequest): Future[Trace] = { + traceStore + .getTrace(request.getTraceId) + .flatMap(process(_) match { + case Success(span) => Future.successful(span) + case Failure(ex) => Future.failed(ex) + }) + } + + def getRawTrace(request: TraceRequest): Future[Trace] = { + traceStore.getTrace(request.getTraceId) + } + + def getRawSpan(request: SpanRequest): Future[SpanResponse] = { + traceStore + .getTrace(request.getTraceId) + .flatMap(trace => { + val spans = trace.getChildSpansList.asScala.filter(_.getSpanId == request.getSpanId) + if (spans.isEmpty) { + Future.failed(new SpanNotFoundException) + } else { + Future.successful(SpanResponse.newBuilder().addAllSpans(spans.asJava).build()) + } + }) + } + + def searchTraces(request: TracesSearchRequest): Future[TracesSearchResult] = { + traceStore + .searchTraces(request) + .map( + traces => { + TracesSearchResult + .newBuilder() + .addAllTraces(traces.flatMap(transformTraceIgnoringInvalid).asJavaCollection) + .build() + }) + } + + private def transformTraceIgnoringInvalid(trace: Trace): Option[Trace] = { + process(trace) match { + case Success(t) => Some(t) + case Failure(ex) => + TraceReader.LOGGER.warn(s"invalid trace=${trace.getTraceId} is rejected", ex) + TraceReader.traceRejectedCounter.mark() + None + } + } + + def getFieldNames: Future[FieldNames] = { + traceStore + .getFieldNames + } + + def getFieldValues(request: FieldValuesRequest): Future[FieldValues] = { + traceStore + .getFieldValues(request) + .map(names => + FieldValues + .newBuilder() + .addAllValues(names.asJavaCollection) + .build()) + } + + def getTraceCallGraph(request: TraceRequest): Future[TraceCallGraph] = { + traceStore + .getTrace(request.getTraceId) + .flatMap(process(_) match { + case Success(trace) => Future.successful(buildTraceCallGraph(trace)) + case Failure(ex) => Future.failed(ex) + }) + } + + def getTraceCounts(request: TraceCountsRequest): Future[TraceCounts] = { + traceStore + .getTraceCounts(request) + } + + def getRawTraces(request: RawTracesRequest): Future[RawTracesResult] = { + traceStore + .getRawTraces(request) + .flatMap(traces => Future.successful(RawTracesResult.newBuilder().addAllTraces(traces.asJava).build())) + } + + private def buildTraceCallGraph(trace: Trace): TraceCallGraph = { + val calls = trace.getChildSpansList + .asScala + .filter(containsTag(_, AuxiliaryTags.IS_MERGED_SPAN)) + .map(span => { + val from = CallNode.newBuilder() + .setServiceName(extractTagStringValue(span, AuxiliaryTags.CLIENT_SERVICE_NAME)) + .setOperationName(extractTagStringValue(span, AuxiliaryTags.CLIENT_OPERATION_NAME)) + .setInfrastructureProvider(extractTagStringValue(span, AuxiliaryTags.CLIENT_INFRASTRUCTURE_PROVIDER)) + .setInfrastructureLocation(extractTagStringValue(span, AuxiliaryTags.CLIENT_INFRASTRUCTURE_LOCATION)) + + val to = CallNode.newBuilder() + .setServiceName(extractTagStringValue(span, AuxiliaryTags.SERVER_SERVICE_NAME)) + .setOperationName(extractTagStringValue(span, AuxiliaryTags.SERVER_OPERATION_NAME)) + .setInfrastructureProvider(extractTagStringValue(span, AuxiliaryTags.SERVER_INFRASTRUCTURE_PROVIDER)) + .setInfrastructureLocation(extractTagStringValue(span, AuxiliaryTags.SERVER_INFRASTRUCTURE_LOCATION)) + + Call.newBuilder() + .setFrom(from) + .setTo(to) + .setNetworkDelta(extractTagLongValue(span, AuxiliaryTags.NETWORK_DELTA)) + .build() + }) + + TraceCallGraph + .newBuilder() + .addAllCalls(calls.asJavaCollection) + .build() + } +} diff --git a/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/transformers/ClientServerEventLogTransformer.scala b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/transformers/ClientServerEventLogTransformer.scala new file mode 100644 index 000000000..42057b31b --- /dev/null +++ b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/transformers/ClientServerEventLogTransformer.scala @@ -0,0 +1,38 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expedia.www.haystack.trace.reader.readers.transformers + +import com.expedia.open.tracing.Span +import com.expedia.www.haystack.trace.commons.utils.{SpanMarkers, SpanUtils} + +import scala.collection.JavaConverters._ + +/** + * add log events(if not present) using span.kind tag value + */ +class ClientServerEventLogTransformer extends TraceTransformer { + + override def transform(spans: Seq[Span]): Seq[Span] = { + spans.map(span => { + span.getTagsList.asScala.find(_.getKey == SpanMarkers.SPAN_KIND_TAG_KEY).map(_.getVStr) match { + case Some(SpanMarkers.SERVER_SPAN_KIND) if !SpanUtils.containsServerLogTag(span) => SpanUtils.addServerLogTag(span) + case Some(SpanMarkers.CLIENT_SPAN_KIND) if !SpanUtils.containsClientLogTag(span) => SpanUtils.addClientLogTag(span) + case _ => span + } + }) + } +} diff --git a/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/transformers/ClockSkewFromParentTransformer.scala b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/transformers/ClockSkewFromParentTransformer.scala new file mode 100644 index 000000000..20e1e70dd --- /dev/null +++ b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/transformers/ClockSkewFromParentTransformer.scala @@ -0,0 +1,84 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expedia.www.haystack.trace.reader.readers.transformers + +import com.expedia.open.tracing.Span +import com.expedia.www.haystack.trace.commons.utils.SpanUtils +import com.expedia.www.haystack.trace.reader.readers.utils.{MutableSpanForest, SpanTree} + +import scala.annotation.tailrec +import scala.collection.mutable.ListBuffer +import scala.collection.{Seq, mutable} + +/** + * Fixes clock skew between parent and child spans + * If any child spans reports a startTime earlier then parent span's startTime or + * an endTime later then the parent span's endTime, then + * the child span's startTime or endTime will be shifted. Where the shift is + * set equal to the parent's startTime or endTime depending on which is off. + */ +class ClockSkewFromParentTransformer extends SpanTreeTransformer { + + case class SpanTreeWithParent(spanTree: SpanTree, parent: Option[Span]) + + override def transform(forest: MutableSpanForest): MutableSpanForest = { + val underlyingSpans = new mutable.ListBuffer[Span] + forest.getAllTrees.foreach(tree => { + adjustSkew(underlyingSpans, List(SpanTreeWithParent(tree, None))) + }) + forest.updateUnderlyingSpans(underlyingSpans) + } + + @tailrec + private def adjustSkew(fixedSpans: ListBuffer[Span], spanTrees: Seq[SpanTreeWithParent]): Unit = { + if (spanTrees.isEmpty) return + + // collect the child trees that need to be corrected for clock skew + val childTrees = mutable.ListBuffer[SpanTreeWithParent]() + + spanTrees.foreach(e => { + val rootSpan = e.spanTree.span + var adjustedSpan = rootSpan + e.parent match { + case Some(parentSpan) => + adjustedSpan = adjustSpan(rootSpan, parentSpan) + fixedSpans += adjustedSpan + case _ => fixedSpans += rootSpan + } + childTrees ++= e.spanTree.children.map(tree => SpanTreeWithParent(tree, Some(adjustedSpan))) + }) + + adjustSkew(fixedSpans, childTrees) + } + + private def adjustSpan(child: Span, parent: Span): Span = { + var shift = 0L + if (child.getStartTime < parent.getStartTime) { + shift = parent.getStartTime - child.getStartTime + } + val childEndTime = SpanUtils.getEndTime(child) + val parentEndTime = SpanUtils.getEndTime(parent) + if (parentEndTime < childEndTime + shift) { + shift = parentEndTime - childEndTime + } + if (shift == 0L) { + child + } else { + Span.newBuilder(child).setStartTime(child.getStartTime + shift).build() + } + } +} diff --git a/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/transformers/ClockSkewTransformer.scala b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/transformers/ClockSkewTransformer.scala new file mode 100644 index 000000000..9fc5abd5a --- /dev/null +++ b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/transformers/ClockSkewTransformer.scala @@ -0,0 +1,118 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expedia.www.haystack.trace.reader.readers.transformers + +import com.expedia.open.tracing.Span +import com.expedia.www.haystack.trace.commons.utils.{SpanMarkers, SpanUtils} +import com.expedia.www.haystack.trace.reader.readers.utils.{MutableSpanForest, SpanTree} + +/** + * Fixes clock skew between parent and child spans + * If any child spans reports a startTime earlier then parent span's startTime, + * corresponding delta will be added in the subtree with child span as root + * + * addSkewInSubtree looks into each child of given subtreeRoot, calculates delta, + * and recursively applies delta in its subtree + */ +class ClockSkewTransformer extends SpanTreeTransformer { + + override def transform(forest: MutableSpanForest): MutableSpanForest = { + require(forest.getAllTrees.size == 1) + + val clockedSkewAdjusted = adjustSkew(forest.getAllTrees.head, None) + forest.updateUnderlyingSpans(clockedSkewAdjusted) + } + + private def adjustSkew(node: SpanTree, previousSkew: Option[Skew]): Seq[Span] = { + val previousSkewAdjustedSpan: Span = previousSkew match { + case Some(skew) => adjustForASpan(node.span, skew) + case None => node.span + } + + getClockSkew(previousSkewAdjustedSpan) match { + case Some(skew) => + val selfSkewAdjustedSpan: Span = adjustForASpan(previousSkewAdjustedSpan, skew) + val children = node.children.flatMap(adjustSkew(_, Some(skew))) + children.prepend(selfSkewAdjustedSpan) + children + case None => + val children = node.children.flatMap(adjustSkew(_, None)) + children.prepend(previousSkewAdjustedSpan) + children + } + } + + private def adjustForASpan(span: Span, skew: Skew): Span = { + if (span.getServiceName == skew.serviceName) { + Span + .newBuilder(span) + .setStartTime(span.getStartTime - skew.delta) + .build() + } + else { + span + } + } + + // if span is a merged span of partial spans, calculate corresponding skew + private def getClockSkew(span: Span): Option[Skew] = { + if (SpanUtils.isMergedSpan(span)) { + calculateClockSkew( + SpanUtils.getEventTimestamp(span, SpanMarkers.CLIENT_SEND_EVENT), + SpanUtils.getEventTimestamp(span, SpanMarkers.CLIENT_RECV_EVENT), + SpanUtils.getEventTimestamp(span, SpanMarkers.SERVER_RECV_EVENT), + SpanUtils.getEventTimestamp(span, SpanMarkers.SERVER_SEND_EVENT), + span.getServiceName + ) + } else { + None + } + } + + /** + * Calculate the clock skew between two servers based on logs in a span + * + * Only adjust for clock skew if logs are not in the following order: + * Client send -> Server receive -> Server send -> Client receive + * + * Special case: if the server (child) span is longer than the client (parent), then do not + * adjust for clock skew. + */ + private def calculateClockSkew(clientSend: Long, + clientRecv: Long, + serverRecv: Long, + serverSend: Long, + serviceName: String): Option[Skew] = { + val clientDuration = clientRecv - clientSend + val serverDuration = serverSend - serverRecv + + // There is only clock skew if CS is after SR or CR is before SS + val csAhead = clientSend < serverRecv + val crAhead = clientRecv > serverSend + if (serverDuration > clientDuration || (csAhead && crAhead)) { + None + } else { + val latency = (clientDuration - serverDuration) / 2 + serverRecv - latency - clientSend match { + case 0 => None + case _ => Some(Skew(serviceName, serverRecv - latency - clientSend)) + } + } + } + + case class Skew(serviceName: String, delta: Long) +} diff --git a/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/transformers/DeDuplicateSpanTransformer.scala b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/transformers/DeDuplicateSpanTransformer.scala new file mode 100644 index 000000000..fafb83a95 --- /dev/null +++ b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/transformers/DeDuplicateSpanTransformer.scala @@ -0,0 +1,37 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expedia.www.haystack.trace.reader.readers.transformers + +import com.expedia.open.tracing.Span + +import scala.collection.mutable + +/** + * dedup the spans with the same span id + */ +class DeDuplicateSpanTransformer extends TraceTransformer { + + override def transform(spans: Seq[Span]): Seq[Span] = { + val seen = mutable.HashSet[Span]() + spans.filter { + span => + val alreadySeen = seen.contains(span) + seen.add(span) + !alreadySeen + } + } +} diff --git a/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/transformers/InfrastructureTagTransformer.scala b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/transformers/InfrastructureTagTransformer.scala new file mode 100644 index 000000000..1613df092 --- /dev/null +++ b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/transformers/InfrastructureTagTransformer.scala @@ -0,0 +1,59 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expedia.www.haystack.trace.reader.readers.transformers + +import com.expedia.open.tracing.{Span, Tag} +import com.expedia.www.haystack.trace.reader.readers.utils.AuxiliaryTags + +import scala.collection.JavaConverters._ +import scala.collection.mutable + +/** + * add the infrastructure tag in all the spans coming for a service, if any of its span(client or server) contains it. + * Many services send the infrastructure tag only in the server span for ease and saving transfer cost. + * This transformer refill the infrastructure tags if required. + */ +class InfrastructureTagTransformer extends TraceTransformer { + + override def transform(spans: Seq[Span]): Seq[Span] = { + val serviceWithInfraTags = mutable.HashMap[String, mutable.ListBuffer[Tag]]() + val spansWithoutInfraTags = mutable.HashSet[Span]() + + spans.foreach { span => + var infraTagsPresent = false + span.getTagsList.asScala.foreach { tag => + if (tag.getKey == AuxiliaryTags.INFRASTRUCTURE_PROVIDER || tag.getKey == AuxiliaryTags.INFRASTRUCTURE_LOCATION) { + val tags = serviceWithInfraTags.getOrElseUpdate(span.getServiceName, mutable.ListBuffer[Tag]()) + tags.append(tag) + infraTagsPresent = true + } + } + + if (!infraTagsPresent) { + spansWithoutInfraTags += span + } + } + + spans.map { span => + if (serviceWithInfraTags.contains(span.getServiceName) && spansWithoutInfraTags.contains(span)) { + span.toBuilder.addAllTags(serviceWithInfraTags(span.getServiceName).asJava).build() + } else { + span + } + } + } +} diff --git a/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/transformers/InvalidParentTransformer.scala b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/transformers/InvalidParentTransformer.scala new file mode 100644 index 000000000..91585391d --- /dev/null +++ b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/transformers/InvalidParentTransformer.scala @@ -0,0 +1,34 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expedia.www.haystack.trace.reader.readers.transformers + +import com.expedia.www.haystack.trace.reader.readers.utils.MutableSpanForest + +/** + * If there are spans with invalid parentId in the trace, mark root to be their parentId + * + * **Apply this transformer only if you are not confident about clients sending in parentIds properly** + */ +class InvalidParentTransformer extends SpanTreeTransformer { + override def transform(spanForest: MutableSpanForest): MutableSpanForest = { + val rootTrees = spanForest.getAllTrees.filter(_.span.getParentSpanId.isEmpty) + + require(rootTrees.size == 1) + + spanForest.mergeTreesUnder(rootTrees.head) + } +} diff --git a/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/transformers/InvalidRootTransformer.scala b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/transformers/InvalidRootTransformer.scala new file mode 100644 index 000000000..ceaae3c66 --- /dev/null +++ b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/transformers/InvalidRootTransformer.scala @@ -0,0 +1,79 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expedia.www.haystack.trace.reader.readers.transformers + +import java.util.UUID + +import com.expedia.open.tracing.{Span, Tag} +import com.expedia.www.haystack.trace.commons.utils.SpanUtils +import com.expedia.www.haystack.trace.reader.readers.utils.MutableSpanForest + +/** + * + * If there are multiple roots in the given trace, use the first root based on startTime to be root + * mark other roots as children of the selected root + * If there is no root, assume loopback span or first span in time order to be root + * + * **Apply this transformer only if you are not confident about clients sending in roots properly** + */ +class InvalidRootTransformer extends SpanTreeTransformer { + private val AUTOGEN_REASON = + """ + |This span is autogenerated by haystack and only a UI sugar to show multiple root spans together in one view. + | This is a symptom that few spans have empty parent id, but only one such root span should exist. + """.stripMargin + + override def transform(spanForest: MutableSpanForest): MutableSpanForest = { + val rootSpans = spanForest.getAllTrees.filter(_.span.getParentSpanId.isEmpty).map(_.span) + + rootSpans.size match { + case 0 => toTraceWithAssumedRoot(spanForest) + case 1 => spanForest + case _ => toTraceWithSingleRoot(spanForest, rootSpans.size) + } + } + + private def toTraceWithAssumedRoot(forest: MutableSpanForest): MutableSpanForest = { + // if we have just one tree, then simply set it's root's parent spanId as empty + if (forest.countTrees <= 1) { + return forest.updateEachSpanTreeRoot(resetParentSpanId) + } + + // if we have just 1 loopback tree root, which means its parentSpanId is the same as its spanId, then mark it as a root + // by setting its parentSpanId as empty + val loopbackTrees = forest.treesWithLoopbackRoots + if (loopbackTrees.size == 1) { + return forest.updateEachSpanTreeRoot(span => if (loopbackTrees.head.span == span) resetParentSpanId(span) else span) + } + + // for all other cases, get the root with the minimum startTime and make it as a root by setting parentSpanId as empty + val spanWithMinStartTime = forest.getAllTrees.minBy(_.span.getStartTime).span + forest.updateEachSpanTreeRoot(span => if (span == spanWithMinStartTime) resetParentSpanId(span) else span) + } + + private def toTraceWithSingleRoot(forest: MutableSpanForest, emptyParentIdSpanTrees: Int): MutableSpanForest = { + val allTreeRootSpans: Seq[Span] = forest.getAllTrees.map(_.span) + val newRootSpan = SpanUtils.addClientLogTag(SpanUtils + .createAutoGeneratedRootSpan(allTreeRootSpans, AUTOGEN_REASON, UUID.randomUUID().toString) + .addTags(Tag.newBuilder().setKey("X-HAYSTACK-SPAN-ROOT-COUNT").setVLong(emptyParentIdSpanTrees).setType(Tag.TagType.LONG)) + .build()) + + forest.addNewRoot(newRootSpan) + } + + private def resetParentSpanId(span: Span): Span = Span.newBuilder(span).setParentSpanId("").build() +} diff --git a/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/transformers/OrphanedTraceTransformer.scala b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/transformers/OrphanedTraceTransformer.scala new file mode 100644 index 000000000..cc66ffbff --- /dev/null +++ b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/transformers/OrphanedTraceTransformer.scala @@ -0,0 +1,62 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expedia.www.haystack.trace.reader.readers.transformers + +import com.expedia.open.tracing.Span +import com.expedia.www.haystack.trace.commons.utils.SpanUtils +import com.expedia.www.haystack.trace.reader.readers.utils.{MutableSpanForest, SpanTree} + + +/** + * If the root span is missing within a trace, create a pseudo root span to wrap all the spans. + * + * ** [[com.expedia.www.haystack.trace.reader.readers.validators.RootValidator]] and [[com.expedia.www.haystack.trace.reader.readers.validators.ParentIdValidator]] must be turned off for this to take into effect. ** + * ** Should place first in the POST transformers sequence of the configuration, as the other transformers may depend on or use the generated root during their transformation. ** + */ +object OrphanedTraceTransformerConstants { + val AUTO_GEN_REASON = "Missing root span" +} + +class OrphanedTraceTransformer extends SpanTreeTransformer { + + override def transform(forest: MutableSpanForest): MutableSpanForest = { + val orphanedTrees = forest.orphanedTrees() + if (orphanedTrees.isEmpty) { + forest + } else if (multipleOrphans(orphanedTrees)) { + forest.updateUnderlyingSpans(Seq.empty) + } else { + val rootSpan = generateRootSpan(forest.getUnderlyingSpans) + forest.addNewRoot(rootSpan) + } + } + + def multipleOrphans(orphanedTrees: Seq[SpanTree]): Boolean = { + val orphanedParents = orphanedTrees.groupBy(_.span.getParentSpanId) + if (orphanedParents.size != 1) return true + + // we may now have multiple orphaned trees but each tree's root span has the same parentId + // if this parentId is same as traceId, then we will not call them as multipleOrphans as + // we will build an autogenerated span as their parent + val orphanedSpan = orphanedParents.head._2.head.span + orphanedSpan.getParentSpanId != orphanedSpan.getTraceId + } + + def generateRootSpan(spans: Seq[Span]): Span = { + SpanUtils.createAutoGeneratedRootSpan(spans, OrphanedTraceTransformerConstants.AUTO_GEN_REASON, spans.head.getTraceId).build() + } +} diff --git a/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/transformers/PartialSpanTransformer.scala b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/transformers/PartialSpanTransformer.scala new file mode 100644 index 000000000..dfc90ed5d --- /dev/null +++ b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/transformers/PartialSpanTransformer.scala @@ -0,0 +1,39 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expedia.www.haystack.trace.reader.readers.transformers + +import com.expedia.open.tracing.Span +import com.expedia.www.haystack.trace.reader.readers.utils._ + +/** + * Merges partial spans and generates a single Span combining a client and corresponding server span + * gracefully fallback to collapse to a single span if there are multiple or missing client/server spans + */ +class PartialSpanTransformer extends SpanTreeTransformer { + override def transform(spanForest: MutableSpanForest): MutableSpanForest = { + var hasAnySpanMerged = false + + val mergedSpans: Seq[Span] = spanForest.getUnderlyingSpans.groupBy(_.getSpanId).map((pair) => pair._2 match { + case Seq(span: Span) => span + case spans: Seq[Span] => + hasAnySpanMerged = true + SpanMerger.mergeSpans(spans) + }).toSeq + + spanForest.updateUnderlyingSpans(mergedSpans, hasAnySpanMerged) + } +} diff --git a/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/transformers/PostTraceTransformationHandler.scala b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/transformers/PostTraceTransformationHandler.scala new file mode 100644 index 000000000..9688139f2 --- /dev/null +++ b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/transformers/PostTraceTransformationHandler.scala @@ -0,0 +1,50 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expedia.www.haystack.trace.reader.readers.transformers + +import com.expedia.open.tracing.api.Trace +import com.expedia.www.haystack.trace.reader.readers.utils.MutableSpanForest + +import scala.collection.JavaConverters._ + +/** + * takes a sequence of [[SpanTreeTransformer]] and apply transform functions on the chain + * + * transformer functions takes [[MutableSpanForest]] and generates a transformed [[MutableSpanForest]] + * [[PostTraceTransformationHandler]] takes a [[Seq]] of [[SpanTreeTransformer]] and applies chaining on them, + * providing response [[List]] of a transformer to the next one + * + * @param transformerSeq + */ +class PostTraceTransformationHandler(transformerSeq: Seq[SpanTreeTransformer]) { + private val transformerChain = + Function.chain(transformerSeq.foldLeft(Seq[MutableSpanForest => MutableSpanForest]())((seq, transformer) => seq :+ transformer.transform _)) + + def transform(trace: Trace): Trace = { + // build a span forest from the given spans in a trace + val spanForest = MutableSpanForest(trace.getChildSpansList.asScala) + + // transform the forest and yeild only one tree + val transformedSpanForest = transformerChain(spanForest) + + Trace + .newBuilder() + .setTraceId(trace.getTraceId) + .addAllChildSpans(transformedSpanForest.getUnderlyingSpans.asJavaCollection) + .build() + } +} diff --git a/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/transformers/ServerClientSpanMergeTransformer.scala b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/transformers/ServerClientSpanMergeTransformer.scala new file mode 100644 index 000000000..73e512585 --- /dev/null +++ b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/transformers/ServerClientSpanMergeTransformer.scala @@ -0,0 +1,41 @@ +/* + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expedia.www.haystack.trace.reader.readers.transformers + +import com.expedia.open.tracing.Span +import com.expedia.www.haystack.trace.reader.readers.utils.{MutableSpanForest, SpanMerger} + +class ServerClientSpanMergeTransformer extends SpanTreeTransformer { + + private def shouldMerge(parentSpan: Span, childSpan: Span) = { + childSpan.getServiceName != parentSpan.getServiceName && + !SpanMerger.isAlreadyMergedSpan(parentSpan) && + !SpanMerger.isAlreadyMergedSpan(childSpan) && + SpanMerger.shouldMergeSpanKinds(parentSpan, childSpan) + } + + override def transform(spanForest: MutableSpanForest): MutableSpanForest = { + spanForest.collapse((tree) => + tree.children match { + case Seq(singleChild) if shouldMerge(tree.span, singleChild.span) => + Some(SpanMerger.mergeParentChildSpans(tree.span, singleChild.span)) + case _ => None + }) + + spanForest + } +} diff --git a/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/transformers/SortSpanTransformer.scala b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/transformers/SortSpanTransformer.scala new file mode 100644 index 000000000..ab2a4cbac --- /dev/null +++ b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/transformers/SortSpanTransformer.scala @@ -0,0 +1,36 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expedia.www.haystack.trace.reader.readers.transformers + +import com.expedia.www.haystack.trace.reader.readers.utils.MutableSpanForest + +/** + * Orders spans in natural ordering - root followed by other spans ordered by start time + * + * Assumes there is only one root in give spans List + * corresponding validations are done in [[com.expedia.www.haystack.trace.reader.readers.validators.RootValidator]] + * corresponding transformation are done in [[InvalidRootTransformer]] + */ +class SortSpanTransformer extends SpanTreeTransformer { + override def transform(spanForest: MutableSpanForest): MutableSpanForest = { + require(spanForest.getAllTrees.size <= 1) + + val (left, right) = spanForest.getUnderlyingSpans.partition(_.getParentSpanId.isEmpty) + val sortedSpans = left.toList.head :: right.toList.sortBy(_.getStartTime) + spanForest.updateUnderlyingSpans(sortedSpans, triggerForestUpdate = false) + } +} diff --git a/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/transformers/SpanTreeTransformer.scala b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/transformers/SpanTreeTransformer.scala new file mode 100644 index 000000000..8fdc24b1e --- /dev/null +++ b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/transformers/SpanTreeTransformer.scala @@ -0,0 +1,23 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expedia.www.haystack.trace.reader.readers.transformers + +import com.expedia.www.haystack.trace.reader.readers.utils.MutableSpanForest + +trait SpanTreeTransformer { + def transform(forest: MutableSpanForest): MutableSpanForest +} diff --git a/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/transformers/TraceTransformationHandler.scala b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/transformers/TraceTransformationHandler.scala new file mode 100644 index 000000000..7d8bd4b6a --- /dev/null +++ b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/transformers/TraceTransformationHandler.scala @@ -0,0 +1,46 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expedia.www.haystack.trace.reader.readers.transformers + +import com.expedia.open.tracing.Span +import com.expedia.open.tracing.api.Trace + +import scala.collection.JavaConverters._ + +/** + * takes a sequence of [[TraceTransformer]] and apply transform functions on the chain + * + * transformer functions takes [[Seq]] of [[Span]]s and generates a [[Seq]] of [[Span]]s + * [[TraceTransformationHandler]] takes a [[Seq]] of [[TraceTransformer]] and applies chaining on them, + * providing response [[List]] of a transformer to the next one + * + * @param transformerSeq + */ +class TraceTransformationHandler(transformerSeq: Seq[TraceTransformer]) { + private val transformerChain = + Function.chain(transformerSeq.foldLeft(Seq[Seq[Span] => Seq[Span]]())((seq, transformer) => seq :+ transformer.transform _)) + + def transform(trace: Trace): Trace = { + val transformedSpans = transformerChain(trace.getChildSpansList.asScala) + + Trace + .newBuilder() + .setTraceId(trace.getTraceId) + .addAllChildSpans(transformedSpans.asJavaCollection) + .build() + } +} diff --git a/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/transformers/TraceTransformer.scala b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/transformers/TraceTransformer.scala new file mode 100644 index 000000000..377695ab6 --- /dev/null +++ b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/transformers/TraceTransformer.scala @@ -0,0 +1,23 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expedia.www.haystack.trace.reader.readers.transformers + +import com.expedia.open.tracing.Span + +trait TraceTransformer { + def transform(spans: Seq[Span]): Seq[Span] +} diff --git a/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/utils/AuxiliaryTags.scala b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/utils/AuxiliaryTags.scala new file mode 100644 index 000000000..a77d834c4 --- /dev/null +++ b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/utils/AuxiliaryTags.scala @@ -0,0 +1,42 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expedia.www.haystack.trace.reader.readers.utils + +object AuxiliaryTags { + val INFRASTRUCTURE_LOCATION = "X-HAYSTACK-AWS-REGION" + val INFRASTRUCTURE_PROVIDER = "X-HAYSTACK-INFRASTRUCTURE-PROVIDER" + + val IS_MERGED_SPAN = "X-HAYSTACK-IS-MERGED-SPAN" + val NETWORK_DELTA = "X-HAYSTACK-NETWORK-DELTA" + + val CLIENT_SERVICE_NAME = "X-HAYSTACK-CLIENT-SERVICE-NAME" + val CLIENT_OPERATION_NAME = "X-HAYSTACK-CLIENT-OPERATION-NAME" + val CLIENT_SPAN_ID = "X-HAYSTACK-CLIENT-SPAN-ID" + val CLIENT_INFRASTRUCTURE_PROVIDER = "X-HAYSTACK-CLIENT-INFRASTRUCTURE-PROVIDER" + val CLIENT_INFRASTRUCTURE_LOCATION = "X-HAYSTACK-CLIENT-INFRASTRUCTURE-LOCATION" + val CLIENT_START_TIME = "X-HAYSTACK-CLIENT-START-TIME" + val CLIENT_DURATION = "X-HAYSTACK-CLIENT-DURATION" + + val SERVER_SERVICE_NAME = "X-HAYSTACK-SERVER-SERVICE-NAME" + val SERVER_OPERATION_NAME = "X-HAYSTACK-SERVER-OPERATION-NAME" + val SERVER_INFRASTRUCTURE_PROVIDER = "X-HAYSTACK-SERVER-INFRASTRUCTURE-PROVIDER" + val SERVER_INFRASTRUCTURE_LOCATION = "X-HAYSTACK-SERVER-INFRASTRUCTURE-LOCATION" + val SERVER_START_TIME = "X-HAYSTACK-SERVER-START-TIME" + val SERVER_DURATION = "X-HAYSTACK-SERVER-DURATION" + + val ERR_IS_MULTI_PARTIAL_SPAN = "X-HAYSTACK-ERR-IS-MULTI-PARTIAL-SPAN" +} diff --git a/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/utils/SpanMerger.scala b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/utils/SpanMerger.scala new file mode 100644 index 000000000..78d2de704 --- /dev/null +++ b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/utils/SpanMerger.scala @@ -0,0 +1,158 @@ +/* + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expedia.www.haystack.trace.reader.readers.utils + +import com.expedia.open.tracing.{Span, Tag} +import com.expedia.www.haystack.trace.commons.utils.{SpanMarkers, SpanUtils} +import com.expedia.www.haystack.trace.reader.readers.utils.TagBuilders.{buildBoolTag, buildLongTag, buildStringTag} +import com.expedia.www.haystack.trace.reader.readers.utils.TagExtractors.extractTagStringValue + +import scala.collection.JavaConverters._ + +object SpanMerger { + + def mergeParentChildSpans(parentSpan: Span, childSpan: Span): Span = { + val clientSpan = if (SpanUtils.containsClientLogTag(parentSpan)) parentSpan else SpanUtils.addClientLogTag(parentSpan) + val serverSpan = if (SpanUtils.containsServerLogTag(childSpan)) childSpan else SpanUtils.addServerLogTag(childSpan) + merge(clientSpan, serverSpan) + } + + def mergeSpans(spans: Seq[Span]): Span = { + val serverSpanOptional = collapseSpans(spans.filter(SpanUtils.containsServerLogTag)) + val clientSpanOptional = collapseSpans(spans.filter(SpanUtils.containsClientLogTag)) + (clientSpanOptional, serverSpanOptional) match { + // ideally there should be one server and one client span + // merging these partial spans to form a new single span + case (Some(clientSpan), Some(serverSpan)) => merge(clientSpan, serverSpan) + + // imperfect scenario, fallback to return available server span + case (None, Some(serverSpan)) => serverSpan + + // imperfect scenario, fallback to return available client span + case (Some(clientSpan), None) => clientSpan + + // imperfect scenario, fallback to collapse all spans + case _ => collapseSpans(spans).get + } + } + + private def merge(clientSpan: Span, serverSpan: Span): Span = { + Span + .newBuilder(serverSpan) + .setParentSpanId(clientSpan.getParentSpanId) // use the parentSpanId of the client span to stitch in the client's trace tree + .addAllTags((clientSpan.getTagsList.asScala + ++ auxiliaryCommonTags(clientSpan, serverSpan) + ++ auxiliaryClientTags(clientSpan) + ++ auxiliaryServerTags(serverSpan)).asJavaCollection) + .clearLogs().addAllLogs((clientSpan.getLogsList.asScala + ++ serverSpan.getLogsList.asScala.sortBy(_.getTimestamp)).asJavaCollection) + .build() + } + + // collapse all spans of a type(eg. client or server) if needed, + // ideally there would be just one span in the list and hence no need of collapsing + private def collapseSpans(spans: Seq[Span]): Option[Span] = { + spans match { + case Nil => None + case Seq(span) => Some(span) + case _ => + // if there are multiple spans fallback to collapse all the spans in a single span + // start the collapsed span from startTime of the first and end at ending of last such span + // also add an error marker in the collapsed span + val firstSpan = spans.minBy(_.getStartTime) + val lastSpan = spans.maxBy(span => span.getStartTime + span.getDuration) + val allTags = spans.flatMap(span => span.getTagsList.asScala) + val allLogs = spans.flatMap(span => span.getLogsList.asScala) + val opName = spans.map(_.getOperationName).reduce((a, b) => a + " & " + b) + + Some( + Span + .newBuilder(firstSpan) + .setOperationName(opName) + .setDuration(lastSpan.getStartTime + lastSpan.getDuration - firstSpan.getStartTime) + .clearTags().addAllTags(allTags.asJava) + .addTags(buildBoolTag(AuxiliaryTags.ERR_IS_MULTI_PARTIAL_SPAN, tagValue = true)) + .clearLogs().addAllLogs(allLogs.asJava) + .build()) + } + } + + // Network delta - difference between server and client duration + // calculate only if serverDuration is smaller then client + private def calculateNetworkDelta(clientSpan: Span, serverSpan: Span): Option[Long] = { + val clientDuration = SpanUtils.getEventTimestamp(clientSpan, SpanMarkers.CLIENT_RECV_EVENT) - SpanUtils.getEventTimestamp(clientSpan, SpanMarkers.CLIENT_SEND_EVENT) + val serverDuration = SpanUtils.getEventTimestamp(serverSpan, SpanMarkers.SERVER_SEND_EVENT) - SpanUtils.getEventTimestamp(serverSpan, SpanMarkers.SERVER_RECV_EVENT) + + // difference of duration of spans + if (serverDuration < clientDuration) { + Some(clientDuration - serverDuration) + } else { + None + } + } + + private def auxiliaryCommonTags(clientSpan: Span, serverSpan: Span): List[Tag] = + List( + buildBoolTag(AuxiliaryTags.IS_MERGED_SPAN, tagValue = true), + buildLongTag(AuxiliaryTags.NETWORK_DELTA, calculateNetworkDelta(clientSpan, serverSpan).getOrElse(-1)) + ) + + private def auxiliaryClientTags(span: Span): List[Tag] = + List( + buildStringTag(AuxiliaryTags.CLIENT_SERVICE_NAME, span.getServiceName), + buildStringTag(AuxiliaryTags.CLIENT_OPERATION_NAME, span.getOperationName), + buildStringTag(AuxiliaryTags.CLIENT_SPAN_ID, span.getSpanId), + buildStringTag(AuxiliaryTags.CLIENT_INFRASTRUCTURE_PROVIDER, extractTagStringValue(span, AuxiliaryTags.INFRASTRUCTURE_PROVIDER)), + buildStringTag(AuxiliaryTags.CLIENT_INFRASTRUCTURE_LOCATION, extractTagStringValue(span, AuxiliaryTags.INFRASTRUCTURE_LOCATION)), + buildLongTag(AuxiliaryTags.CLIENT_START_TIME, span.getStartTime), + buildLongTag(AuxiliaryTags.CLIENT_DURATION, span.getDuration) + ) + + private def auxiliaryServerTags(span: Span): List[Tag] = { + List( + buildStringTag(AuxiliaryTags.SERVER_SERVICE_NAME, span.getServiceName), + buildStringTag(AuxiliaryTags.SERVER_OPERATION_NAME, span.getOperationName), + buildStringTag(AuxiliaryTags.SERVER_INFRASTRUCTURE_PROVIDER, extractTagStringValue(span, AuxiliaryTags.INFRASTRUCTURE_PROVIDER)), + buildStringTag(AuxiliaryTags.SERVER_INFRASTRUCTURE_LOCATION, extractTagStringValue(span, AuxiliaryTags.INFRASTRUCTURE_LOCATION)), + buildLongTag(AuxiliaryTags.SERVER_START_TIME, span.getStartTime), + buildLongTag(AuxiliaryTags.SERVER_DURATION, span.getDuration) + ) + } + + private def isProducerConsumerSpanKind(spanKind: String): Boolean = { + "producer".equalsIgnoreCase(spanKind) || "consumer".equalsIgnoreCase(spanKind) + } + + def isAlreadyMergedSpan(span: Span): Boolean = { + span.getTagsList.asScala.exists(tag => tag.getKey.equals(AuxiliaryTags.IS_MERGED_SPAN)) + } + + def shouldMergeSpanKinds(spanA: Span, spanB: Span): Boolean = { + val spanAKind = SpanUtils.spanKind(spanA) + val spanBKind = SpanUtils.spanKind(spanB) + // if we find the span kind correctly(non-empty), then return false if they are same + // for all other cases, return true. + // also dont merge the spans with 'producer' and 'consumer' span.kind + if ((spanAKind != "" && spanBKind != "" && spanAKind == spanBKind) || + isProducerConsumerSpanKind(spanAKind) || + isProducerConsumerSpanKind(spanBKind)) { + false + } else { + true + } + } +} diff --git a/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/utils/SpanTree.scala b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/utils/SpanTree.scala new file mode 100644 index 000000000..d537d97fb --- /dev/null +++ b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/utils/SpanTree.scala @@ -0,0 +1,160 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expedia.www.haystack.trace.reader.readers.utils + +import com.expedia.open.tracing.Span + +import scala.collection.mutable +import scala.collection.mutable.ListBuffer + +case class MutableSpanForest(private var spans: Seq[Span]) { + private var forest: mutable.ListBuffer[SpanTree] = _ + private var needForestUpdate = true + + def getAllTrees: Seq[SpanTree] = { + if (needForestUpdate) reCreateForest() + forest + } + + def countTrees: Int = getAllTrees.size + + def treesWithLoopbackRoots: Seq[SpanTree] = getAllTrees.filter(tree => tree.span.getSpanId == tree.span.getParentSpanId) + + def getUnderlyingSpans: Seq[Span] = this.spans + + def updateUnderlyingSpans(spans: Seq[Span], triggerForestUpdate: Boolean = true): MutableSpanForest = { + this.spans = spans + if (triggerForestUpdate) needForestUpdate = true + this + } + + def orphanedTrees(): Seq[SpanTree] = getAllTrees.filter(_.span.getParentSpanId.nonEmpty) + + def addNewRoot(rootSpan: Span): MutableSpanForest = { + val newTree = SpanTree(rootSpan) + mergeTreesUnder(newTree) + spans = spans :+ rootSpan + forest = mutable.ListBuffer(newTree) + needForestUpdate = false + this + } + + def mergeTreesUnder(root: SpanTree): MutableSpanForest = { + val toBeMergedTrees = forest.filter(_ != root) + + val toBeUpdatedUnderlyingSpans = mutable.ListBuffer[(Span, Span)]() + toBeMergedTrees.foreach(tree => { + val originalSpan = tree.span + val updatedSpan = Span.newBuilder(originalSpan).setParentSpanId(root.span.getSpanId).build() + toBeUpdatedUnderlyingSpans += ((originalSpan, updatedSpan)) + tree.span = updatedSpan + root.children += tree + }) + + updateUnderlyingSpanWith(toBeUpdatedUnderlyingSpans) + this.forest = mutable.ListBuffer[SpanTree](root) + needForestUpdate = false + this + } + + def updateEachSpanTreeRoot(updateFunc: (Span) => Span): MutableSpanForest = { + val toBeUpdatedUnderlyingSpans = mutable.ListBuffer[(Span, Span)]() + + for (tree <- getAllTrees) { + val originalSpan = tree.span + val updatedSpan = updateFunc(originalSpan) + if (originalSpan != updatedSpan) { + tree.span = updatedSpan + toBeUpdatedUnderlyingSpans += ((originalSpan, updatedSpan)) + } + } + updateUnderlyingSpanWith(toBeUpdatedUnderlyingSpans) + + this + } + + private def reCreateForest() = { + this.forest = mutable.ListBuffer[SpanTree]() + if (this.spans.nonEmpty) { + val spanIdTreeMap = mutable.HashMap[String, SpanTree]() + val possibleRoots = mutable.HashSet[String]() + + spans.foreach { + span => + spanIdTreeMap.put(span.getSpanId, SpanTree(span)) + possibleRoots.add(span.getSpanId) + } + + for (span <- spans; + parentTree <- spanIdTreeMap.get(span.getParentSpanId)) { + val self = spanIdTreeMap(span.getSpanId) + if (parentTree != self) { + parentTree.children += self + possibleRoots.remove(span.getSpanId) + } + } + + spanIdTreeMap.foreach { + case (spanId, tree) => if (possibleRoots.contains(spanId)) this.forest += tree + } + } + needForestUpdate = false + } + + private def updateUnderlyingSpanWith(updateList: ListBuffer[(Span, Span)]) = { + if (updateList.nonEmpty) { + // update the underlying spans + this.spans = this.spans.map(span => { + updateList.find { + case (curr, _) => curr == span + } match { + case Some((_, ne)) => ne + case _ => span + } + }) + } + } + + def collapse(applyCondition: (SpanTree) => Option[Span]): Unit = { + val underlyingSpans = mutable.ListBuffer[Span]() + + def collapseTree(spanTree: SpanTree): Unit = { + val queue = mutable.Queue[SpanTree]() + queue.enqueue(spanTree) + + while (queue.nonEmpty) { + val tree = queue.dequeue() + applyCondition(tree) match { + case Some(mergedSpan) => + tree.span = mergedSpan + val childSpanTrees = new ListBuffer[SpanTree]() + tree.children.foreach(t => childSpanTrees.appendAll(t.children)) + tree.children.clear() + childSpanTrees.foreach(tr => tree.children.append(tr)) + case _ => + } + underlyingSpans.append(tree.span) + queue.enqueue(tree.children:_*) + } + } + + getAllTrees.foreach(collapseTree) + updateUnderlyingSpans(underlyingSpans, triggerForestUpdate = false) + } +} + +case class SpanTree(var span: Span, children: mutable.ListBuffer[SpanTree] = mutable.ListBuffer[SpanTree]()) diff --git a/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/utils/TagBuilders.scala b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/utils/TagBuilders.scala new file mode 100644 index 000000000..8c03bb0b0 --- /dev/null +++ b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/utils/TagBuilders.scala @@ -0,0 +1,43 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expedia.www.haystack.trace.reader.readers.utils + +import com.expedia.open.tracing.Tag + +object TagBuilders { + def buildStringTag(tagKey: String, tagValue: String): Tag = + Tag.newBuilder() + .setKey(tagKey) + .setType(Tag.TagType.STRING) + .setVStr(tagValue) + .build() + + def buildBoolTag(tagKey: String, tagValue: Boolean): Tag = + Tag.newBuilder() + .setKey(tagKey) + .setType(Tag.TagType.BOOL) + .setVBool(tagValue) + .build() + + def buildLongTag(tagKey: String, tagValue: Long): Tag = + Tag.newBuilder() + .setKey(tagKey) + .setType(Tag.TagType.LONG) + .setVLong(tagValue) + .build() + +} diff --git a/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/utils/TagExtractors.scala b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/utils/TagExtractors.scala new file mode 100644 index 000000000..074ddde8e --- /dev/null +++ b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/utils/TagExtractors.scala @@ -0,0 +1,40 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expedia.www.haystack.trace.reader.readers.utils + +import com.expedia.open.tracing.Span +import scala.collection.JavaConverters._ + +object TagExtractors { + def containsTag(span: Span, tagKey: String): Boolean = { + span.getTagsList.asScala.exists(_.getKey == tagKey) + } + + def extractTagStringValue(span: Span, tagKey: String): String = { + span.getTagsList.asScala.find(_.getKey == tagKey) match { + case Some(t) => t.getVStr + case _ => "" + } + } + + def extractTagLongValue(span: Span, tagKey: String): Long = { + span.getTagsList.asScala.find(_.getKey == tagKey) match { + case Some(t) => t.getVLong + case _ => -1 + } + } +} diff --git a/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/utils/TraceMerger.scala b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/utils/TraceMerger.scala new file mode 100644 index 000000000..f5264f546 --- /dev/null +++ b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/utils/TraceMerger.scala @@ -0,0 +1,35 @@ +/* + * Copyright 2019 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expedia.www.haystack.trace.reader.readers.utils + +import com.expedia.open.tracing.api.Trace + +object TraceMerger { + def merge(traces: Seq[Trace]): Seq[Trace] = { + traces.groupBy(_.getTraceId).mapValues { + seq => { + if (seq.size == 1) { + seq.head + } else { + val head = seq.head.toBuilder + seq.tail.foreach(t => head.addAllChildSpans(t.getChildSpansList)) + head.build() + } + } + }.values.toSeq + } +} diff --git a/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/validators/ParentIdValidator.scala b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/validators/ParentIdValidator.scala new file mode 100644 index 000000000..085744b2d --- /dev/null +++ b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/validators/ParentIdValidator.scala @@ -0,0 +1,43 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expedia.www.haystack.trace.reader.readers.validators + +import com.expedia.open.tracing.api.Trace +import com.expedia.www.haystack.trace.reader.exceptions.InvalidTraceException + +import scala.collection.JavaConverters._ +import scala.util.{Failure, Success, Try} + +/** + * validates if spans in the trace has a valid parentIds + * assumes that traceId is a non-empty string and there is a single root, apply [[TraceIdValidator]] and [[RootValidator]] to make sure + */ +class ParentIdValidator extends TraceValidator { + override def validate(trace: Trace): Try[Trace] = { + val spans = trace.getChildSpansList.asScala + val spanIdSet = spans.map(_.getSpanId).toSet + + if (!spans.forall(sp => spanIdSet.contains(sp.getParentSpanId) || sp.getParentSpanId.isEmpty)) { + Failure(new InvalidTraceException(s"spans without valid parent found for traceId=${spans.head.getTraceId}")) + } else if (!spans.forall(sp => sp.getSpanId != sp.getParentSpanId)) { + Failure(new InvalidTraceException(s"same parent and span id found for one ore more span for traceId=${spans.head.getTraceId}")) + } + else { + Success(trace) + } + } +} diff --git a/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/validators/RootValidator.scala b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/validators/RootValidator.scala new file mode 100644 index 000000000..ffee3121a --- /dev/null +++ b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/validators/RootValidator.scala @@ -0,0 +1,39 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expedia.www.haystack.trace.reader.readers.validators + +import com.expedia.open.tracing.api.Trace +import com.expedia.www.haystack.trace.reader.exceptions.InvalidTraceException + +import scala.collection.JavaConverters._ +import scala.util.{Failure, Success, Try} + +/** + * validates if the trace has a single root or not + * assumes that traceId is a non-empty string, apply [[TraceIdValidator]] to make sure + */ +class RootValidator extends TraceValidator { + override def validate(trace: Trace): Try[Trace] = { + val roots = trace.getChildSpansList.asScala.filter(_.getParentSpanId.isEmpty).map(_.getSpanId).toSet + + if (roots.size != 1) { + Failure(new InvalidTraceException(s"found ${roots.size} roots with spanIDs=${roots.mkString(",")} and traceID=${trace.getTraceId}")) + } else { + Success(trace) + } + } +} diff --git a/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/validators/TraceIdValidator.scala b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/validators/TraceIdValidator.scala new file mode 100644 index 000000000..cfcdb16f6 --- /dev/null +++ b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/validators/TraceIdValidator.scala @@ -0,0 +1,37 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expedia.www.haystack.trace.reader.readers.validators + +import com.expedia.open.tracing.api.Trace +import com.expedia.www.haystack.trace.reader.exceptions.InvalidTraceException + +import scala.collection.JavaConverters._ +import scala.util.{Failure, Success, Try} + +/** + * validates if the traceId is non empty and consistent for all the spans + */ +class TraceIdValidator extends TraceValidator { + override def validate(trace: Trace): Try[Trace] = + if (trace.getTraceId.isEmpty) { + Failure(new InvalidTraceException("invalid traceId")) + } else if (!trace.getChildSpansList.asScala.forall(_.getTraceId == trace.getTraceId)) { + Failure(new InvalidTraceException(s"span with different traceId are not allowed for traceId=${trace.getTraceId}")) + } else { + Success(trace) + } +} diff --git a/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/validators/TraceValidationHandler.scala b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/validators/TraceValidationHandler.scala new file mode 100644 index 000000000..ce16adac4 --- /dev/null +++ b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/validators/TraceValidationHandler.scala @@ -0,0 +1,36 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expedia.www.haystack.trace.reader.readers.validators + +import com.expedia.open.tracing.api.Trace + +import scala.util.{Success, Try} + +/** + * takes a sequence of [[TraceValidator]] and apply validations on the trace + * will either return Success or Failure with the first failed validation as exception + * + * @param validatorSeq sequence of validations to apply on the trace + */ +class TraceValidationHandler(validatorSeq: Seq[TraceValidator]) { + def validate(trace: Trace): Try[Trace] = { + validatorSeq + .map(_.validate(trace)) + .find(_.isFailure) + .getOrElse(Success(trace)) + } +} diff --git a/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/validators/TraceValidator.scala b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/validators/TraceValidator.scala new file mode 100644 index 000000000..4bb3c065e --- /dev/null +++ b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/readers/validators/TraceValidator.scala @@ -0,0 +1,25 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expedia.www.haystack.trace.reader.readers.validators + +import com.expedia.open.tracing.api.Trace + +import scala.util.Try + +trait TraceValidator { + def validate(trace: Trace): Try[Trace] +} diff --git a/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/services/GrpcHandler.scala b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/services/GrpcHandler.scala new file mode 100644 index 000000000..358a8711a --- /dev/null +++ b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/services/GrpcHandler.scala @@ -0,0 +1,64 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expedia.www.haystack.trace.reader.services + +import com.expedia.www.haystack.commons.metrics.MetricsSupport +import com.google.protobuf.GeneratedMessageV3 +import io.grpc.Status +import io.grpc.stub.StreamObserver +import org.slf4j.{Logger, LoggerFactory} + +import scala.concurrent.{ExecutionContextExecutor, Future} +import scala.util.{Failure, Success} + +object GrpcHandler { + protected val LOGGER: Logger = LoggerFactory.getLogger(classOf[GrpcHandler]) +} + +/** + * Handler for Grpc response + * populates responseObserver with response object or error accordingly + * takes care of corresponding logging and updating counters + * + * @param operationName : name of operation + * @param executor : executor service on which handler is invoked + */ + +class GrpcHandler(operationName: String)(implicit val executor: ExecutionContextExecutor) extends MetricsSupport { + private val metricFriendlyOperationName = operationName.replace('/', '.') + private val timer = metricRegistry.timer(metricFriendlyOperationName) + private val failureMeter = metricRegistry.meter(s"$metricFriendlyOperationName.failures") + + import GrpcHandler._ + + def handle[Rs](request: GeneratedMessageV3, responseObserver: StreamObserver[Rs])(op: => Future[Rs]): Unit = { + val time = timer.time() + op onComplete { + case Success(response) => + responseObserver.onNext(response) + responseObserver.onCompleted() + time.stop() + LOGGER.debug(s"service invocation for operation=$operationName and request=${request.toString} completed successfully") + + case Failure(ex) => + responseObserver.onError(Status.fromThrowable(ex).asRuntimeException()) + failureMeter.mark() + time.stop() + LOGGER.error(s"service invocation for operation=$operationName and request=${request.toString} failed with error", ex) + } + } +} diff --git a/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/services/GrpcHealthService.scala b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/services/GrpcHealthService.scala new file mode 100644 index 000000000..9299b5bc0 --- /dev/null +++ b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/services/GrpcHealthService.scala @@ -0,0 +1,31 @@ +/* + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expedia.www.haystack.trace.reader.services + +import io.grpc.health.v1.{HealthCheckRequest, HealthCheckResponse, HealthGrpc} +import io.grpc.stub.StreamObserver + +class GrpcHealthService extends HealthGrpc.HealthImplBase { + + override def check(request: HealthCheckRequest, responseObserver: StreamObserver[HealthCheckResponse]): Unit = { + responseObserver.onNext(HealthCheckResponse + .newBuilder() + .setStatus(HealthCheckResponse.ServingStatus.SERVING) + .build()) + responseObserver.onCompleted() + } +} diff --git a/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/services/TraceService.scala b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/services/TraceService.scala new file mode 100644 index 000000000..c95990c8c --- /dev/null +++ b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/services/TraceService.scala @@ -0,0 +1,135 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expedia.www.haystack.trace.reader.services + +import com.expedia.open.tracing.Span +import com.expedia.open.tracing.api._ +import com.expedia.www.haystack.trace.reader.config.entities.{TraceTransformersConfiguration, TraceValidatorsConfiguration} +import com.expedia.www.haystack.trace.reader.readers.TraceReader +import com.expedia.www.haystack.trace.reader.stores.TraceStore +import io.grpc.stub.StreamObserver + +import scala.concurrent.ExecutionContextExecutor + +class TraceService(traceStore: TraceStore, + validatorsConfig: TraceValidatorsConfiguration, + transformersConfig: TraceTransformersConfiguration) + (implicit val executor: ExecutionContextExecutor) extends TraceReaderGrpc.TraceReaderImplBase { + + private val handleGetTraceResponse = new GrpcHandler(TraceReaderGrpc.METHOD_GET_TRACE.getFullMethodName) + private val handleGetRawTraceResponse = new GrpcHandler(TraceReaderGrpc.METHOD_GET_RAW_TRACE.getFullMethodName) + private val handleGetRawSpanResponse = new GrpcHandler(TraceReaderGrpc.METHOD_GET_RAW_SPAN.getFullMethodName) + private val handleSearchResponse = new GrpcHandler(TraceReaderGrpc.METHOD_SEARCH_TRACES.getFullMethodName) + private val handleFieldNamesResponse = new GrpcHandler(TraceReaderGrpc.METHOD_GET_FIELD_NAMES.getFullMethodName) + private val handleFieldValuesResponse = new GrpcHandler(TraceReaderGrpc.METHOD_GET_FIELD_VALUES.getFullMethodName) + private val handleTraceCallGraphResponse = new GrpcHandler(TraceReaderGrpc.METHOD_GET_TRACE_CALL_GRAPH.getFullMethodName) + private val traceReader = new TraceReader(traceStore, validatorsConfig, transformersConfig) + + /** + * endpoint for fetching a trace + * trace will be validated and transformed + * + * @param request TraceRequest object containing traceId of the trace to fetch + * @param responseObserver response observer will contain Trace object + * or will error out with [[com.expedia.www.haystack.trace.reader.exceptions.TraceNotFoundException]] + */ + override def getTrace(request: TraceRequest, responseObserver: StreamObserver[Trace]): Unit = { + handleGetTraceResponse.handle(request, responseObserver) { + traceReader.getTrace(request) + } + } + + /** + * endpoint for fetching raw trace logs, trace will returned without validations and transformations + * + * @param request TraceRequest object containing traceId of the trace to fetch + * @param responseObserver response observer will stream out [[Trace]] object + * or will error out with [[com.expedia.www.haystack.trace.reader.exceptions.TraceNotFoundException]] + */ + override def getRawTrace(request: TraceRequest, responseObserver: StreamObserver[Trace]): Unit = { + handleGetRawTraceResponse.handle(request, responseObserver) { + traceReader.getRawTrace(request) + } + } + + /** + * endpoint for fetching raw span logs, span will returned without validations and transformations + * + * @param request SpanRequest object containing spanId and parent traceId of the span to fetch + * @param responseObserver response observer will stream out [[Span]] object + * or will error out with [[com.expedia.www.haystack.trace.reader.exceptions.SpanNotFoundException]] + */ + override def getRawSpan(request: SpanRequest, responseObserver: StreamObserver[SpanResponse]): Unit = { + handleGetRawSpanResponse.handle(request, responseObserver) { + traceReader.getRawSpan(request) + } + } + + /** + * endpoint for searching traces + * + * @param request TracesSearchRequest object containing criteria and filters for traces to find + * @param responseObserver response observer will stream out [[List[Trace]] + */ + override def searchTraces(request: TracesSearchRequest, responseObserver: StreamObserver[TracesSearchResult]): Unit = { + handleSearchResponse.handle(request, responseObserver) { + traceReader.searchTraces(request) + } + } + + /** + * get list of field names available in indexing system + * + * @param request empty request object + * @param responseObserver response observer will contain list of field names + */ + override def getFieldNames(request: Empty, responseObserver: StreamObserver[FieldNames]): Unit = { + handleFieldNamesResponse.handle(request, responseObserver) { + traceReader.getFieldNames + } + } + + /** + * get list of possible field values for a given field + * + * @param request contains field name and other field name-value pairs to be used as filters + * @param responseObserver response observer will contain list of field values for filter condition + */ + override def getFieldValues(request: FieldValuesRequest, responseObserver: StreamObserver[FieldValues]): Unit = { + handleFieldValuesResponse.handle(request, responseObserver) { + traceReader.getFieldValues(request) + } + } + + override def getTraceCallGraph(request: TraceRequest, responseObserver: StreamObserver[TraceCallGraph]): Unit = { + handleTraceCallGraphResponse.handle(request, responseObserver) { + traceReader.getTraceCallGraph(request) + } + } + + override def getTraceCounts(request: TraceCountsRequest, responseObserver: StreamObserver[TraceCounts]): Unit = { + handleTraceCallGraphResponse.handle(request, responseObserver) { + traceReader.getTraceCounts(request) + } + } + + override def getRawTraces(request: RawTracesRequest, responseObserver: StreamObserver[RawTracesResult]): Unit = { + handleTraceCallGraphResponse.handle(request, responseObserver) { + traceReader.getRawTraces(request) + } + } +} diff --git a/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/stores/EsIndexedTraceStore.scala b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/stores/EsIndexedTraceStore.scala new file mode 100644 index 000000000..b91673b5e --- /dev/null +++ b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/stores/EsIndexedTraceStore.scala @@ -0,0 +1,149 @@ +/* + * Copyright 2019, Expedia Group. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expedia.www.haystack.trace.reader.stores + +import com.expedia.open.tracing.api._ +import com.expedia.www.haystack.commons.metrics.MetricsSupport +import com.expedia.www.haystack.trace.commons.clients.es.document.TraceIndexDoc +import com.expedia.www.haystack.trace.commons.config.entities.{TraceStoreBackends, WhitelistIndexFieldConfiguration} +import com.expedia.www.haystack.trace.reader.config.entities.ElasticSearchConfiguration +import com.expedia.www.haystack.trace.reader.stores.readers.es.ElasticSearchReader +import com.expedia.www.haystack.trace.reader.stores.readers.es.query.{FieldValuesQueryGenerator, ServiceMetadataQueryGenerator, TraceCountsQueryGenerator, TraceSearchQueryGenerator} +import com.expedia.www.haystack.trace.reader.stores.readers.grpc.GrpcTraceReaders +import io.searchbox.core.SearchResult +import org.elasticsearch.index.IndexNotFoundException +import org.slf4j.LoggerFactory + +import scala.collection.JavaConverters._ +import scala.concurrent.{ExecutionContextExecutor, Future} + +class EsIndexedTraceStore(traceStoreBackendConfig: TraceStoreBackends, + elasticSearchConfiguration: ElasticSearchConfiguration, + whitelistedFieldsConfiguration: WhitelistIndexFieldConfiguration)(implicit val executor: ExecutionContextExecutor) + extends TraceStore with MetricsSupport with ResponseParser { + private val LOGGER = LoggerFactory.getLogger(classOf[EsIndexedTraceStore]) + + private val traceReader: GrpcTraceReaders = new GrpcTraceReaders(traceStoreBackendConfig) + private val esReader: ElasticSearchReader = new ElasticSearchReader(elasticSearchConfiguration.clientConfiguration, elasticSearchConfiguration.awsRequestSigningConfiguration) + private val traceSearchQueryGenerator = new TraceSearchQueryGenerator(elasticSearchConfiguration.spansIndexConfiguration, ES_NESTED_DOC_NAME, whitelistedFieldsConfiguration) + private val traceCountsQueryGenerator = new TraceCountsQueryGenerator(elasticSearchConfiguration.spansIndexConfiguration, ES_NESTED_DOC_NAME, whitelistedFieldsConfiguration) + private val fieldValuesQueryGenerator = new FieldValuesQueryGenerator(elasticSearchConfiguration.spansIndexConfiguration, ES_NESTED_DOC_NAME, whitelistedFieldsConfiguration) + private val serviceMetadataQueryGenerator = new ServiceMetadataQueryGenerator(elasticSearchConfiguration.serviceMetadataIndexConfiguration) + + private val esCountTraces = (request: TraceCountsRequest, useSpecificIndices: Boolean) => { + esReader.count(traceCountsQueryGenerator.generate(request, useSpecificIndices)) + } + + private val esSearchTraces = (request: TracesSearchRequest, useSpecificIndices: Boolean) => { + esReader.search(traceSearchQueryGenerator.generate(request, useSpecificIndices)) + } + + private def handleIndexNotFoundResult(result: Future[SearchResult], + retryFunc: () => Future[SearchResult]): Future[SearchResult] = { + result.recoverWith { + case _: IndexNotFoundException => retryFunc() + } + } + + override def searchTraces(request: TracesSearchRequest): Future[Seq[Trace]] = { + // search ES with specific indices + val esResult = esSearchTraces(request, true) + // handle the response and retry in case of IndexNotFoundException + handleIndexNotFoundResult(esResult, () => esSearchTraces(request, false)).flatMap(result => extractTraces(result)) + } + + private def extractTraces(result: SearchResult): Future[Seq[Trace]] = { + val traceIdKey = "traceid" + + // go through each hit and fetch trace for parsed traceId + val sourceList = result.getSourceAsStringList + if (sourceList != null && sourceList.size() > 0) { + val traceIds = sourceList + .asScala + .map(source => extractStringFieldFromSource(source, traceIdKey)) + .filter(!_.isEmpty) + .toSet[String] // de-dup traceIds + .toList + + traceReader.readTraces(traceIds) + } else { + Future.successful(Nil) + } + } + + override def getTrace(traceId: String): Future[Trace] = traceReader.readTraces(List(traceId)).map(_.head) + + override def getFieldNames: Future[FieldNames] = { + val fields = whitelistedFieldsConfiguration.whitelistIndexFields.distinct.sortBy(_.name) + val builder = FieldNames.newBuilder() + + fields.foreach { + f => { + builder.addNames(f.name) + builder.addFieldMetadata(FieldMetadata.newBuilder().setIsRangeQuery(f.enableRangeQuery)) + } + } + + Future.successful(builder.build()) + } + + private def readFromServiceMetadata(request: FieldValuesRequest): Option[Future[Seq[String]]] = { + val serviceMetadataConfig = elasticSearchConfiguration.serviceMetadataIndexConfiguration + if (!serviceMetadataConfig.enabled) return None + + if (request.getFieldName.toLowerCase == TraceIndexDoc.SERVICE_KEY_NAME && request.getFiltersCount == 0) { + Some(esReader + .search(serviceMetadataQueryGenerator.generateSearchServiceQuery()) + .map(extractServiceMetadata)) + } else if (request.getFieldName.toLowerCase == TraceIndexDoc.OPERATION_KEY_NAME + && (request.getFiltersCount == 1) + && request.getFiltersList.get(0).getName.toLowerCase == TraceIndexDoc.SERVICE_KEY_NAME) { + Some(esReader + .search(serviceMetadataQueryGenerator.generateSearchOperationQuery(request.getFilters(0).getValue)) + .map(extractOperationMetadataFromSource(_, request.getFieldName.toLowerCase))) + } else { + LOGGER.info("read from service metadata request isn't served by elasticsearch") + None + } + } + + + override def getFieldValues(request: FieldValuesRequest): Future[Seq[String]] = { + readFromServiceMetadata(request).getOrElse( + esReader + .search(fieldValuesQueryGenerator.generate(request)) + .map(extractFieldValues(_, request.getFieldName.toLowerCase))) + } + + override def getTraceCounts(request: TraceCountsRequest): Future[TraceCounts] = { + // search ES with specific indices + val esResponse = esCountTraces(request, true) + + // handle the response and retry in case of IndexNotFoundException + handleIndexNotFoundResult(esResponse, () => esCountTraces(request, false)) + .map(result => mapSearchResultToTraceCount(request.getStartTime, request.getEndTime, result)) + } + + override def getRawTraces(request: RawTracesRequest): Future[Seq[Trace]] = { + traceReader.readTraces(request.getTraceIdList.asScala.toList) + } + + override def close(): Unit = { + traceReader.close() + esReader.close() + } +} diff --git a/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/stores/ResponseParser.scala b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/stores/ResponseParser.scala new file mode 100644 index 000000000..aaa6cf71d --- /dev/null +++ b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/stores/ResponseParser.scala @@ -0,0 +1,118 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expedia.www.haystack.trace.reader.stores + +import com.expedia.open.tracing.api.{TraceCount, TraceCounts} +import com.expedia.www.haystack.trace.commons.config.entities.IndexFieldType +import com.expedia.www.haystack.trace.reader.stores.readers.es.query.TraceCountsQueryGenerator +import io.searchbox.core.SearchResult +import org.json4s.ext.EnumNameSerializer +import org.json4s.jackson.JsonMethods.parse +import org.json4s.{DefaultFormats, Formats} + +import scala.collection.JavaConverters._ +import scala.concurrent.Future + +trait ResponseParser { + protected implicit val formats: Formats = DefaultFormats + new EnumNameSerializer(IndexFieldType) + + private val ES_FIELD_AGGREGATIONS = "aggregations" + private val ES_FIELD_BUCKETS = "buckets" + private val ES_FIELD_KEY = "key" + private val ES_COUNT_PER_INTERVAL = "__count_per_interval" + private val ES_AGG_DOC_COUNT = "doc_count" + protected val ES_NESTED_DOC_NAME = "spans" + + protected def mapSearchResultToTraceCounts(result: SearchResult): Future[TraceCounts] = { + val aggregation = result.getJsonObject + .getAsJsonObject(ES_FIELD_AGGREGATIONS) + .getAsJsonObject(ES_NESTED_DOC_NAME) + .getAsJsonObject(ES_NESTED_DOC_NAME) + .getAsJsonObject(ES_COUNT_PER_INTERVAL) + + val traceCounts = aggregation + .getAsJsonArray(ES_FIELD_BUCKETS).asScala.map( + element => TraceCount.newBuilder() + .setTimestamp(element.getAsJsonObject.get(ES_FIELD_KEY).getAsLong) + .setCount(element.getAsJsonObject.get(ES_AGG_DOC_COUNT).getAsLong) + .build() + ).asJava + + Future.successful(TraceCounts.newBuilder().addAllTraceCount(traceCounts).build()) + } + + protected def mapSearchResultToTraceCount(startTime: Long, endTime: Long, result: SearchResult): TraceCounts = { + val traceCountsBuilder = TraceCounts.newBuilder() + + result.getAggregations.getHistogramAggregation(TraceCountsQueryGenerator.COUNT_HISTOGRAM_NAME) + .getBuckets.asScala + .filter(bucket => startTime <= bucket.getKey && bucket.getKey <= endTime) + .foreach(bucket => { + val traceCount = TraceCount.newBuilder().setCount(bucket.getCount).setTimestamp(bucket.getKey) + traceCountsBuilder.addTraceCount(traceCount) + }) + traceCountsBuilder.build() + } + + protected def extractFieldValues(result: SearchResult, fieldName: String): List[String] = { + val aggregations = + result + .getJsonObject + .getAsJsonObject(ES_FIELD_AGGREGATIONS) + .getAsJsonObject(ES_NESTED_DOC_NAME) + .getAsJsonObject(fieldName) + + if (aggregations.has(ES_FIELD_BUCKETS)) { + aggregations + .getAsJsonArray(ES_FIELD_BUCKETS) + .asScala + .map(element => element.getAsJsonObject.get(ES_FIELD_KEY).getAsString) + .toList + } + else { + aggregations + .getAsJsonObject(fieldName) + .getAsJsonArray(ES_FIELD_BUCKETS) + .asScala + .map(element => element.getAsJsonObject.get(ES_FIELD_KEY).getAsString) + .toList + } + } + + protected def extractStringFieldFromSource(source: String, fieldName:String): String = { + (parse(source) \ fieldName).extract[String] + } + + protected def extractServiceMetadata(result: SearchResult): Seq[String] = { + result.getAggregations.getTermsAggregation("distinct_services").getBuckets.asScala.map(_.getKey) + } + + protected def extractOperationMetadataFromSource(result: SearchResult, fieldName: String): List[String] = { + // go through each hit and fetch field from service_metadata + val sourceList = result.getSourceAsStringList + if (sourceList != null && sourceList.size() > 0) { + sourceList + .asScala + .map(source => extractStringFieldFromSource(source, fieldName)) + .filter(!_.isEmpty) + .toSet[String] // de-dup fieldValues + .toList + } else { + Nil + } + } +} diff --git a/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/stores/TraceStore.scala b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/stores/TraceStore.scala new file mode 100644 index 000000000..257f634e4 --- /dev/null +++ b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/stores/TraceStore.scala @@ -0,0 +1,30 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expedia.www.haystack.trace.reader.stores + +import com.expedia.open.tracing.api._ + +import scala.concurrent.Future + +trait TraceStore extends AutoCloseable { + def getTrace(traceId: String): Future[Trace] + def searchTraces(request: TracesSearchRequest): Future[Seq[Trace]] + def getFieldNames: Future[FieldNames] + def getFieldValues(request: FieldValuesRequest): Future[Seq[String]] + def getTraceCounts(request: TraceCountsRequest): Future[TraceCounts] + def getRawTraces(request: RawTracesRequest): Future[Seq[Trace]] +} diff --git a/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/stores/readers/es/ESUtils.scala b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/stores/readers/es/ESUtils.scala new file mode 100644 index 000000000..5e6386d27 --- /dev/null +++ b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/stores/readers/es/ESUtils.scala @@ -0,0 +1,27 @@ +/* + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expedia.www.haystack.trace.reader.stores.readers.es + +import com.google.gson.Gson +import io.searchbox.action.AbstractAction +import io.searchbox.client.JestResult + +object ESUtils { + implicit class ElasticSearchSearchExtension[T <: JestResult](val search: AbstractAction[T]) extends AnyVal { + def toJson: String = search.getData(new Gson()).replaceAll("\n", "") + } +} diff --git a/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/stores/readers/es/ElasticSearchCountResultListener.scala b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/stores/readers/es/ElasticSearchCountResultListener.scala new file mode 100644 index 000000000..b6da6351e --- /dev/null +++ b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/stores/readers/es/ElasticSearchCountResultListener.scala @@ -0,0 +1,64 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expedia.www.haystack.trace.reader.stores.readers.es + +import com.codahale.metrics.{Meter, Timer} +import com.expedia.www.haystack.trace.reader.exceptions.ElasticSearchClientError +import com.expedia.www.haystack.trace.reader.stores.readers.es.ESUtils._ +import com.expedia.www.haystack.trace.reader.stores.readers.es.ElasticSearchCountResultListener._ +import io.searchbox.client.JestResultHandler +import io.searchbox.core.{Search, SearchResult} +import org.elasticsearch.index.IndexNotFoundException +import org.slf4j.{Logger, LoggerFactory} + +import scala.concurrent.Promise + +object ElasticSearchCountResultListener { + protected val LOGGER: Logger = LoggerFactory.getLogger(classOf[ElasticSearchCountResultListener]) +} + +class ElasticSearchCountResultListener(request: Search, + promise: Promise[SearchResult], + timer: Timer.Context, + failure: Meter) extends JestResultHandler[SearchResult] with ElasticSearchResultListener { + + override def completed(result: SearchResult): Unit = { + timer.close() + + if (is2xx(result.getResponseCode)) { + promise.success(result) + } else { + val errorJsonString = result.getJsonString + if (errorJsonString.toLowerCase.contains(INDEX_NOT_FOUND_EXCEPTION)) { + val indexNotFoundEx = new IndexNotFoundException("Index not found exception, should retry", ElasticSearchClientError(result.getResponseCode, errorJsonString)) + promise.failure(indexNotFoundEx) + } else { + val ex = ElasticSearchClientError(result.getResponseCode, errorJsonString) + LOGGER.error(s"Failed in reading from elasticsearch for request='${request.toJson}'", ex) + failure.mark() + promise.failure(ex) + } + } + } + + override def failed(ex: Exception): Unit = { + LOGGER.error(s"Failed in reading from elasticsearch for request=${request.toJson}", ex) + failure.mark() + timer.close() + promise.failure(ex) + } +} diff --git a/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/stores/readers/es/ElasticSearchReadResultListener.scala b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/stores/readers/es/ElasticSearchReadResultListener.scala new file mode 100644 index 000000000..17fc98e96 --- /dev/null +++ b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/stores/readers/es/ElasticSearchReadResultListener.scala @@ -0,0 +1,63 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expedia.www.haystack.trace.reader.stores.readers.es + +import com.codahale.metrics.{Meter, Timer} +import com.expedia.www.haystack.trace.reader.exceptions.ElasticSearchClientError +import com.expedia.www.haystack.trace.reader.stores.readers.es.ESUtils._ +import com.expedia.www.haystack.trace.reader.stores.readers.es.ElasticSearchReadResultListener._ +import io.searchbox.client.JestResultHandler +import io.searchbox.core.{Search, SearchResult} +import org.elasticsearch.index.IndexNotFoundException +import org.slf4j.{Logger, LoggerFactory} + +import scala.concurrent.Promise + +object ElasticSearchReadResultListener { + protected val LOGGER: Logger = LoggerFactory.getLogger(classOf[ElasticSearchReadResultListener]) +} + +class ElasticSearchReadResultListener(request: Search, + promise: Promise[SearchResult], + timer: Timer.Context, + failure: Meter) extends JestResultHandler[SearchResult] with ElasticSearchResultListener { + + override def completed(result: SearchResult): Unit = { + timer.close() + + if (is2xx(result.getResponseCode)) { + promise.success(result) + } else { + if (result.getJsonString.toLowerCase.contains(INDEX_NOT_FOUND_EXCEPTION)) { + val indexNotFoundEx = new IndexNotFoundException("Index not found exception, should retry", ElasticSearchClientError(result.getResponseCode, result.getJsonString)) + promise.failure(indexNotFoundEx) + } else { + val ex = ElasticSearchClientError(result.getResponseCode, result.getJsonString) + LOGGER.error(s"Failed in reading from elasticsearch for request='${request.toJson}'", ex) + failure.mark() + promise.failure(ex) + } + } + } + + override def failed(ex: Exception): Unit = { + LOGGER.error(s"Failed in reading from elasticsearch for request=${request.toJson}", ex) + failure.mark() + timer.close() + promise.failure(ex) + } +} diff --git a/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/stores/readers/es/ElasticSearchReader.scala b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/stores/readers/es/ElasticSearchReader.scala new file mode 100644 index 000000000..7b29ef519 --- /dev/null +++ b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/stores/readers/es/ElasticSearchReader.scala @@ -0,0 +1,99 @@ +/* + * Copyright 2019, Expedia Group. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expedia.www.haystack.trace.reader.stores.readers.es + +import com.expedia.www.haystack.commons.metrics.MetricsSupport +import com.expedia.www.haystack.trace.commons.clients.es.AWSSigningJestClientFactory +import com.expedia.www.haystack.trace.commons.config.entities.AWSRequestSigningConfiguration +import com.expedia.www.haystack.trace.reader.config.entities.ElasticSearchClientConfiguration +import com.expedia.www.haystack.trace.reader.metrics.AppMetricNames +import com.expedia.www.haystack.trace.reader.stores.readers.es.ESUtils._ +import com.google.gson.Gson +import io.searchbox.client.config.HttpClientConfig +import io.searchbox.client.{JestClient, JestClientFactory} +import io.searchbox.core.{Search, SearchResult} +import org.slf4j.LoggerFactory + +import scala.concurrent.{ExecutionContextExecutor, Future, Promise} +import scala.util.Try + +class ElasticSearchReader(config: ElasticSearchClientConfiguration, awsRequestSigningConfig: AWSRequestSigningConfiguration)(implicit val dispatcher: ExecutionContextExecutor) extends MetricsSupport with AutoCloseable { + private val LOGGER = LoggerFactory.getLogger(classOf[ElasticSearchReader]) + private val readTimer = metricRegistry.timer(AppMetricNames.ELASTIC_SEARCH_READ_TIME) + private val readFailures = metricRegistry.meter(AppMetricNames.ELASTIC_SEARCH_READ_FAILURES) + + // initialize the elastic search client + private val esClient: JestClient = { + LOGGER.info("Initializing the http elastic search client with endpoint={}", config.endpoint) + + val factory = { + if (awsRequestSigningConfig.enabled) { + LOGGER.info("using AWSSigningJestClientFactory for es client") + new AWSSigningJestClientFactory(awsRequestSigningConfig) + } else { + LOGGER.info("using JestClientFactory for es client") + new JestClientFactory() + } + } + + val builder = new HttpClientConfig.Builder(config.endpoint) + .multiThreaded(true) + .connTimeout(config.connectionTimeoutMillis) + .readTimeout(config.readTimeoutMillis) + + if (config.username.isDefined && config.password.isDefined) { + builder.defaultCredentials(config.username.get, config.password.get) + } + + factory.setHttpClientConfig(builder.build()) + factory.getObject + } + + def search(request: Search): Future[SearchResult] = { + val promise = Promise[SearchResult]() + val time = readTimer.time() + try { + LOGGER.debug(s"elastic search query requested: ${request.toString}', query: '${request.toJson}'") + esClient.executeAsync(request, new ElasticSearchReadResultListener(request, promise, time, readFailures)) + promise.future + } catch { + case ex: Exception => + readFailures.mark() + time.stop() + LOGGER.error(s"Failed to read from elasticsearch for request=${request.toJson} with exception", ex) + Future.failed(ex) + } + } + + def count(request: Search): Future[SearchResult] = { + val promise = Promise[SearchResult]() + val time = readTimer.time() + try { + LOGGER.debug(s"elastic count query requested: ${request.toString}', query: '${request.toJson}'") + esClient.executeAsync(request, new ElasticSearchCountResultListener(request, promise, time, readFailures)) + promise.future + } catch { + case ex: Exception => + readFailures.mark() + time.stop() + LOGGER.error(s"Failed to read from elasticsearch for request=${request.getData(new Gson())} with exception", ex) + Future.failed(ex) + } + } + + override def close(): Unit = Try(esClient.shutdownClient()) +} diff --git a/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/stores/readers/es/ElasticSearchResultListener.scala b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/stores/readers/es/ElasticSearchResultListener.scala new file mode 100644 index 000000000..58545176b --- /dev/null +++ b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/stores/readers/es/ElasticSearchResultListener.scala @@ -0,0 +1,23 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expedia.www.haystack.trace.reader.stores.readers.es + +trait ElasticSearchResultListener { + protected val INDEX_NOT_FOUND_EXCEPTION = "index_not_found_exception" + + protected def is2xx(code: Int): Boolean = (code / 100) == 2 +} diff --git a/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/stores/readers/es/query/FieldValuesQueryGenerator.scala b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/stores/readers/es/query/FieldValuesQueryGenerator.scala new file mode 100644 index 000000000..bb4957f1b --- /dev/null +++ b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/stores/readers/es/query/FieldValuesQueryGenerator.scala @@ -0,0 +1,51 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expedia.www.haystack.trace.reader.stores.readers.es.query + +import com.expedia.open.tracing.api.FieldValuesRequest +import com.expedia.www.haystack.trace.commons.config.entities.WhitelistIndexFieldConfiguration +import com.expedia.www.haystack.trace.reader.config.entities.SpansIndexConfiguration +import io.searchbox.core.Search +import org.elasticsearch.search.builder.SearchSourceBuilder + +class FieldValuesQueryGenerator(config: SpansIndexConfiguration, + nestedDocName: String, + indexConfiguration: WhitelistIndexFieldConfiguration) + extends SpansIndexQueryGenerator(nestedDocName, indexConfiguration) { + + def generate(request: FieldValuesRequest): Search = { + new Search.Builder(buildQueryString(request)) + .addIndex(s"${config.indexNamePrefix}*") + .addType(config.indexType) + .build() + } + + private def buildQueryString(request: FieldValuesRequest): String = { + val query = createFilterFieldBasedQuery(request.getFiltersList) + if (query.filter().size() > 0) { + new SearchSourceBuilder() + .aggregation(createNestedAggregationQueryWithNestedFilters(request.getFieldName.toLowerCase, request.getFiltersList)) + .size(0) + .toString + } else { + new SearchSourceBuilder() + .aggregation(createNestedAggregationQuery(request.getFieldName.toLowerCase)) + .size(0) + .toString + } + } +} \ No newline at end of file diff --git a/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/stores/readers/es/query/ServiceMetadataQueryGenerator.scala b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/stores/readers/es/query/ServiceMetadataQueryGenerator.scala new file mode 100644 index 000000000..9fc1602da --- /dev/null +++ b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/stores/readers/es/query/ServiceMetadataQueryGenerator.scala @@ -0,0 +1,59 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expedia.www.haystack.trace.reader.stores.readers.es.query + +import com.expedia.www.haystack.trace.reader.config.entities.ServiceMetadataIndexConfiguration +import io.searchbox.core.Search +import org.elasticsearch.index.query.QueryBuilders.termQuery +import org.elasticsearch.search.aggregations.AggregationBuilders +import org.elasticsearch.search.builder.SearchSourceBuilder + +class ServiceMetadataQueryGenerator(config: ServiceMetadataIndexConfiguration) { + private val SERVICE_NAME_KEY = "servicename" + private val OPERATION_NAME_KEY = "operationname" + private val LIMIT = 10000 + + def generateSearchServiceQuery(): Search = { + val serviceAggregationQuery = buildServiceAggregationQuery() + generateSearchQuery(serviceAggregationQuery) + } + + def generateSearchOperationQuery(serviceName: String): Search = { + val serviceAggregationQuery = buildOperationAggregationQuery(serviceName) + generateSearchQuery(serviceAggregationQuery) + } + + private def generateSearchQuery(queryString: String): Search = { + new Search.Builder(queryString) + .addIndex(config.indexName) + .addType(config.indexType) + .build() + } + + private def buildServiceAggregationQuery(): String = { + val aggr = AggregationBuilders.terms("distinct_services").field(SERVICE_NAME_KEY).size(LIMIT) + new SearchSourceBuilder().aggregation(aggr).size(0).toString + } + + private def buildOperationAggregationQuery(serviceName: String): String = { + new SearchSourceBuilder() + .query(termQuery(SERVICE_NAME_KEY, serviceName)) + .fetchSource(OPERATION_NAME_KEY, SERVICE_NAME_KEY) + .size(LIMIT) + .toString + } +} diff --git a/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/stores/readers/es/query/SpansIndexQueryGenerator.scala b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/stores/readers/es/query/SpansIndexQueryGenerator.scala new file mode 100644 index 000000000..dc465dc5d --- /dev/null +++ b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/stores/readers/es/query/SpansIndexQueryGenerator.scala @@ -0,0 +1,218 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expedia.www.haystack.trace.reader.stores.readers.es.query + +import java.text.SimpleDateFormat +import java.util.{Date, TimeZone} + +import com.expedia.open.tracing.api.Operand.OperandCase +import com.expedia.open.tracing.api.{ExpressionTree, Field} +import com.expedia.www.haystack.trace.commons.clients.es.document.TraceIndexDoc +import com.expedia.www.haystack.trace.commons.config.entities.{IndexFieldType, WhitelistIndexFieldConfiguration} +import io.searchbox.strings.StringUtils +import org.apache.lucene.search.join.ScoreMode +import org.elasticsearch.index.query.QueryBuilders.{boolQuery, nestedQuery, termQuery} +import org.elasticsearch.index.query._ +import org.elasticsearch.search.aggregations.AggregationBuilder +import org.elasticsearch.search.aggregations.bucket.filter.FilterAggregationBuilder +import org.elasticsearch.search.aggregations.bucket.nested.NestedAggregationBuilder +import org.elasticsearch.search.aggregations.bucket.terms.TermsAggregationBuilder +import org.elasticsearch.search.aggregations.support.ValueType + +import scala.collection.JavaConverters._ + +abstract class SpansIndexQueryGenerator(nestedDocName: String, + whitelistIndexFieldConfiguration: WhitelistIndexFieldConfiguration) { + private final val TIME_ZONE = TimeZone.getTimeZone("UTC") + + // create search query by using filters list + @deprecated + protected def createFilterFieldBasedQuery(filterFields: java.util.List[Field]): BoolQueryBuilder = { + val traceContextWhitelistFields = whitelistIndexFieldConfiguration.globalTraceContextIndexFieldNames + val (traceContextFields, serviceContextFields) = filterFields + .asScala + .partition(f => traceContextWhitelistFields.contains(f.getName.toLowerCase)) + + val query = boolQuery() + + createNestedQuery(serviceContextFields).map(query.filter) + + traceContextFields foreach { + field => { + createNestedQuery(Seq(field)) match { + case Some(nestedQuery) => query.filter(nestedQuery) + case _ => /* may be log ? */ + } + } + } + query + } + + // create search query by using filters expression tree + protected def createExpressionTreeBasedQuery(expression: ExpressionTree): BoolQueryBuilder = { + val query = boolQuery() + val contextFiltersList = listOfContextFilters(expression) + + // create a nested boolean query per context + contextFiltersList foreach { + filters => { + createNestedQuery(filters) match { + case Some(nestedQuery) => query.filter(nestedQuery) + case _ => /* may be log ?*/ + } + } + } + query + } + + // create list of fields, one for each trace level query and one for each span level groups + // assuming that first level is trace level filters + // and second level are span level filter groups + private def listOfContextFilters(expression: ExpressionTree): List[List[Field]] = { + val (spanLevel, traceLevel) = expression.getOperandsList.asScala.partition(operand => operand.getOperandCase == OperandCase.EXPRESSION) + + val traceLevelFilters = traceLevel.map(field => List(field.getField)) + val spanLevelFilters = spanLevel.map(tree => toListOfSpanLevelFilters(tree.getExpression)) + + (spanLevelFilters ++ traceLevelFilters).toList + } + + private def toListOfSpanLevelFilters(expression: ExpressionTree): List[Field] = { + expression.getOperandsList.asScala.map(field => field.getField).toList + } + + private def createNestedQuery(fields: Seq[Field]): Option[NestedQueryBuilder] = { + if (fields.isEmpty) { + None + } else { + val nestedBoolQueryBuilder = createNestedBoolQuery(fields) + Some(nestedQuery(nestedDocName, nestedBoolQueryBuilder, ScoreMode.None)) + } + } + + private def buildNestedTermQuery(field: Field): TermQueryBuilder = { + termQuery(withBaseDoc(field.getName.toLowerCase), field.getValue) + } + + private def buildNestedRangeQuery(field: Field): RangeQueryBuilder = { + def rangeValue(): Any = { + if(field.getName == TraceIndexDoc.DURATION_KEY_NAME || field.getName == TraceIndexDoc.START_TIME_KEY_NAME) { + field.getValue.toLong + } else { + val fieldType = whitelistIndexFieldConfiguration.whitelistIndexFields + .find(wf => wf.name.equalsIgnoreCase(field.getName)) + .map(wf => wf.`type`) + .getOrElse(IndexFieldType.string) + + fieldType match { + case IndexFieldType.int | IndexFieldType.long => field.getValue.toLong + case IndexFieldType.double => field.getValue.toDouble + case IndexFieldType.bool => field.getValue.toBoolean + case _ => field.getValue + } + } + } + + val rangeQuery = QueryBuilders.rangeQuery(withBaseDoc(field.getName.toLowerCase)) + val value = rangeValue() + field.getOperator match { + case Field.Operator.GREATER_THAN => rangeQuery.gt(value) + case Field.Operator.LESS_THAN => rangeQuery.lt(value) + case _ => throw new RuntimeException("Fail to understand the operator -" + field.getOperator) + } + rangeQuery + } + + protected def createNestedBoolQuery(fields: Seq[Field]): BoolQueryBuilder = { + val boolQueryBuilder = boolQuery() + + val validFields = fields.filterNot(f => StringUtils.isBlank(f.getValue)) + validFields foreach { + field => { + field match { + case _ if field.getOperator == null || field.getOperator == Field.Operator.EQUAL => + boolQueryBuilder.filter(buildNestedTermQuery(field)) + case _ if field.getOperator == Field.Operator.NOT_EQUAL => + boolQueryBuilder.mustNot(buildNestedTermQuery(field)) + case _ if field.getOperator == Field.Operator.GREATER_THAN || field.getOperator == Field.Operator.LESS_THAN => + boolQueryBuilder.filter(buildNestedRangeQuery(field)) + case _ => throw new RuntimeException("Fail to understand the operator type of the field!") + } + } + } + + + boolQueryBuilder + } + + protected def createNestedAggregationQuery(fieldName: String): AggregationBuilder = + new NestedAggregationBuilder(nestedDocName, nestedDocName) + .subAggregation( + new TermsAggregationBuilder(fieldName, ValueType.STRING) + .field(withBaseDoc(fieldName)) + .size(1000)) + + protected def createNestedAggregationQueryWithNestedFilters(fieldName: String, filterFields: java.util.List[Field]): AggregationBuilder = { + val boolQueryBuilder = createNestedBoolQuery(filterFields.asScala) + + new NestedAggregationBuilder(nestedDocName, nestedDocName) + .subAggregation( + new FilterAggregationBuilder(s"$fieldName", boolQueryBuilder) + .subAggregation(new TermsAggregationBuilder(s"$fieldName", ValueType.STRING) + .field(withBaseDoc(fieldName)) + .size(1000)) + ) + } + + def getESIndexes(startTimeInMicros: Long, + endTimeInMicros: Long, + indexNamePrefix: String, + indexHourBucket: Int, + indexHourTtl: Int): Seq[String] = { + + if (!isValidTimeRange(startTimeInMicros, endTimeInMicros, indexHourTtl)) { + Seq(s"$indexNamePrefix") + } else { + val INDEX_BUCKET_TIME_IN_MICROS: Long = indexHourBucket.toLong * 60 * 60 * 1000 * 1000 + val flooredStarttime = startTimeInMicros - (startTimeInMicros % INDEX_BUCKET_TIME_IN_MICROS) + val flooredEndtime = endTimeInMicros - (endTimeInMicros % INDEX_BUCKET_TIME_IN_MICROS) + + for (datetimeInMicros <- flooredStarttime to flooredEndtime by INDEX_BUCKET_TIME_IN_MICROS) + yield { + val date = new Date(datetimeInMicros / 1000) + val dateBucket = createSimpleDateFormat("yyyy-MM-dd").format(date) + val hourBucket = createSimpleDateFormat("HH").format(date).toInt / indexHourBucket + + s"$indexNamePrefix-$dateBucket-$hourBucket" + } + } + } + + private def createSimpleDateFormat(pattern: String): SimpleDateFormat = { + val sdf = new SimpleDateFormat(pattern) + sdf.setTimeZone(TIME_ZONE) + sdf + } + + private def isValidTimeRange(startTimeInMicros: Long, + endTimeInMicros: Long, + indexHourTtl: Int): Boolean = { + (endTimeInMicros - startTimeInMicros) < (indexHourTtl.toLong * 60 * 60 * 1000 * 1000) + } + + protected def withBaseDoc(field: String) = s"$nestedDocName.$field" +} diff --git a/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/stores/readers/es/query/TraceCountsQueryGenerator.scala b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/stores/readers/es/query/TraceCountsQueryGenerator.scala new file mode 100644 index 000000000..f822dbc07 --- /dev/null +++ b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/stores/readers/es/query/TraceCountsQueryGenerator.scala @@ -0,0 +1,100 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expedia.www.haystack.trace.reader.stores.readers.es.query + +import com.expedia.open.tracing.api.TraceCountsRequest +import com.expedia.www.haystack.trace.commons.clients.es.document.TraceIndexDoc +import com.expedia.www.haystack.trace.commons.config.entities.WhitelistIndexFieldConfiguration +import com.expedia.www.haystack.trace.reader.config.entities.SpansIndexConfiguration +import io.searchbox.core.Search +import org.elasticsearch.index.query.{BoolQueryBuilder, QueryBuilders} +import org.elasticsearch.search.aggregations.AggregationBuilders +import org.elasticsearch.search.builder.SearchSourceBuilder + +import scala.collection.JavaConverters._ + + +object TraceCountsQueryGenerator { + val COUNT_HISTOGRAM_NAME = "countagg" +} + +class TraceCountsQueryGenerator(config: SpansIndexConfiguration, + nestedDocName: String, + whitelistIndexFields: WhitelistIndexFieldConfiguration) + extends SpansIndexQueryGenerator(nestedDocName, whitelistIndexFields) { + + import TraceCountsQueryGenerator._ + + def generate(request: TraceCountsRequest, useSpecificIndices: Boolean): Search = { + require(request.getStartTime > 0) + require(request.getEndTime > 0) + require(request.getInterval > 0) + + if (useSpecificIndices) { + generate(request) + } else { + new Search.Builder(buildQueryString(request)) + .addIndex(config.indexNamePrefix) + .addType(config.indexType) + .build() + } + } + + def generate(request: TraceCountsRequest): Search = { + require(request.getStartTime > 0) + require(request.getEndTime > 0) + require(request.getInterval > 0) + + // create ES count query + val targetIndicesToSearch = getESIndexes( + request.getStartTime, + request.getEndTime, + config.indexNamePrefix, + config.indexHourBucket, + config.indexHourTtl).asJava + + new Search.Builder(buildQueryString(request)) + .addIndex(targetIndicesToSearch) + .addType(config.indexType) + .build() + } + + private def buildQueryString(request: TraceCountsRequest): String = { + val query: BoolQueryBuilder = + if(request.hasFilterExpression) { + createExpressionTreeBasedQuery(request.getFilterExpression) + } + else { + // this is deprecated + createFilterFieldBasedQuery(request.getFieldsList) + } + + query.must(QueryBuilders.rangeQuery(TraceIndexDoc.START_TIME_KEY_NAME).gte(request.getStartTime).lte(request.getEndTime)) + + val aggregation = AggregationBuilders + .histogram(COUNT_HISTOGRAM_NAME) + .field(TraceIndexDoc.START_TIME_KEY_NAME) + .interval(request.getInterval) + .extendedBounds(request.getStartTime, request.getEndTime) + + new SearchSourceBuilder() + .query(query) + .aggregation(aggregation) + .size(0) + .toString + } +} diff --git a/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/stores/readers/es/query/TraceSearchQueryGenerator.scala b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/stores/readers/es/query/TraceSearchQueryGenerator.scala new file mode 100644 index 000000000..14a4ed04d --- /dev/null +++ b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/stores/readers/es/query/TraceSearchQueryGenerator.scala @@ -0,0 +1,99 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expedia.www.haystack.trace.reader.stores.readers.es.query + +import com.expedia.open.tracing.api.TracesSearchRequest +import com.expedia.www.haystack.trace.commons.clients.es.document.TraceIndexDoc._ +import com.expedia.www.haystack.trace.commons.config.entities.WhitelistIndexFieldConfiguration +import com.expedia.www.haystack.trace.reader.config.entities.SpansIndexConfiguration +import io.searchbox.core.Search +import org.apache.lucene.search.join.ScoreMode +import org.elasticsearch.index.query.QueryBuilders._ +import org.elasticsearch.search.builder.SearchSourceBuilder +import org.elasticsearch.search.sort.{FieldSortBuilder, SortOrder} + +import scala.collection.JavaConverters._ + +class TraceSearchQueryGenerator(config: SpansIndexConfiguration, + nestedDocName: String, + whitelistIndexFields: WhitelistIndexFieldConfiguration) + extends SpansIndexQueryGenerator(nestedDocName, whitelistIndexFields) { + + def generate(request: TracesSearchRequest, useSpecificIndices: Boolean): Search = { + require(request.getStartTime > 0) + require(request.getEndTime > 0) + require(request.getLimit > 0) + + if (useSpecificIndices) { + generate(request) + } else { + new Search.Builder(buildQueryString(request)) + .addIndex(config.indexNamePrefix) + .addType(config.indexType) + .build() + } + } + + def generate(request: TracesSearchRequest): Search = { + require(request.getStartTime > 0) + require(request.getEndTime > 0) + require(request.getLimit > 0) + + val targetIndicesToSearch = getESIndexes( + request.getStartTime, + request.getEndTime, + config.indexNamePrefix, + config.indexHourBucket, + config.indexHourTtl).asJava + + new Search.Builder(buildQueryString(request)) + .addIndex(targetIndicesToSearch) + .addType(config.indexType) + .build() + } + + private def buildQueryString(request: TracesSearchRequest): String = { + val query = + if(request.hasFilterExpression) + createExpressionTreeBasedQuery(request.getFilterExpression) + else + createFilterFieldBasedQuery(request.getFieldsList) + + if(config.useRootDocumentStartTime) { + query + .must(rangeQuery(START_TIME_KEY_NAME) + .gte(request.getStartTime) + .lte(request.getEndTime)) + } else { + query.must( + nestedQuery(nestedDocName, + rangeQuery(withBaseDoc(START_TIME_KEY_NAME)) + .gte(request.getStartTime) + .lte(request.getEndTime), ScoreMode.None)) + } + + val sortBuilder = + if(config.useRootDocumentStartTime) { + new FieldSortBuilder(START_TIME_KEY_NAME).order(SortOrder.DESC) + } + else { + new FieldSortBuilder(withBaseDoc(START_TIME_KEY_NAME)).order(SortOrder.DESC).setNestedPath(nestedDocName) + } + + new SearchSourceBuilder().query(query).sort(sortBuilder).size(request.getLimit).toString + } +} diff --git a/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/stores/readers/grpc/GrpcTraceReaders.scala b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/stores/readers/grpc/GrpcTraceReaders.scala new file mode 100644 index 000000000..a6c739fe2 --- /dev/null +++ b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/stores/readers/grpc/GrpcTraceReaders.scala @@ -0,0 +1,99 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expedia.www.haystack.trace.reader.stores.readers.grpc + +import com.expedia.open.tracing.api.Trace +import com.expedia.open.tracing.backend.{ReadSpansRequest, StorageBackendGrpc} +import com.expedia.www.haystack.commons.metrics.MetricsSupport +import com.expedia.www.haystack.trace.commons.config.entities.TraceStoreBackends +import com.expedia.www.haystack.trace.reader.exceptions.TraceNotFoundException +import com.expedia.www.haystack.trace.reader.metrics.AppMetricNames +import com.expedia.www.haystack.trace.reader.readers.utils.TraceMerger +import io.grpc.{ManagedChannel, ManagedChannelBuilder} +import org.slf4j.LoggerFactory + +import scala.collection.JavaConverters._ +import scala.concurrent.{ExecutionContextExecutor, Future, Promise} + +class GrpcTraceReaders(config: TraceStoreBackends) + (implicit val dispatcher: ExecutionContextExecutor) extends MetricsSupport with AutoCloseable { + private val LOGGER = LoggerFactory.getLogger(classOf[GrpcTraceReaders]) + + private val readTimer = metricRegistry.timer(AppMetricNames.BACKEND_READ_TIME) + private val readFailures = metricRegistry.meter(AppMetricNames.BACKEND_READ_FAILURES) + private val tracesFailures = metricRegistry.meter(AppMetricNames.BACKEND_TRACES_FAILURE) + + private val clients: Seq[GrpcChannelClient] = config.backends.map { + backend => { + val channel = ManagedChannelBuilder + .forAddress(backend.host, backend.port) + .usePlaintext(true) + .build() + + val client = StorageBackendGrpc.newFutureStub(channel) + GrpcChannelClient(channel, client) + } + } + + def readTraces(traceIds: List[String]): Future[Seq[Trace]] = { + val allFutures = clients.map { + client => + readTraces(traceIds, client.stub) recoverWith { + case _: Exception => Future.successful(Seq.empty[Trace]) + } + } + + Future.sequence(allFutures) + .map(traceSeq => traceSeq.flatten) + .map { + traces => + if (traces.isEmpty) throw new TraceNotFoundException() else TraceMerger.merge(traces) + } + } + + private def readTraces(traceIds: List[String], client: StorageBackendGrpc.StorageBackendFutureStub): Future[Seq[Trace]] = { + val timer = readTimer.time() + val promise = Promise[Seq[Trace]] + + try { + val readSpansRequest = ReadSpansRequest.newBuilder().addAllTraceIds(traceIds.asJavaCollection).build() + val futureResponse = client.readSpans(readSpansRequest) + futureResponse.addListener(new ReadSpansResponseListener( + futureResponse, + promise, + timer, + readFailures, + tracesFailures, + traceIds.size), dispatcher) + + // return the future with the results for the given client + promise.future + } catch { + case ex: Exception => + readFailures.mark() + timer.stop() + LOGGER.error("Failed to read raw traces with exception", ex) + Future.failed(ex) + } + } + + override def close(): Unit = { + clients.foreach(_.channel.shutdown()) + } + + case class GrpcChannelClient(channel: ManagedChannel, stub: StorageBackendGrpc.StorageBackendFutureStub) +} diff --git a/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/stores/readers/grpc/ReadSpansResponseListener.scala b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/stores/readers/grpc/ReadSpansResponseListener.scala new file mode 100644 index 000000000..5602ce36b --- /dev/null +++ b/traces/reader/src/main/scala/com/expedia/www/haystack/trace/reader/stores/readers/grpc/ReadSpansResponseListener.scala @@ -0,0 +1,82 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expedia.www.haystack.trace.reader.stores.readers.grpc + +import java.util.concurrent.Future + +import com.codahale.metrics.{Meter, Timer} +import com.expedia.open.tracing.api.Trace +import com.expedia.open.tracing.backend.{ReadSpansResponse, TraceRecord} +import com.expedia.www.haystack.trace.commons.packer.Unpacker +import com.expedia.www.haystack.trace.reader.exceptions.TraceNotFoundException +import org.slf4j.{Logger, LoggerFactory} + +import scala.collection.JavaConverters._ +import scala.collection.mutable +import scala.concurrent.Promise +import scala.util.{Failure, Success, Try} + +object ReadSpansResponseListener { + protected val LOGGER: Logger = LoggerFactory.getLogger(classOf[ReadSpansResponseListener]) +} + +class ReadSpansResponseListener(readSpansResponse: Future[ReadSpansResponse], + promise: Promise[Seq[Trace]], + timer: Timer.Context, + failure: Meter, + tracesFailure: Meter, + traceIdCount: Int) extends Runnable { + + import ReadSpansResponseListener._ + + override def run(): Unit = { + timer.close() + + Try(readSpansResponse.get) + .flatMap(tryGetTraceRows) + .flatMap(tryDeserialize) + match { + case Success(traces) => + tracesFailure.mark(traceIdCount - traces.length) + promise.success(traces) + case Failure(ex) => + LOGGER.error("Failed in reading the record from trace-backend", ex) + failure.mark() + tracesFailure.mark(traceIdCount) + promise.failure(ex) + } + } + + private def tryGetTraceRows(response: ReadSpansResponse): Try[Seq[TraceRecord]] = { + val records = response.getRecordsList + if (records.isEmpty) Failure(new TraceNotFoundException) else Success(records.asScala) + } + + private def tryDeserialize(records: Seq[TraceRecord]): Try[Seq[Trace]] = { + val traceBuilderMap = new mutable.HashMap[String, Trace.Builder]() + var deserFailed: Failure[Seq[Trace]] = null + + records.foreach(record => { + Try(Unpacker.readSpanBuffer(record.getSpans.toByteArray)) match { + case Success(sBuffer) => + traceBuilderMap.getOrElseUpdate(sBuffer.getTraceId, Trace.newBuilder().setTraceId(sBuffer.getTraceId)).addAllChildSpans(sBuffer.getChildSpansList) + case Failure(cause) => deserFailed = Failure(cause) + } + }) + if (deserFailed == null) Success(traceBuilderMap.values.map(_.build).toSeq) else deserFailed + } +} diff --git a/traces/reader/src/test/resources/config/base.conf b/traces/reader/src/test/resources/config/base.conf new file mode 100644 index 000000000..2a3707325 --- /dev/null +++ b/traces/reader/src/test/resources/config/base.conf @@ -0,0 +1,99 @@ +health.status.path = "isHealthy" + +service { + port = 8088 + ssl { + enabled = false + cert.path = "/ssl/cert" + private.key.path = "/ssl/private-key" + } + max.message.size = 52428800 # 50MB in bytes +} + +backend { + client { + host = "localhost" + port = 8090 + } +} + +elasticsearch { + client { + endpoint = "http://elasticsearch:9200" + conn.timeout.ms = 10000 + read.timeout.ms = 5000 + } + index { + spans { + name.prefix = "haystack-traces" + type = "spans" + hour.bucket = 6 + hour.ttl = 72 // 3 * 24 hours + use.root.doc.starttime = true + } + service.metadata { + enabled = false + name = "service_metadata" + type = "metadata" + } + } + + # if enabled flag is true, es requests will be signed + signing.request.aws { + enabled = false + region = "us-west-2" + service.name = "es" + # if 'access.key' is not provided, will use DefaultAWSCredentialsProviderChain to resolve creds + access.key = "" + secret.key = "" + } +} + +trace { + validators { + sequence = [ + "com.expedia.www.haystack.trace.reader.readers.validators.TraceIdValidator" + "com.expedia.www.haystack.trace.reader.readers.validators.ParentIdValidator" + "com.expedia.www.haystack.trace.reader.readers.validators.RootValidator" + ] + } + + transformers { + pre { + sequence = [ + "com.expedia.www.haystack.trace.reader.readers.transformers.DeDuplicateSpanTransformer" + "com.expedia.www.haystack.trace.reader.readers.transformers.ClientServerEventLogTransformer" + "com.expedia.www.haystack.trace.reader.readers.transformers.InfrastructureTagTransformer" + ] + } + post { + sequence = [ + "com.expedia.www.haystack.trace.reader.readers.transformers.PartialSpanTransformer" + "com.expedia.www.haystack.trace.reader.readers.transformers.ClockSkewTransformer" + "com.expedia.www.haystack.trace.reader.readers.transformers.SortSpanTransformer" + ] + } + } +} + +reload { + tables { + index.fields.config = "whitelist-index-fields" + } + config { + endpoint = "http://elasticsearch:9200" + database.name = "reload-configs" + } + startup.load = true + interval.ms = 5000 # -1 will imply 'no reload' + + # if enabled flag is true, es requests will be signed + signing.request.aws { + enabled = false + region = "us-west-2" + service.name = "es" + # if 'access.key' is not provided, will use DefaultAWSCredentialsProviderChain to resolve creds + access.key = "" + secret.key = "" + } +} diff --git a/traces/reader/src/test/resources/logback-test.xml b/traces/reader/src/test/resources/logback-test.xml new file mode 100644 index 000000000..298193e01 --- /dev/null +++ b/traces/reader/src/test/resources/logback-test.xml @@ -0,0 +1 @@ + diff --git a/traces/reader/src/test/resources/raw_trace.json b/traces/reader/src/test/resources/raw_trace.json new file mode 100644 index 000000000..4e8a4585f --- /dev/null +++ b/traces/reader/src/test/resources/raw_trace.json @@ -0,0 +1,651 @@ +{ + "traceId": "c4fffc7d-7c0d-4073-98ea-34362506d323", + "childSpans": [ + { + "traceId": "c4fffc7d-7c0d-4073-98ea-34362506d323", + "spanId": "2fe438d3-9742-4973-a5b6-e0e7870c60e3", + "parentSpanId": "9b0523d6-f14b-4a33-ba75-1e46720684cc", + "serviceName": "service_a", + "operationName": "oper_1", + "startTime": 1534487370387000, + "duration": 0, + "logs": [], + "tags": [ + { + "key": "span.kind", + "value": "server" + } + ] + }, + { + "traceId": "c4fffc7d-7c0d-4073-98ea-34362506d323", + "spanId": "933e7aa6-9dd6-4818-86b0-a8561fc0b4da", + "parentSpanId": "9b0523d6-f14b-4a33-ba75-1e46720684cc", + "serviceName": "service_b", + "operationName": "oper_2", + "startTime": 1534487370212000, + "duration": 155000, + "logs": [ + { + "timestamp": 1534487370212000, + "fields": [ + { + "key": "event", + "value": "cs" + } + ] + }, + { + "timestamp": 1534487370367000, + "fields": [ + { + "key": "event", + "value": "cr" + } + ] + } + ], + "tags": [] + }, + { + "traceId": "c4fffc7d-7c0d-4073-98ea-34362506d323", + "spanId": "2fe438d3-9742-4973-a5b6-e0e7870c60e3", + "parentSpanId": "9b0523d6-f14b-4a33-ba75-1e46720684cc", + "serviceName": "service_b", + "operationName": "oper_3", + "startTime": 1534487370368000, + "duration": 53000, + "logs": [ + { + "timestamp": 1534487370368000, + "fields": [ + { + "key": "event", + "value": "cs" + } + ] + }, + { + "timestamp": 1534487370421000, + "fields": [ + { + "key": "event", + "value": "cr" + } + ] + } + ], + "tags": [] + }, + { + "traceId": "c4fffc7d-7c0d-4073-98ea-34362506d323", + "spanId": "f9d9fd32-55b9-4355-a07a-5e9455d17820", + "parentSpanId": "5e5a9451-08f2-4d83-90cb-4eb424f657fb", + "serviceName": "service_c", + "operationName": "oper_4", + "startTime": 1534487372108000, + "duration": 177000, + "logs": [ + { + "timestamp": 1534487372108000, + "fields": [ + { + "key": "event", + "value": "cs" + } + ] + }, + { + "timestamp": 1534487372285000, + "fields": [ + { + "key": "event", + "value": "cr" + } + ] + } + ], + "tags": [] + }, + { + "traceId": "c4fffc7d-7c0d-4073-98ea-34362506d323", + "spanId": "5e5a9451-08f2-4d83-90cb-4eb424f657fb", + "parentSpanId": "12e410a5-58a4-4f3a-b559-6822ade5acfd", + "serviceName": "service_c", + "operationName": "oper_5", + "startTime": 1534487370603000, + "duration": 1686000, + "logs": [ + { + "timestamp": 1534487370603000, + "fields": [ + { + "key": "event", + "value": "sr" + } + ] + }, + { + "timestamp": 1534487372289000, + "fields": [ + { + "key": "event", + "value": "ss" + } + ] + } + ], + "tags": [] + }, + { + "traceId": "c4fffc7d-7c0d-4073-98ea-34362506d323", + "spanId": "5e5a9451-08f2-4d83-90cb-4eb424f657fb", + "parentSpanId": "9b0523d6-f14b-4a33-ba75-1e46720684cc", + "serviceName": "service_b", + "operationName": "oper_6", + "startTime": 1534487370430000, + "duration": 1893000, + "logs": [ + { + "timestamp": 1534487370430000, + "fields": [ + { + "key": "event", + "value": "cs" + } + ] + }, + { + "timestamp": 1534487372323000, + "fields": [ + { + "key": "event", + "value": "cr" + } + ] + } + ], + "tags": [] + }, + { + "traceId": "c4fffc7d-7c0d-4073-98ea-34362506d323", + "spanId": "0e4ebf10-ad12-4d3c-bf4c-db2bafa64e60", + "parentSpanId": "9b0523d6-f14b-4a33-ba75-1e46720684cc", + "serviceName": "service_b", + "operationName": "oper_7", + "startTime": 1534487372324000, + "duration": 399000, + "logs": [ + { + "timestamp": 1534487372324000, + "fields": [ + { + "key": "event", + "value": "cs" + } + ] + }, + { + "timestamp": 1534487372723000, + "fields": [ + { + "key": "event", + "value": "cr" + } + ] + } + ], + "tags": [] + }, + { + "traceId": "c4fffc7d-7c0d-4073-98ea-34362506d323", + "spanId": "eb9b1f0d-c0e5-487e-9b50-a2175c69807e", + "parentSpanId": "9b0523d6-f14b-4a33-ba75-1e46720684cc", + "serviceName": "service_b", + "operationName": "oper_7", + "startTime": 1534487372726000, + "duration": 95000, + "logs": [ + { + "timestamp": 1534487372726000, + "fields": [ + { + "key": "event", + "value": "cs" + } + ] + }, + { + "timestamp": 1534487372821000, + "fields": [ + { + "key": "event", + "value": "cr" + } + ] + } + ], + "tags": [] + }, + { + "traceId": "c4fffc7d-7c0d-4073-98ea-34362506d323", + "spanId": "9b0523d6-f14b-4a33-ba75-1e46720684cc", + "parentSpanId": "a6575426-1bd5-4195-ba8d-1bd36bc390b1", + "serviceName": "service_b", + "operationName": "oper_8", + "startTime": 1534487370211000, + "duration": 2617000, + "logs": [ + { + "timestamp": 1534487370211000, + "fields": [ + { + "key": "event", + "value": "sr" + } + ] + }, + { + "timestamp": 1534487372828000, + "fields": [ + { + "key": "event", + "value": "ss" + } + ] + } + ], + "tags": [] + }, + { + "traceId": "c4fffc7d-7c0d-4073-98ea-34362506d323", + "spanId": "14eeae8e-90c2-4e43-8a3a-2737b8a8dd55", + "parentSpanId": "9b0523d6-f14b-4a33-ba75-1e46720684cc", + "serviceName": "service_b", + "operationName": "oper_9", + "startTime": 1534487372725000, + "duration": 986000, + "logs": [ + { + "timestamp": 1534487372725000, + "fields": [ + { + "key": "event", + "value": "cs" + } + ] + }, + { + "timestamp": 1534487373711000, + "fields": [ + { + "key": "event", + "value": "cr" + } + ] + } + ], + "tags": [] + }, + { + "traceId": "c4fffc7d-7c0d-4073-98ea-34362506d323", + "spanId": "e799c8f3-7dd1-49e6-bb8a-df6f4f02966e", + "parentSpanId": "0e4ebf10-ad12-4d3c-bf4c-db2bafa64e60", + "serviceName": "service_d", + "operationName": "oper_10", + "startTime": 1534487372347000, + "duration": 81000, + "logs": [ + { + "timestamp": 1534487372347000, + "fields": [ + { + "key": "event", + "value": "cs" + } + ] + }, + { + "timestamp": 1534487372428000, + "fields": [ + { + "key": "event", + "value": "cr" + } + ] + } + ], + "tags": [] + }, + { + "traceId": "c4fffc7d-7c0d-4073-98ea-34362506d323", + "spanId": "0e4ebf10-ad12-4d3c-bf4c-db2bafa64e60", + "parentSpanId": "0e4ebf10-ad12-4d3c-bf4c-db2bafa64e60", + "serviceName": "service_d", + "operationName": "oper_11", + "startTime": 1534487372435000, + "duration": 146000, + "logs": [ + { + "timestamp": 1534487372435000, + "fields": [ + { + "key": "event", + "value": "cs" + } + ] + }, + { + "timestamp": 1534487372581000, + "fields": [ + { + "key": "event", + "value": "cr" + } + ] + } + ], + "tags": [] + }, + { + "traceId": "c4fffc7d-7c0d-4073-98ea-34362506d323", + "spanId": "fc1d74f3-94a4-4a95-86af-b6100bb6a337", + "parentSpanId": "0e4ebf10-ad12-4d3c-bf4c-db2bafa64e60", + "serviceName": "service_d", + "operationName": "oper_12", + "startTime": 1534487372691000, + "duration": 0, + "logs": [ + { + "timestamp": 1534487372691000, + "fields": [ + { + "key": "event", + "value": "cs" + } + ] + }, + { + "timestamp": 1534487372691000, + "fields": [ + { + "key": "event", + "value": "cr" + } + ] + } + ], + "tags": [] + }, + { + "traceId": "c4fffc7d-7c0d-4073-98ea-34362506d323", + "spanId": "0e4ebf10-ad12-4d3c-bf4c-db2bafa64e60", + "parentSpanId": "9b0523d6-f14b-4a33-ba75-1e46720684cc", + "serviceName": "service_d", + "operationName": "oper_13", + "startTime": 1534487372345000, + "duration": 348000, + "logs": [ + { + "timestamp": 1534487372345000, + "fields": [ + { + "key": "event", + "value": "sr" + } + ] + }, + { + "timestamp": 1534487372693000, + "fields": [ + { + "key": "event", + "value": "ss" + } + ] + } + ], + "tags": [] + }, + { + "traceId": "c4fffc7d-7c0d-4073-98ea-34362506d323", + "spanId": "435a043c-fcd9-4a81-8045-9d69bc9966c9", + "parentSpanId": "eb9b1f0d-c0e5-487e-9b50-a2175c69807e", + "serviceName": "service_d", + "operationName": "oper_12", + "startTime": 1534487372789000, + "duration": 0, + "logs": [ + { + "timestamp": 1534487372789000, + "fields": [ + { + "key": "event", + "value": "cs" + } + ] + }, + { + "timestamp": 1534487372789000, + "fields": [ + { + "key": "event", + "value": "cr" + } + ] + } + ], + "tags": [] + }, + { + "traceId": "c4fffc7d-7c0d-4073-98ea-34362506d323", + "spanId": "eb9b1f0d-c0e5-487e-9b50-a2175c69807e", + "parentSpanId": "9b0523d6-f14b-4a33-ba75-1e46720684cc", + "serviceName": "service_d", + "operationName": "oper_13", + "startTime": 1534487372748000, + "duration": 43000, + "logs": [ + { + "timestamp": 1534487372748000, + "fields": [ + { + "key": "event", + "value": "sr" + } + ] + }, + { + "timestamp": 1534487372791000, + "fields": [ + { + "key": "event", + "value": "ss" + } + ] + } + ], + "tags": [] + }, + { + "traceId": "c4fffc7d-7c0d-4073-98ea-34362506d323", + "spanId": "fc1d74f3-94a4-4a95-86af-b6100bb6a337", + "parentSpanId": "0e4ebf10-ad12-4d3c-bf4c-db2bafa64e60", + "serviceName": "service_e", + "operationName": "oper_14", + "startTime": 1534487372648000, + "duration": 44000, + "logs": [ + { + "timestamp": 1534487372648000, + "fields": [ + { + "key": "event", + "value": "sr" + } + ] + }, + { + "timestamp": 1534487372692000, + "fields": [ + { + "key": "event", + "value": "ss" + } + ] + } + ], + "tags": [] + }, + { + "traceId": "c4fffc7d-7c0d-4073-98ea-34362506d323", + "spanId": "435a043c-fcd9-4a81-8045-9d69bc9966c9", + "parentSpanId": "eb9b1f0d-c0e5-487e-9b50-a2175c69807e", + "serviceName": "service_e", + "operationName": "oper_14", + "startTime": 1534487372756000, + "duration": 34000, + "logs": [ + { + "timestamp": 1534487372756000, + "fields": [ + { + "key": "event", + "value": "sr" + } + ] + }, + { + "timestamp": 1534487372790000, + "fields": [ + { + "key": "event", + "value": "ss" + } + ] + } + ], + "tags": [] + }, + { + "traceId": "c4fffc7d-7c0d-4073-98ea-34362506d323", + "spanId": "9b0523d6-f14b-4a33-ba75-1e46720684cc", + "parentSpanId": "5fbe1254-1036-4d47-80b9-0cbf36fdd128", + "serviceName": "service_f", + "operationName": "oper_15", + "startTime": 1534487372849000, + "duration": 0, + "logs": [ + { + "timestamp": 1534487372849000, + "fields": [ + { + "key": "event", + "value": "cs" + } + ] + }, + { + "timestamp": 1534487372849000, + "fields": [ + { + "key": "event", + "value": "cr" + } + ] + } + ], + "tags": [] + }, + { + "traceId": "c4fffc7d-7c0d-4073-98ea-34362506d323", + "spanId": "5fbe1254-1036-4d47-80b9-0cbf36fdd128", + "parentSpanId": "", + "serviceName": "service_f", + "operationName": "oper_16", + "startTime": 1534487370131000, + "duration": 2844000, + "logs": [ + { + "timestamp": 1534487370131000, + "fields": [ + { + "key": "event", + "value": "sr" + } + ] + }, + { + "timestamp": 1534487372975000, + "fields": [ + { + "key": "event", + "value": "ss" + } + ] + } + ], + "tags": [] + }, + { + "traceId": "c4fffc7d-7c0d-4073-98ea-34362506d323", + "spanId": "095f1fa8-d920-4230-b142-6ccb0d9e7a04", + "parentSpanId": "", + "serviceName": "service_g", + "operationName": "oper_17", + "startTime": 1534487368981000, + "duration": 3019000, + "logs": [ + { + "timestamp": 1534487368981000, + "fields": [ + { + "key": "event", + "value": "sr" + } + ] + }, + { + "timestamp": 1534487372000000, + "fields": [ + { + "key": "event", + "value": "ss" + } + ] + } + ], + "tags": [] + }, + { + "traceId": "c4fffc7d-7c0d-4073-98ea-34362506d323", + "spanId": "5fbe1254-1036-4d47-80b9-0cbf36fdd128", + "parentSpanId": "095f1fa8-d920-4230-b142-6ccb0d9e7a04", + "serviceName": "service_g", + "operationName": "oper_18", + "startTime": 1534487368981000, + "duration": 3019000, + "logs": [ + { + "timestamp": 1534487368981000, + "fields": [ + { + "key": "event", + "value": "cs" + } + ] + }, + { + "timestamp": 1534487372000000, + "fields": [ + { + "key": "event", + "value": "cr" + } + ] + } + ], + "tags": [] + } + ] +} \ No newline at end of file diff --git a/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/integration/BaseIntegrationTestSpec.scala b/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/integration/BaseIntegrationTestSpec.scala new file mode 100644 index 000000000..c0c7b5cef --- /dev/null +++ b/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/integration/BaseIntegrationTestSpec.scala @@ -0,0 +1,338 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expedia.www.haystack.trace.reader.integration + +import java.text.SimpleDateFormat +import java.util.concurrent.{Executors, TimeUnit} +import java.util.{Date, UUID} + +import com.expedia.open.tracing.Span +import com.expedia.open.tracing.api.TraceReaderGrpc +import com.expedia.open.tracing.api.TraceReaderGrpc.TraceReaderBlockingStub +import com.expedia.open.tracing.backend.StorageBackendGrpc.StorageBackendBlockingStub +import com.expedia.open.tracing.backend.{StorageBackendGrpc, TraceRecord, WriteSpansRequest, WriteSpansResponse} +import com.expedia.open.tracing.buffer.SpanBuffer +import com.expedia.www.haystack.trace.commons.clients.es.document.TraceIndexDoc +import com.expedia.www.haystack.trace.commons.config.entities.{IndexFieldType, WhiteListIndexFields, WhitelistIndexField} +import com.expedia.www.haystack.trace.commons.packer.{PackerFactory, PackerType} +import com.expedia.www.haystack.trace.reader.Service +import com.expedia.www.haystack.trace.reader.unit.readers.builders.ValidTraceBuilder +import com.expedia.www.haystack.trace.storage.backends.memory.{Service => BackendService} +import com.google.protobuf.ByteString +import io.grpc.ManagedChannelBuilder +import io.grpc.health.v1.HealthGrpc +import io.searchbox.client.config.HttpClientConfig +import io.searchbox.client.{JestClient, JestClientFactory} +import io.searchbox.core.Index +import io.searchbox.indices.CreateIndex +import org.json4s.ext.EnumNameSerializer +import org.json4s.jackson.Serialization +import org.json4s.{DefaultFormats, Formats} +import org.scalatest._ + +import scala.collection.JavaConverters._ +import scala.collection.mutable + +trait BaseIntegrationTestSpec extends FunSpec with GivenWhenThen with Matchers with BeforeAndAfterAll with BeforeAndAfterEach with ValidTraceBuilder { + protected implicit val formats: Formats = DefaultFormats + new EnumNameSerializer(IndexFieldType) + protected var client: TraceReaderBlockingStub = _ + + protected var healthCheckClient: HealthGrpc.HealthBlockingStub = _ + + private val ELASTIC_SEARCH_ENDPOINT = "http://elasticsearch:9200" + private val ELASTIC_SEARCH_WHITELIST_INDEX = "reload-configs" + private val ELASTIC_SEARCH_WHITELIST_TYPE = "whitelist-index-fields" + private val SPANS_INDEX_TYPE = "spans" + + private val executors = Executors.newFixedThreadPool(2) + + private val DEFAULT_DURATION = TimeUnit.MILLISECONDS.toMicros(500) + + private val HAYSTACK_TRACES_INDEX = { + val date = new Date() + + val dateBucket = new SimpleDateFormat("yyyy-MM-dd").format(date) + val hourBucket = new SimpleDateFormat("HH").format(date).toInt / 6 + + s"haystack-traces-$dateBucket-$hourBucket" + } + private val INDEX_TEMPLATE = + """{ + | "template": "haystack-traces*", + | "settings": { + | "number_of_shards": 1, + | "index.mapping.ignore_malformed": true, + | "analysis": { + | "normalizer": { + | "lowercase_normalizer": { + | "type": "custom", + | "filter": ["lowercase"] + | } + | } + | } + | }, + | "aliases": { + | "haystack-traces": {} + | }, + | "mappings": { + | "spans": { + | "_all": { + | "enabled": false + | }, + | "_source": { + | "includes": ["traceid"] + | }, + | "properties": { + | "traceid": { + | "enabled": false + | }, + | "starttime": { + | "type": "long", + | "doc_values": true + | }, + | "spans": { + | "type": "nested", + | "properties": { + | "servicename": { + | "type": "keyword", + | "normalizer": "lowercase_normalizer", + | "doc_values": true, + | "norms": false + | }, + | "operationname": { + | "type": "keyword", + | "normalizer": "lowercase_normalizer", + | "doc_values": true, + | "norms": false + | }, + | "starttime": { + | "type": "long", + | "doc_values": true + | }, + | "duration": { + | "type": "long", + | "doc_values": true + | } + | } + | } + | }, + | "dynamic_templates": [{ + | "strings_as_keywords_1": { + | "match_mapping_type": "string", + | "mapping": { + | "type": "keyword", + | "normalizer": "lowercase_normalizer", + | "doc_values": false, + | "norms": false + | } + | } + | }, { + | "longs_disable_doc_norms": { + | "match_mapping_type": "long", + | "mapping": { + | "type": "long", + | "doc_values": false, + | "norms": false + | } + | } + | }] + | } + | } + |} + |""".stripMargin + + + private var esClient: JestClient = _ + private var traceBackendClient: StorageBackendBlockingStub = _ + + def setupTraceBackend(): StorageBackendBlockingStub = { + val port = 8090 + executors.submit(new Runnable { + override def run(): Unit = BackendService.main(Array { + port.toString + }) + }) + traceBackendClient = StorageBackendGrpc.newBlockingStub( + ManagedChannelBuilder.forAddress("localhost", port) + .usePlaintext(true) + .build()) + traceBackendClient + } + + override def beforeAll() { + // setup traceBackend + traceBackendClient = setupTraceBackend() + + // setup elasticsearch + val factory = new JestClientFactory() + factory.setHttpClientConfig( + new HttpClientConfig.Builder(ELASTIC_SEARCH_ENDPOINT) + .multiThreaded(true) + .build()) + esClient = factory.getObject + esClient.execute(new CreateIndex.Builder(HAYSTACK_TRACES_INDEX) + .settings(INDEX_TEMPLATE) + .build) + + executors.submit(new Runnable { + override def run(): Unit = Service.main(null) + }) + + Thread.sleep(5000) + + client = TraceReaderGrpc.newBlockingStub(ManagedChannelBuilder.forAddress("localhost", 8088) + .usePlaintext(true) + .build()) + + healthCheckClient = HealthGrpc.newBlockingStub(ManagedChannelBuilder.forAddress("localhost", 8088) + .usePlaintext(true) + .build()) + } + + + protected def putTraceInEsAndTraceBackend(traceId: String = UUID.randomUUID().toString, + spanId: String = UUID.randomUUID().toString, + serviceName: String = "", + operationName: String = "", + tags: Map[String, String] = Map.empty, + startTime: Long = System.currentTimeMillis() * 1000, + sleep: Boolean = true, + duration: Long = DEFAULT_DURATION): Unit = { + insertTraceInBackend(traceId, spanId, serviceName, operationName, tags, startTime, duration) + insertTraceInEs(traceId, spanId, serviceName, operationName, tags, startTime, duration) + + // wait for few sec to let ES refresh its index + if (sleep) Thread.sleep(5000) + } + + private def insertTraceInEs(traceId: String, + spanId: String, + serviceName: String, + operationName: String, + tags: Map[String, String], + startTime: Long, + duration: Long) = { + import TraceIndexDoc._ + // create map using service, operation and tags + val fieldMap: mutable.Map[String, Any] = mutable.Map( + SERVICE_KEY_NAME -> serviceName, + OPERATION_KEY_NAME -> operationName, + START_TIME_KEY_NAME -> mutable.Set[Any](startTime), + DURATION_KEY_NAME -> mutable.Set[Any](duration) + ) + tags.foreach(pair => fieldMap.put(pair._1.toLowerCase(), pair._2)) + + // index the document + val result = esClient.execute(new Index.Builder(TraceIndexDoc(traceId, 0, startTime, Seq(fieldMap)).json) + .index(HAYSTACK_TRACES_INDEX) + .`type`(SPANS_INDEX_TYPE) + .build) + + if (result.getErrorMessage != null) { + fail("Fail to execute the indexing request " + result.getErrorMessage) + } + } + + case class FieldWithMetadata(name: String, isRangeQuery: Boolean) + + protected def putWhitelistIndexFieldsInEs(fields: List[FieldWithMetadata]): Unit = { + val whitelistFields = for (field <- fields) yield WhitelistIndexField(field.name, IndexFieldType.string, aliases = Set(s"_${field.name}"), field.isRangeQuery) + esClient.execute(new Index.Builder(Serialization.write(WhiteListIndexFields(whitelistFields))) + .index(ELASTIC_SEARCH_WHITELIST_INDEX) + .`type`(ELASTIC_SEARCH_WHITELIST_TYPE) + .build) + + // wait for few sec to let ES refresh its index and app to reload its config + Thread.sleep(10000) + } + + private def insertTraceInBackend(traceId: String, + spanId: String, + serviceName: String, + operationName: String, + tags: Map[String, String], + startTime: Long, + duration: Long): WriteSpansResponse = { + val spanBuffer = createSpanBufferWithSingleSpan(traceId, spanId, serviceName, operationName, tags, startTime, duration) + writeToBackend(spanBuffer, traceId) + } + + protected def putTraceInBackend(traceId: String, + spanId: String = UUID.randomUUID().toString, + serviceName: String = "", + operationName: String = "", + tags: Map[String, String] = Map.empty, + startTime: Long = System.currentTimeMillis() * 1000, + duration: Long = DEFAULT_DURATION): Unit = { + insertTraceInBackend(traceId, spanId, serviceName, operationName, tags, startTime, duration) + // wait for few sec to let ES refresh its index + Thread.sleep(1000) + } + + protected def putTraceInBackendWithPartialSpans(traceId: String): WriteSpansResponse = { + val trace = buildMultiServiceTrace() + val spanBuffer = SpanBuffer + .newBuilder() + .setTraceId(traceId) + .addAllChildSpans(trace.getChildSpansList) + .build() + + writeToBackend(spanBuffer, traceId) + } + + private def writeToBackend(spanBuffer: SpanBuffer, traceId: String): WriteSpansResponse = { + val packer = PackerFactory.spanBufferPacker(PackerType.NONE) + + val traceRecord = TraceRecord + .newBuilder() + .setTraceId(traceId) + .setSpans(ByteString.copyFrom(packer.apply(spanBuffer).packedDataBytes)) + .build() + + + val writeSpanRequest = WriteSpansRequest.newBuilder() + .addRecords(traceRecord) + .build() + + traceBackendClient.writeSpans(writeSpanRequest) + } + + private def createSpanBufferWithSingleSpan(traceId: String, + spanId: String, + serviceName: String, + operationName: String, + tags: Map[String, String], + startTime: Long, + duration: Long) = { + val spanTags = tags.map(tag => com.expedia.open.tracing.Tag.newBuilder().setKey(tag._1).setVStr(tag._2).build()) + + SpanBuffer + .newBuilder() + .setTraceId(traceId) + .addChildSpans(Span + .newBuilder() + .setTraceId(traceId) + .setSpanId(spanId) + .setOperationName(operationName) + .setServiceName(serviceName) + .setStartTime(startTime) + .setDuration(duration) + .addAllTags(spanTags.asJava) + .build()) + .build() + } +} diff --git a/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/integration/TraceServiceIntegrationTestSpec.scala b/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/integration/TraceServiceIntegrationTestSpec.scala new file mode 100644 index 000000000..1c870505a --- /dev/null +++ b/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/integration/TraceServiceIntegrationTestSpec.scala @@ -0,0 +1,538 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expedia.www.haystack.trace.reader.integration + +import java.util.UUID + +import com.expedia.open.tracing.api.ExpressionTree.Operator +import com.expedia.open.tracing.api._ +import com.expedia.www.haystack.trace.commons.clients.es.document.TraceIndexDoc +import io.grpc.health.v1.{HealthCheckRequest, HealthCheckResponse} +import io.grpc.{Status, StatusRuntimeException} + +import scala.collection.JavaConverters._ +import scala.collection.mutable + +class TraceServiceIntegrationTestSpec extends BaseIntegrationTestSpec { + + describe("TraceReader.getFieldNames") { + it("should return names of enabled fields") { + Given("trace in trace-backend and elasticsearch") + val field1 = FieldWithMetadata("abc", isRangeQuery = true) + val field2 = FieldWithMetadata("def", isRangeQuery = false) + putWhitelistIndexFieldsInEs(List(field1, field2)) + + When("calling getFieldNames") + val fieldNames = client.getFieldNames(Empty.newBuilder().build()) + + Then("should return fieldNames available in index") + fieldNames.getNamesCount should be(2) + fieldNames.getFieldMetadataCount should be(2) + fieldNames.getNamesList.asScala.toList should contain allOf("abc", "def") + } + } + + describe("TraceReader.getFieldValues") { + it("should return values of a given fields") { + Given("trace in trace-backend and elasticsearch") + val serviceName = "get_values_servicename" + putTraceInEsAndTraceBackend(UUID.randomUUID().toString, UUID.randomUUID().toString, serviceName, "op") + val request = FieldValuesRequest.newBuilder() + .setFieldName(TraceIndexDoc.SERVICE_KEY_NAME) + .build() + + When("calling getFieldValues") + val result = client.getFieldValues(request) + + Then("should return possible values for given field") + result.getValuesList.asScala should contain(serviceName) + } + + it("should return values of a given fields with filters") { + Given("trace in trace-backend and elasticsearch") + val serviceName = "get_values_with_filters_servicename" + val op1 = "get_values_with_filters_operationname_1" + val op2 = "get_values_with_filters_operationname_2" + + putTraceInEsAndTraceBackend(UUID.randomUUID().toString, UUID.randomUUID().toString, serviceName, op1) + putTraceInEsAndTraceBackend(UUID.randomUUID().toString, UUID.randomUUID().toString, serviceName, op2) + putTraceInEsAndTraceBackend(UUID.randomUUID().toString, UUID.randomUUID().toString, "non_matching_servicename", "non_matching_operationname") + + val request = FieldValuesRequest.newBuilder() + .addFilters(Field.newBuilder().setName(TraceIndexDoc.SERVICE_KEY_NAME).setValue(serviceName)) + .setFieldName(TraceIndexDoc.OPERATION_KEY_NAME) + .build() + + When("calling getFieldValues") + val result = client.getFieldValues(request) + + Then("should return filtered values for given field") + result.getValuesList.size() should be(2) + result.getValuesList.asScala should contain allOf(op1, op2) + } + } + + describe("TraceReader.getTrace") { + it("should get trace for given traceID from trace-backend") { + Given("trace in trace-backend") + val traceId = UUID.randomUUID().toString + putTraceInBackend(traceId) + + When("getTrace is invoked") + val trace = client.getTrace(TraceRequest.newBuilder().setTraceId(traceId).build()) + + Then("should return the trace") + trace.getTraceId shouldBe traceId + } + + it("should return TraceNotFound exception if traceID is not in trace-backend") { + Given("trace in trace-backend") + putTraceInBackend(UUID.randomUUID().toString) + + When("getTrace is invoked") + val thrown = the[StatusRuntimeException] thrownBy { + client.getTrace(TraceRequest.newBuilder().setTraceId(UUID.randomUUID().toString).build()) + } + + Then("thrown StatusRuntimeException should have 'not found' error") + thrown.getStatus.getCode should be(Status.NOT_FOUND.getCode) + thrown.getStatus.getDescription should include("traceId not found") + } + } + + describe("TraceReader.getRawTrace") { + it("should get trace for given traceID from trace-backend") { + Given("trace in trace-backend") + val traceId = UUID.randomUUID().toString + putTraceInBackend(traceId) + + When("getRawTrace is invoked") + val trace = client.getRawTrace(TraceRequest.newBuilder().setTraceId(traceId).build()) + + Then("should return the trace") + trace.getTraceId shouldBe traceId + } + + it("should return TraceNotFound exception if traceID is not in trace-backend") { + Given("trace in trace-backend") + putTraceInBackend(UUID.randomUUID().toString) + + When("getRawTrace is invoked") + val thrown = the[StatusRuntimeException] thrownBy { + client.getRawTrace(TraceRequest.newBuilder().setTraceId(UUID.randomUUID().toString).build()) + } + + Then("thrown StatusRuntimeException should have 'not found' error") + thrown.getStatus.getCode should be(Status.NOT_FOUND.getCode) + thrown.getStatus.getDescription should include("traceId not found") + } + } + + describe("TraceReader.getRawSpan") { + it("should get spanId for given traceID-spanId from trace-backend") { + Given("trace in trace-backend") + val traceId = UUID.randomUUID().toString + val spanId = UUID.randomUUID().toString + putTraceInBackend(traceId, spanId, "svc1") + putTraceInBackend(traceId, spanId, "svc2") + + + When("getRawSpan is invoked") + val spanResponse = client.getRawSpan(SpanRequest + .newBuilder() + .setTraceId(traceId) + .setSpanId(spanId) + .build()) + + Then("should return the trace") + spanResponse.getSpansCount shouldBe 2 + val servicesObserved = mutable.Set[String]() + spanResponse.getSpansList.asScala foreach { span => + span.getTraceId shouldBe traceId + span.getSpanId shouldBe spanId + servicesObserved += span.getServiceName + } + servicesObserved should contain allOf("svc1", "svc2") + } + + it("should return TraceNotFound exception if traceID is not in trace-backend") { + Given("trace in trace-backend") + putTraceInBackend(UUID.randomUUID().toString) + + When("getRawSpan is invoked") + val thrown = the[StatusRuntimeException] thrownBy { + client.getRawSpan(SpanRequest + .newBuilder() + .setTraceId(UUID.randomUUID().toString) + .setSpanId(UUID.randomUUID().toString) + .build()) + } + + Then("thrown StatusRuntimeException should have 'traceId not found' error") + thrown.getStatus.getCode should be(Status.NOT_FOUND.getCode) + thrown.getStatus.getDescription should include("traceId not found") + } + + it("should return SpanNotFound exception if spanId is not part of Trace") { + Given("trace in trace-backend") + val traceId = UUID.randomUUID().toString + putTraceInBackend(traceId) + + When("getRawSpan is invoked") + val thrown = the[StatusRuntimeException] thrownBy { + client.getRawSpan(SpanRequest + .newBuilder() + .setTraceId(traceId) + .setSpanId(UUID.randomUUID().toString) + .build()) + } + + Then("thrown StatusRuntimeException should have 'spanId not found' error") + thrown.getStatus.getCode should be(Status.NOT_FOUND.getCode) + thrown.getStatus.getDescription should include("spanId not found") + } + } + + describe("TraceReader.searchTraces") { + it("should search traces for given service and operation") { + Given("trace in trace-backend and elasticsearch") + val traceId = UUID.randomUUID().toString + val spanId = UUID.randomUUID().toString + val serviceName = "svcName" + val operationName = "opName" + val startTime = 1 + val endTime = (System.currentTimeMillis() + 10000000) * 1000 + putTraceInEsAndTraceBackend(traceId, spanId, serviceName, operationName) + + When("searching traces for service and operation") + val traces = client.searchTraces(TracesSearchRequest + .newBuilder() + .addFields(Field.newBuilder().setName(TraceIndexDoc.SERVICE_KEY_NAME).setValue(serviceName).build()) + .addFields(Field.newBuilder().setName(TraceIndexDoc.OPERATION_KEY_NAME).setValue(operationName).build()) + .setStartTime(startTime) + .setEndTime(endTime) + .setLimit(10) + .build()) + + Then("should return traces for the searched service and operation name") + traces.getTracesList.size() should be > 0 + traces.getTraces(0).getTraceId shouldBe traceId + traces.getTraces(0).getChildSpans(0).getServiceName shouldBe serviceName + traces.getTraces(0).getChildSpans(0).getOperationName shouldBe operationName + } + + it("should search traces for given service") { + Given("traces in trace-backend and elasticsearch") + val traceId1 = UUID.randomUUID().toString + val traceId2 = UUID.randomUUID().toString + val serviceName = "serviceToSearch" + val operationName = "opName" + val startTime = 1 + val endTime = (System.currentTimeMillis() + 10000000) * 1000 + putTraceInEsAndTraceBackend(traceId1, UUID.randomUUID().toString, serviceName, operationName) + putTraceInEsAndTraceBackend(traceId2, UUID.randomUUID().toString, serviceName, operationName) + + When("searching traces for service") + val traces = client.searchTraces(TracesSearchRequest + .newBuilder() + .addFields(Field.newBuilder().setName(TraceIndexDoc.SERVICE_KEY_NAME).setValue(serviceName).build()) + .setStartTime(startTime) + .setEndTime(endTime) + .setLimit(10) + .build()) + + Then("should return all traces for the service") + traces.getTracesList.size() should be(2) + traces.getTracesList.asScala.exists(_.getTraceId == traceId1) shouldBe true + traces.getTracesList.asScala.exists(_.getTraceId == traceId2) shouldBe true + } + + it("should not return traces for unavailable searches") { + Given("traces in trace-backend and elasticsearch") + + When("searching traces for service") + val traces = client.searchTraces(TracesSearchRequest + .newBuilder() + .addFields(Field.newBuilder().setName(TraceIndexDoc.SERVICE_KEY_NAME).setValue("unavailableService").build()) + .setStartTime(1) + .setEndTime((System.currentTimeMillis() + 10000000) * 1000) + .setLimit(10) + .build()) + + Then("should not return traces") + traces.getTracesList.size() should be(0) + } + + it("should search traces for given whitelisted tags") { + Given("traces with tags in trace-backend and elasticsearch") + val traceId = UUID.randomUUID().toString + val serviceName = "svcWhitelisteTags" + val operationName = "opWhitelisteTags" + val tags = Map("aKey" -> "aValue", "bKey" -> "bValue") + val startTime = 1 + val endTime = (System.currentTimeMillis() + 10000000) * 1000 + putTraceInEsAndTraceBackend(traceId, UUID.randomUUID().toString, serviceName, operationName, tags) + + When("searching traces for tags") + val traces = client.searchTraces(TracesSearchRequest + .newBuilder() + .addFields(Field.newBuilder().setName(TraceIndexDoc.SERVICE_KEY_NAME).setValue(serviceName).build()) + .addFields(Field.newBuilder().setName("akey").setValue("avalue").build()) + .addFields(Field.newBuilder().setName("bkey").setValue("bvalue").build()) + .setStartTime(startTime) + .setEndTime(endTime) + .setLimit(10) + .build()) + + Then("should return traces having tags") + traces.getTracesList.asScala.exists(_.getTraceId == traceId) shouldBe true + } + + it("should not return traces if tags are not available") { + Given("traces with tags in trace-backend and elasticsearch") + val traceId = UUID.randomUUID().toString + val serviceName = "svcWhitelisteTags" + val operationName = "opWhitelisteTags" + val tags = Map("cKey" -> "cValue", "dKey" -> "dValue") + val startTime = 1 + val endTime = (System.currentTimeMillis() + 10000000) * 1000 + putTraceInEsAndTraceBackend(traceId, UUID.randomUUID().toString, serviceName, operationName, tags) + + When("searching traces for tags") + val traces = client.searchTraces(TracesSearchRequest + .newBuilder() + .addFields(Field.newBuilder().setName(TraceIndexDoc.SERVICE_KEY_NAME).setValue(serviceName).build()) + .addFields(Field.newBuilder().setName("ckey").setValue("cvalue").build()) + .addFields(Field.newBuilder().setName("akey").setValue("avalue").build()) + .setStartTime(startTime) + .setEndTime(endTime) + .setLimit(10) + .build()) + + Then("should not return traces") + traces.getTracesList.asScala.exists(_.getTraceId == traceId) shouldBe false + } + + it("should return traces for expression tree based search targeting ") { + Given("traces with tags in trace-backend and elasticsearch") + val traceId = UUID.randomUUID().toString + val serviceName = "expressionTraceSvc" + val operationName = "expressionTraceOp" + val tags = Map("uKey" -> "uValue", "vKey" -> "vValue") + val startTime = 1 + val endTime = (System.currentTimeMillis() + 10000000) * 1000 + putTraceInEsAndTraceBackend(traceId, UUID.randomUUID().toString, serviceName, operationName, tags) + + When("searching traces for tags using expression tree") + val expression = ExpressionTree + .newBuilder() + .setOperator(Operator.AND) + .addOperands(Operand.newBuilder().setField(Field.newBuilder().setName(TraceIndexDoc.SERVICE_KEY_NAME).setValue(serviceName))) + + val traces = client.searchTraces(TracesSearchRequest + .newBuilder() + .setFilterExpression(expression) + .setStartTime(startTime) + .setEndTime(endTime) + .setLimit(10) + .build()) + + Then("should return traces") + traces.getTracesList.asScala.exists(_.getTraceId == traceId) shouldBe true + } + + it("should not return traces for expression tree using not_equal operator based search") { + Given("traces with tags in trace-backend and elasticsearch") + val traceId = UUID.randomUUID().toString + val serviceName = "expressionTraceSvc" + val operationName = "expressionTraceOp" + val tags = Map("uKey" -> "uValue", "vKey" -> "vValue") + val startTime = 1 + val endTime = (System.currentTimeMillis() + 10000000) * 1000 + putTraceInEsAndTraceBackend(traceId, UUID.randomUUID().toString, serviceName, operationName, tags) + + When("searching traces for tags using expression tree") + val expression = ExpressionTree + .newBuilder() + .setOperator(Operator.AND) + .addOperands(Operand.newBuilder().setField(Field.newBuilder().setName(TraceIndexDoc.SERVICE_KEY_NAME).setValue(serviceName))) + .addOperands(Operand.newBuilder().setField(Field.newBuilder().setName(TraceIndexDoc.OPERATION_KEY_NAME).setValue(operationName).setOperator(Field.Operator.NOT_EQUAL))) + + val traces = client.searchTraces(TracesSearchRequest + .newBuilder() + .setFilterExpression(expression) + .setStartTime(startTime) + .setEndTime(endTime) + .setLimit(10) + .build()) + + Then("should not return traces") + traces.getTracesList.size() shouldBe 0 + } + + it("should return traces for expression tree using not_equal operator based search") { + Given("traces with tags in trace-backend and elasticsearch") + val traceId = UUID.randomUUID().toString + val serviceName = "expressionTraceSvc_1" + val operationName = "expressionTraceOp_1" + val tags = Map("uKey" -> "uValue", "vKey" -> "vValue") + val startTime = 1 + val endTime = (System.currentTimeMillis() + 10000000) * 1000 + putTraceInEsAndTraceBackend(traceId, UUID.randomUUID().toString, serviceName, operationName, tags) + + When("searching traces for tags using expression tree") + val expression = ExpressionTree + .newBuilder() + .setOperator(Operator.AND) + .addOperands(Operand.newBuilder().setField(Field.newBuilder().setName(TraceIndexDoc.SERVICE_KEY_NAME).setValue(serviceName))) + .addOperands(Operand.newBuilder().setField(Field.newBuilder().setName(TraceIndexDoc.OPERATION_KEY_NAME).setValue("somethingelse").setOperator(Field.Operator.NOT_EQUAL))) + + val traces = client.searchTraces(TracesSearchRequest + .newBuilder() + .setFilterExpression(expression) + .setStartTime(startTime) + .setEndTime(endTime) + .setLimit(10) + .build()) + + Then("should return traces") + traces.getTracesList.asScala.exists(_.getTraceId == traceId) shouldBe true + } + + it("should return traces for expression tree with duration filter") { + Given("traces with tags in trace-backend and elasticsearch") + val traceId = UUID.randomUUID().toString + val serviceName = "expressionTraceSvc_1" + val operationName = "expressionTraceOp_1" + val tags = Map("uKey" -> "uValue", "vKey" -> "vValue") + val startTime = 1 + val endTime = (System.currentTimeMillis() + 10000000) * 1000 + putTraceInEsAndTraceBackend(traceId, UUID.randomUUID().toString, serviceName, operationName, tags) + + When("searching traces for tags using expression tree") + val baseExpr = ExpressionTree + .newBuilder() + .setOperator(Operator.AND) + .setIsSpanLevelExpression(true) + .addOperands(Operand.newBuilder().setField(Field.newBuilder().setName(TraceIndexDoc.SERVICE_KEY_NAME).setValue(serviceName))) + + val greaterThanExpr = baseExpr.addOperands(Operand.newBuilder().setField(Field.newBuilder().setName(TraceIndexDoc.DURATION_KEY_NAME).setOperator(Field.Operator.GREATER_THAN).setValue("300000"))) + val nonEmptyTraces = client.searchTraces(TracesSearchRequest + .newBuilder() + .setFilterExpression(greaterThanExpr) + .setStartTime(startTime) + .setEndTime(endTime) + .setLimit(10) + .build()) + + val lessThanExpr = baseExpr.addOperands(Operand.newBuilder().setField(Field.newBuilder().setName(TraceIndexDoc.DURATION_KEY_NAME).setOperator(Field.Operator.LESS_THAN).setValue("300000"))) + val emptyTraces = client.searchTraces(TracesSearchRequest + .newBuilder() + .setFilterExpression(lessThanExpr) + .setStartTime(startTime) + .setEndTime(endTime) + .setLimit(10) + .build()) + + Then("should return traces") + nonEmptyTraces.getTracesList.asScala.exists(_.getTraceId == traceId) shouldBe true + emptyTraces.getTracesList shouldBe empty + } + } + + describe("TraceReader.getTraceCallGraph") { + it("should get trace for given traceID from trace-backend") { + Given("trace in trace-backend") + val traceId = "traceId" + putTraceInBackendWithPartialSpans(traceId) + + When("getTrace is invoked") + val traceCallGraph = client.getTraceCallGraph(TraceRequest.newBuilder().setTraceId(traceId).build()) + + Then("should return the trace call graph") + traceCallGraph.getCallsCount should be(3) + } + } + + describe("TraceReader.getTraceCounts") { + it("should return trace counts histogram for given time span") { + Given("traces elasticsearch") + val serviceName = "dummy-servicename" + val operationName = "dummy-operationname" + val currentTimeMicros = System.currentTimeMillis() * 1000l + + val bucketIntervalInMicros = 10l * 1000 * 10000 + val bucketCount = 4 + val randomStartTimes = 0 until bucketCount map (idx => currentTimeMicros - (bucketIntervalInMicros * idx)) + val startTimeInMicroSec = currentTimeMicros - (bucketIntervalInMicros * bucketCount) + val endTimeInMicroSec = currentTimeMicros + + randomStartTimes.foreach(startTime => + putTraceInEsAndTraceBackend(serviceName = serviceName, operationName = operationName, startTime = startTime, sleep = false)) + Thread.sleep(5000) + + When("calling getTraceCounts") + val traceCountsRequest = TraceCountsRequest + .newBuilder() + .addFields(Field.newBuilder().setName(TraceIndexDoc.SERVICE_KEY_NAME).setValue(serviceName).build()) + .addFields(Field.newBuilder().setName(TraceIndexDoc.OPERATION_KEY_NAME).setValue(operationName).build()) + .setStartTime(startTimeInMicroSec) + .setEndTime(endTimeInMicroSec) + .setInterval(bucketIntervalInMicros) + .build() + + val traceCounts = client.getTraceCounts(traceCountsRequest) + + Then("should return possible values for given field") + traceCounts.getTraceCountCount shouldEqual bucketCount + traceCounts.getTraceCountList.asScala.foreach(_.getCount shouldBe 1) + } + } + + describe("TraceReader.getRawTraces") { + it("should get raw traces for given traceIds from trace-backend") { + Given("traces in trace-backend") + val traceId1 = UUID.randomUUID().toString + val spanId1 = UUID.randomUUID().toString + val spanId2 = UUID.randomUUID().toString + putTraceInBackend(traceId1, spanId1, "svc1", "oper1") + putTraceInBackend(traceId1, spanId2, "svc2", "oper2") + + val traceId2 = UUID.randomUUID().toString + val spanId3 = UUID.randomUUID().toString + putTraceInBackend(traceId2, spanId3, "svc1", "oper1") + + When("getRawTraces is invoked") + val tracesResult = client.getRawTraces(RawTracesRequest.newBuilder().addAllTraceId(Seq(traceId1, traceId2).asJava).build()) + + Then("should return the traces") + val traceIdSpansMap: Map[String, Set[String]] = tracesResult.getTracesList.asScala + .map(trace => trace.getTraceId -> trace.getChildSpansList.asScala.map(_.getSpanId).toSet).toMap + + traceIdSpansMap(traceId1) shouldEqual Set(spanId1, spanId2) + traceIdSpansMap(traceId2) shouldEqual Set(spanId3) + } + } + + describe("TraceReader.HealthCheck") { + it("should return SERVING as health check response") { + val request = HealthCheckRequest.newBuilder().build() + val response = healthCheckClient.check(request) + response.getStatus shouldEqual HealthCheckResponse.ServingStatus.SERVING + } + } +} diff --git a/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/BaseUnitTestSpec.scala b/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/BaseUnitTestSpec.scala new file mode 100644 index 000000000..a18ffc8c3 --- /dev/null +++ b/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/BaseUnitTestSpec.scala @@ -0,0 +1,22 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expedia.www.haystack.trace.reader.unit + +import org.scalatest.easymock.EasyMockSugar +import org.scalatest.{FunSpec, GivenWhenThen, Matchers} + +trait BaseUnitTestSpec extends FunSpec with GivenWhenThen with Matchers with EasyMockSugar diff --git a/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/config/ConfigurationLoaderSpec.scala b/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/config/ConfigurationLoaderSpec.scala new file mode 100644 index 000000000..89f6c77d9 --- /dev/null +++ b/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/config/ConfigurationLoaderSpec.scala @@ -0,0 +1,88 @@ +/* + * Copyright 2019, Expedia Group. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.expedia.www.haystack.trace.reader.unit.config + +import com.expedia.www.haystack.trace.reader.config.ProviderConfiguration +import com.expedia.www.haystack.trace.reader.config.entities.{ServiceConfiguration, TraceTransformersConfiguration} +import com.expedia.www.haystack.trace.reader.readers.transformers.{ClientServerEventLogTransformer, DeDuplicateSpanTransformer, InfrastructureTagTransformer, PartialSpanTransformer} +import com.expedia.www.haystack.trace.reader.unit.BaseUnitTestSpec + +class ConfigurationLoaderSpec extends BaseUnitTestSpec { + describe("ConfigurationLoader") { + it("should load the service config from base.conf") { + val serviceConfig: ServiceConfiguration = new ProviderConfiguration().serviceConfig + serviceConfig.port shouldBe 8088 + serviceConfig.ssl.enabled shouldBe false + serviceConfig.ssl.certChainFilePath shouldBe "/ssl/cert" + serviceConfig.ssl.privateKeyPath shouldBe "/ssl/private-key" + serviceConfig.maxSizeInBytes shouldBe 52428800 + } + + it("should load the trace transformers") { + val traceConfig: TraceTransformersConfiguration = new ProviderConfiguration().traceTransformerConfig + traceConfig.postTransformers.length shouldBe 3 + traceConfig.postTransformers.head.isInstanceOf[PartialSpanTransformer] shouldBe true + traceConfig.preTransformers.length shouldBe 3 + traceConfig.preTransformers.head.isInstanceOf[DeDuplicateSpanTransformer] shouldBe true + traceConfig.preTransformers(1).isInstanceOf[ClientServerEventLogTransformer] shouldBe true + traceConfig.preTransformers(2).isInstanceOf[InfrastructureTagTransformer] shouldBe true + } + + it("should load the trace validators") { + val traceConfig: TraceTransformersConfiguration = new ProviderConfiguration().traceTransformerConfig + traceConfig.postTransformers.length shouldBe 3 + traceConfig.postTransformers.head.isInstanceOf[PartialSpanTransformer] shouldBe true + traceConfig.preTransformers.length shouldBe 3 + traceConfig.preTransformers.head.isInstanceOf[DeDuplicateSpanTransformer] shouldBe true + traceConfig.preTransformers(1).isInstanceOf[ClientServerEventLogTransformer] shouldBe true + traceConfig.preTransformers(2).isInstanceOf[InfrastructureTagTransformer] shouldBe true + } + + it("should load elastic search configuration") { + + + val elasticSearchConfig = new ProviderConfiguration().elasticSearchConfiguration + + elasticSearchConfig.clientConfiguration.endpoint shouldEqual "http://elasticsearch:9200" + elasticSearchConfig.clientConfiguration.connectionTimeoutMillis shouldEqual 10000 + elasticSearchConfig.clientConfiguration.readTimeoutMillis shouldEqual 5000 + + + elasticSearchConfig.spansIndexConfiguration.indexHourBucket shouldEqual 6 + elasticSearchConfig.spansIndexConfiguration.indexHourTtl shouldEqual 72 + elasticSearchConfig.spansIndexConfiguration.useRootDocumentStartTime shouldEqual true + elasticSearchConfig.spansIndexConfiguration.indexType shouldEqual "spans" + elasticSearchConfig.spansIndexConfiguration.indexNamePrefix shouldEqual "haystack-traces" + + + elasticSearchConfig.serviceMetadataIndexConfiguration.enabled shouldEqual false + elasticSearchConfig.serviceMetadataIndexConfiguration.indexName shouldEqual "service_metadata" + elasticSearchConfig.serviceMetadataIndexConfiguration.indexType shouldEqual "metadata" + + elasticSearchConfig.awsRequestSigningConfiguration.enabled shouldEqual false + elasticSearchConfig.awsRequestSigningConfiguration.region shouldEqual "us-west-2" + elasticSearchConfig.awsRequestSigningConfiguration.awsServiceName shouldEqual "es" + elasticSearchConfig.awsRequestSigningConfiguration.accessKey shouldBe None + elasticSearchConfig.awsRequestSigningConfiguration.secretKey shouldBe None + } + + it("should load trace backend configuration") { + val traceBackendConfig = new ProviderConfiguration().traceBackendConfiguration + traceBackendConfig.backends.head.host shouldEqual "localhost" + traceBackendConfig.backends.head.port shouldEqual 8090 + } + } +} diff --git a/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/readers/TraceMergerSpec.scala b/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/readers/TraceMergerSpec.scala new file mode 100644 index 000000000..e0d4d7fd7 --- /dev/null +++ b/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/readers/TraceMergerSpec.scala @@ -0,0 +1,61 @@ +/* + * Copyright 2019 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expedia.www.haystack.trace.reader.unit.readers + +import com.expedia.open.tracing.Span +import com.expedia.open.tracing.api.Trace +import com.expedia.www.haystack.trace.reader.readers.utils.TraceMerger +import com.expedia.www.haystack.trace.reader.unit.BaseUnitTestSpec + +class TraceMergerSpec extends BaseUnitTestSpec { + describe("Trace Merger") { + it("should merge the traces by traceId") { + val trace_1 = buildTrace("t1", "s1", "svc1", "op1") + val trace_2 = buildTrace("t2", "s2", "svc2", "op1") + val trace_3 = buildTrace("t1", "s3", "svc3", "op1") + val trace_4 = buildTrace("t2", "s4", "svc4", "op1") + val trace_5 = buildTrace("t3", "s5", "svc5", "op1") + + val mergedTraces = TraceMerger.merge(Seq(trace_1, trace_2, trace_3, trace_4, trace_5)) + mergedTraces.size shouldBe 3 + val t1 = mergedTraces.find(_.getTraceId == "t1").head + t1.getChildSpansCount shouldBe 2 + t1.getChildSpans(0).getTraceId shouldBe "t1" + t1.getChildSpans(0).getSpanId shouldBe "s1" + t1.getChildSpans(1).getTraceId shouldBe "t1" + t1.getChildSpans(1).getSpanId shouldBe "s3" + t1.getChildSpans(0).getServiceName shouldBe "svc1" + t1.getChildSpans(1).getServiceName shouldBe "svc3" + + val t2 = mergedTraces.find(_.getTraceId == "t2").head + t2.getChildSpansCount shouldBe 2 + t2.getChildSpans(0).getSpanId shouldBe "s2" + t2.getChildSpans(1).getSpanId shouldBe "s4" + + mergedTraces.find(_.getTraceId == "t3").head.getChildSpansCount shouldBe 1 + } + } + + private def buildTrace(traceId: String, spanId: String, serviceName: String, operationName: String): Trace = { + Trace.newBuilder().setTraceId(traceId).addChildSpans( + Span.newBuilder() + .setSpanId(spanId) + .setTraceId(traceId) + .setServiceName(serviceName) + .setOperationName(operationName)).build() + } +} diff --git a/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/readers/TraceProcessorSpec.scala b/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/readers/TraceProcessorSpec.scala new file mode 100644 index 000000000..cd1ce01a7 --- /dev/null +++ b/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/readers/TraceProcessorSpec.scala @@ -0,0 +1,383 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expedia.www.haystack.trace.reader.unit.readers + +import com.expedia.open.tracing.api.Trace +import com.expedia.www.haystack.trace.reader.readers.TraceProcessor +import com.expedia.www.haystack.trace.reader.readers.transformers._ +import com.expedia.www.haystack.trace.reader.readers.validators._ +import com.expedia.www.haystack.trace.reader.unit.BaseUnitTestSpec +import com.expedia.www.haystack.trace.reader.unit.readers.builders.{ClockSkewedTraceBuilder, MultiRootTraceBuilder, MultiServerSpanTraceBuilder, ValidTraceBuilder} +import com.google.protobuf.util.JsonFormat + +import scala.io.Source + +class TraceProcessorSpec + extends BaseUnitTestSpec + with ValidTraceBuilder + with MultiServerSpanTraceBuilder + with MultiRootTraceBuilder + with ClockSkewedTraceBuilder { + + /** + * This test can be used to debug the prod issue using the raw trace. + * Copy-paste the raw trace under the child spans in the json file. And update + * the traceId which is at the first level in the json file. + * Also, make sure to set the same transformers (in same sequence) which are applied in your prod env. + */ + describe("TraceProcessor for well-formed raw trace from json file") { + + val traceProcessor = new TraceProcessor( + Seq(new TraceIdValidator), + Seq(new DeDuplicateSpanTransformer, new ClientServerEventLogTransformer), + Seq(new PartialSpanTransformer, new ServerClientSpanMergeTransformer, new InvalidRootTransformer, + new InvalidParentTransformer, new ClockSkewTransformer, new SortSpanTransformer)) + + it("should successfully process a simple valid raw trace from json") { + Given("a raw trace from json file") + val trace = getTraceFromJson(jsonFile = "raw_trace.json") + + When("invoking process") + val processedTraceOption = traceProcessor.process(trace) + + Then("successfully process trace") + processedTraceOption.isSuccess should be(true) + val processedTrace = processedTraceOption.get + + processedTrace.getChildSpansList.size() should be(13) + } + } + + private def getTraceFromJson(jsonFile: String): Trace = { + val stringJson = Source.fromResource(jsonFile).mkString + + // replace "value" with proto supported "vStr" for tags and log + val replacedStringJson = stringJson.replaceAll("value", "vStr") + val builder = Trace.newBuilder() + JsonFormat.parser().merge(replacedStringJson, builder) + builder.build() + } + + describe("TraceProcessor for well-formed traces") { + val traceProcessor = new TraceProcessor( + Seq(new TraceIdValidator, new RootValidator, new ParentIdValidator), + Seq(new DeDuplicateSpanTransformer), + Seq(new PartialSpanTransformer, new ClockSkewTransformer, new SortSpanTransformer)) + + it("should successfully process a simple valid trace") { + Given("a simple liner trace ") + val trace = buildSimpleLinerTrace() + + When("invoking process") + val processedTraceOption = traceProcessor.process(trace) + + Then("successfully process trace") + processedTraceOption.isSuccess should be(true) + val processedTrace = processedTraceOption.get + + processedTrace.getChildSpansList.size() should be(4) + getSpanById(processedTrace, "a").getStartTime should be(startTimestamp) + getSpanById(processedTrace, "b").getStartTime should be(startTimestamp + 50) + getSpanById(processedTrace, "c").getStartTime should be(startTimestamp + 550) + getSpanById(processedTrace, "d").getStartTime should be(startTimestamp + 750) + } + + it("should reject a multi-root trace") { + Given("a multi-root trace ") + val trace = buildMultiRootTrace() + + When("invoking process") + val processedTraceOption = traceProcessor.process(trace) + + Then("reject trace") + processedTraceOption.isSuccess should be(false) + } + + it("should successfully process a valid multi-service trace without clock skew") { + Given("a valid multi-service trace without skew") + val trace = buildMultiServiceWithoutSkewTrace() + + When("invoking process") + val processedTraceOption = traceProcessor.process(trace) + + Then("successfully process trace") + processedTraceOption.isSuccess should be(true) + val processedTrace = processedTraceOption.get + + processedTrace.getChildSpansList.size() should be(5) + getSpanById(processedTrace, "a").getStartTime should be(startTimestamp) + getSpanById(processedTrace, "a").getServiceName should be("x") + + getSpanById(processedTrace, "b").getStartTime should be(startTimestamp) + getSpanById(processedTrace, "b").getServiceName should be("y") + + getSpanById(processedTrace, "c").getStartTime should be(startTimestamp + 500) + getSpanById(processedTrace, "c").getServiceName should be("x") + + getSpanById(processedTrace, "d").getStartTime should be(startTimestamp) + getSpanById(processedTrace, "d").getServiceName should be("y") + + getSpanById(processedTrace, "e").getStartTime should be(startTimestamp + 200) + getSpanById(processedTrace, "e").getServiceName should be("y") + } + + it("should successfully process a valid multi-service trace with positive clock skew") { + Given("a valid multi-service trace with skew") + val trace = buildMultiServiceWithPositiveSkewTrace() + + When("invoking process") + val processedTraceOption = traceProcessor.process(trace) + + Then("successfully process trace") + processedTraceOption.isSuccess should be(true) + val processedTrace = processedTraceOption.get + + processedTrace.getChildSpansList.size() should be(5) + getSpanById(processedTrace, "a").getStartTime should be(startTimestamp) + getSpanById(processedTrace, "a").getServiceName should be("x") + + getSpanById(processedTrace, "b").getStartTime should be(startTimestamp) + getSpanById(processedTrace, "b").getServiceName should be("y") + + getSpanById(processedTrace, "c").getStartTime should be(startTimestamp + 500) + getSpanById(processedTrace, "c").getServiceName should be("x") + + getSpanById(processedTrace, "d").getStartTime should be(startTimestamp) + getSpanById(processedTrace, "d").getServiceName should be("y") + + getSpanById(processedTrace, "e").getStartTime should be(startTimestamp + 200) + getSpanById(processedTrace, "e").getServiceName should be("y") + } + + it("should successfully process a valid multi-service trace with negative clock skew") { + Given("a valid multi-service trace with negative skew") + val trace = buildMultiServiceWithNegativeSkewTrace() + + When("invoking process") + val processedTraceOption = traceProcessor.process(trace) + + Then("successfully process trace") + processedTraceOption.isSuccess should be(true) + val processedTrace = processedTraceOption.get + + processedTrace.getChildSpansList.size() should be(5) + getSpanById(processedTrace, "a").getStartTime should be(startTimestamp) + getSpanById(processedTrace, "a").getServiceName should be("x") + + getSpanById(processedTrace, "b").getStartTime should be(startTimestamp) + getSpanById(processedTrace, "b").getServiceName should be("y") + + getSpanById(processedTrace, "c").getStartTime should be(startTimestamp + 500) + getSpanById(processedTrace, "c").getServiceName should be("x") + + getSpanById(processedTrace, "d").getStartTime should be(startTimestamp) + getSpanById(processedTrace, "d").getServiceName should be("y") + + getSpanById(processedTrace, "e").getStartTime should be(startTimestamp + 200) + getSpanById(processedTrace, "e").getServiceName should be("y") + } + + it("should successfully process a valid complex multi-service trace") { + Given("a valid multi-service trace ") + val trace = buildMultiServiceTrace() + + When("invoking process") + val processedTraceOption = traceProcessor.process(trace) + + Then("successfully process trace") + processedTraceOption.isSuccess should be(true) + val processedTrace = processedTraceOption.get + + processedTrace.getChildSpansList.size() should be(6) + getSpanById(processedTrace, "a").getStartTime should be(startTimestamp) + getSpanById(processedTrace, "a").getServiceName should be("w") + + getSpanById(processedTrace, "b").getStartTime should be(startTimestamp + 20) + getSpanById(processedTrace, "b").getServiceName should be("x") + + getSpanById(processedTrace, "c").getStartTime should be(startTimestamp + 520) + getSpanById(processedTrace, "c").getServiceName should be("y") + + getSpanById(processedTrace, "d").getStartTime should be(startTimestamp + 20) + getSpanById(processedTrace, "d").getServiceName should be("x") + + getSpanById(processedTrace, "e").getStartTime should be(startTimestamp + 20) + getSpanById(processedTrace, "e").getServiceName should be("x") + + getSpanById(processedTrace, "f").getStartTime should be(startTimestamp + 540) + getSpanById(processedTrace, "f").getServiceName should be("z") + } + } + + describe("TraceProcessor for non well-formed traces") { + val traceProcessor = new TraceProcessor( + Seq(new TraceIdValidator), + Seq(new DeDuplicateSpanTransformer), + Seq(new PartialSpanTransformer, new InvalidRootTransformer, new InvalidParentTransformer, new ClockSkewTransformer, new SortSpanTransformer)) + + it("should successfully process a simple valid trace") { + Given("a simple liner trace ") + val trace = buildSimpleLinerTrace() + + When("invoking process") + val processedTraceOption = traceProcessor.process(trace) + + Then("successfully process trace") + processedTraceOption.isSuccess should be(true) + val processedTrace = processedTraceOption.get + + processedTrace.getChildSpansList.size() should be(4) + getSpanById(processedTrace, "a").getStartTime should be(startTimestamp) + getSpanById(processedTrace, "b").getStartTime should be(startTimestamp + 50) + getSpanById(processedTrace, "c").getStartTime should be(startTimestamp + 550) + getSpanById(processedTrace, "d").getStartTime should be(startTimestamp + 750) + } + + it("should successfully process a multi-root trace") { + Given("a multi-root trace ") + val trace = buildMultiRootTrace() + + When("invoking process") + val processedTraceOption = traceProcessor.process(trace) + + Then("successfully process trace") + processedTraceOption.isSuccess should be(true) + val processedTrace = processedTraceOption.get + + processedTrace.getChildSpansList.size() should be(5) + getSpanById(processedTrace, "a").getServiceName should be("x") + getSpanById(processedTrace, "b").getParentSpanId should not be "a" + getSpanById(processedTrace, "c").getParentSpanId should be("b") + getSpanById(processedTrace, "d").getParentSpanId should be("b") + } + + it("should successfully process a valid multi-service trace without clock skew") { + Given("a valid multi-service trace without skew") + val trace = buildMultiServiceWithoutSkewTrace() + + When("invoking process") + val processedTraceOption = traceProcessor.process(trace) + + Then("successfully process trace") + processedTraceOption.isSuccess should be(true) + val processedTrace = processedTraceOption.get + + processedTrace.getChildSpansList.size() should be(5) + getSpanById(processedTrace, "a").getStartTime should be(startTimestamp) + getSpanById(processedTrace, "a").getServiceName should be("x") + + getSpanById(processedTrace, "b").getStartTime should be(startTimestamp) + getSpanById(processedTrace, "b").getServiceName should be("y") + + getSpanById(processedTrace, "c").getStartTime should be(startTimestamp + 500) + getSpanById(processedTrace, "c").getServiceName should be("x") + + getSpanById(processedTrace, "d").getStartTime should be(startTimestamp) + getSpanById(processedTrace, "d").getServiceName should be("y") + + getSpanById(processedTrace, "e").getStartTime should be(startTimestamp + 200) + getSpanById(processedTrace, "e").getServiceName should be("y") + } + + it("should successfully process a valid multi-service trace with positive clock skew") { + Given("a valid multi-service trace with skew") + val trace = buildMultiServiceWithPositiveSkewTrace() + + When("invoking process") + val processedTraceOption = traceProcessor.process(trace) + + Then("successfully process trace") + processedTraceOption.isSuccess should be(true) + val processedTrace = processedTraceOption.get + + processedTrace.getChildSpansList.size() should be(5) + getSpanById(processedTrace, "a").getStartTime should be(startTimestamp) + getSpanById(processedTrace, "a").getServiceName should be("x") + + getSpanById(processedTrace, "b").getStartTime should be(startTimestamp) + getSpanById(processedTrace, "b").getServiceName should be("y") + + getSpanById(processedTrace, "c").getStartTime should be(startTimestamp + 500) + getSpanById(processedTrace, "c").getServiceName should be("x") + + getSpanById(processedTrace, "d").getStartTime should be(startTimestamp) + getSpanById(processedTrace, "d").getServiceName should be("y") + + getSpanById(processedTrace, "e").getStartTime should be(startTimestamp + 200) + getSpanById(processedTrace, "e").getServiceName should be("y") + } + + it("should successfully process a valid multi-service trace with negative clock skew") { + Given("a valid multi-service trace with negative skew") + val trace = buildMultiServiceWithNegativeSkewTrace() + + When("invoking process") + val processedTraceOption = traceProcessor.process(trace) + + Then("successfully process trace") + processedTraceOption.isSuccess should be(true) + val processedTrace = processedTraceOption.get + + processedTrace.getChildSpansList.size() should be(5) + getSpanById(processedTrace, "a").getStartTime should be(startTimestamp) + getSpanById(processedTrace, "a").getServiceName should be("x") + + getSpanById(processedTrace, "b").getStartTime should be(startTimestamp) + getSpanById(processedTrace, "b").getServiceName should be("y") + + getSpanById(processedTrace, "c").getStartTime should be(startTimestamp + 500) + getSpanById(processedTrace, "c").getServiceName should be("x") + + getSpanById(processedTrace, "d").getStartTime should be(startTimestamp) + getSpanById(processedTrace, "d").getServiceName should be("y") + + getSpanById(processedTrace, "e").getStartTime should be(startTimestamp + 200) + getSpanById(processedTrace, "e").getServiceName should be("y") + } + + it("should successfully process a valid complex multi-service trace") { + Given("a valid multi-service trace ") + val trace = buildMultiServiceTrace() + + When("invoking process") + val processedTraceOption = traceProcessor.process(trace) + + Then("successfully process trace") + processedTraceOption.isSuccess should be(true) + val processedTrace = processedTraceOption.get + + processedTrace.getChildSpansList.size() should be(6) + getSpanById(processedTrace, "a").getStartTime should be(startTimestamp) + getSpanById(processedTrace, "a").getServiceName should be("w") + + getSpanById(processedTrace, "b").getStartTime should be(startTimestamp + 20) + getSpanById(processedTrace, "b").getServiceName should be("x") + + getSpanById(processedTrace, "c").getStartTime should be(startTimestamp + 520) + getSpanById(processedTrace, "c").getServiceName should be("y") + + getSpanById(processedTrace, "d").getStartTime should be(startTimestamp + 20) + getSpanById(processedTrace, "d").getServiceName should be("x") + + getSpanById(processedTrace, "e").getStartTime should be(startTimestamp + 20) + getSpanById(processedTrace, "e").getServiceName should be("x") + + getSpanById(processedTrace, "f").getStartTime should be(startTimestamp + 540) + getSpanById(processedTrace, "f").getServiceName should be("z") + } + } +} diff --git a/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/readers/builders/ClockSkewedTraceBuilder.scala b/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/readers/builders/ClockSkewedTraceBuilder.scala new file mode 100644 index 000000000..f2b868505 --- /dev/null +++ b/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/readers/builders/ClockSkewedTraceBuilder.scala @@ -0,0 +1,321 @@ +package com.expedia.www.haystack.trace.reader.unit.readers.builders + +import com.expedia.open.tracing.Span +import com.expedia.open.tracing.api.Trace +import com.expedia.www.haystack.trace.reader.readers.utils.SpanTree + +import scala.collection.JavaConverters._ + +// helper to create various types of traces for unit testing +trait ClockSkewedTraceBuilder extends TraceBuilder { + + /** + * trace spanning multiple services without clock skew + * + * ...................................................... x + * a |============================================| + * b |---------------------| + * c |----------------------| + * + * ..................................................... y + * b |=====================| + * d |---------| + * e |-----------| + * + */ + def buildMultiServiceWithoutSkewTrace(): Trace = { + val aSpan = Span.newBuilder() + .setSpanId("a") + .setTraceId(traceId) + .setServiceName("x") + .setStartTime(startTimestamp) + .setDuration(1000) + .addAllLogs(createServerSpanTags(startTimestamp, startTimestamp + 1000).asJavaCollection) + .build() + + val bSpan = Span.newBuilder() + .setSpanId("b") + .setParentSpanId("a") + .setTraceId(traceId) + .setServiceName("x") + .setStartTime(startTimestamp) + .setDuration(500) + .addAllLogs(createClientSpanTags(startTimestamp, startTimestamp + 500).asJavaCollection) + .build() + + val cSpan = Span.newBuilder() + .setSpanId("c") + .setParentSpanId("a") + .setTraceId(traceId) + .setServiceName("x") + .setStartTime(startTimestamp + 500) + .setDuration(500) + .addAllLogs(createClientSpanTags(startTimestamp + 500, startTimestamp + 500 + 500).asJavaCollection) + .build() + + val bServerSpan = Span.newBuilder() + .setSpanId("b") + .setParentSpanId("a") + .setTraceId(traceId) + .setServiceName("y") + .setStartTime(startTimestamp) + .setDuration(500) + .addAllLogs(createServerSpanTags(startTimestamp, startTimestamp + 500).asJavaCollection) + .build() + + val dSpan = Span.newBuilder() + .setSpanId("d") + .setParentSpanId("b") + .setTraceId(traceId) + .setServiceName("y") + .setStartTime(startTimestamp) + .setDuration(200) + .addAllLogs(createClientSpanTags(startTimestamp, startTimestamp + 200).asJavaCollection) + .build() + + val eSpan = Span.newBuilder() + .setSpanId("e") + .setParentSpanId("b") + .setTraceId(traceId) + .setServiceName("y") + .setStartTime(startTimestamp + 200) + .setDuration(300) + .addAllLogs(createClientSpanTags(startTimestamp + 200, startTimestamp + 200 + 300).asJavaCollection) + .build() + + toTrace(aSpan, bSpan, cSpan, bServerSpan, dSpan, eSpan) + } + + /** + * trace spanning multiple services with positive clock skew + * + * ...................................................... x + * a |============================================| + * b |---------------------| + * c |----------------------| + * + * ..................................................... y + * b |=====================| + * d |--------| + * e |------------| + * + */ + def buildMultiServiceWithPositiveSkewTrace(): Trace = { + val aSpan = Span.newBuilder() + .setSpanId("a") + .setTraceId(traceId) + .setServiceName("x") + .setStartTime(startTimestamp) + .setDuration(1000) + .addAllLogs(createServerSpanTags(startTimestamp, startTimestamp + 1000).asJavaCollection) + .build() + + val bSpan = Span.newBuilder() + .setSpanId("b") + .setParentSpanId("a") + .setTraceId(traceId) + .setServiceName("x") + .setStartTime(startTimestamp) + .setDuration(500) + .addAllLogs(createClientSpanTags(startTimestamp, startTimestamp + 500).asJavaCollection) + .build() + + val cSpan = Span.newBuilder() + .setSpanId("c") + .setParentSpanId("a") + .setTraceId(traceId) + .setServiceName("x") + .setStartTime(startTimestamp + 500) + .setDuration(500) + .addAllLogs(createClientSpanTags(startTimestamp + 500, startTimestamp + 500 + 500).asJavaCollection) + .build() + + val bServerSpan = Span.newBuilder() + .setSpanId("b") + .setParentSpanId("a") + .setTraceId(traceId) + .setServiceName("y") + .setStartTime(startTimestamp + 100) + .setDuration(500) + .addAllLogs(createServerSpanTags(startTimestamp + 100, startTimestamp + 100 + 500).asJavaCollection) + .build() + + val dSpan = Span.newBuilder() + .setSpanId("d") + .setParentSpanId("b") + .setTraceId(traceId) + .setServiceName("y") + .setStartTime(startTimestamp + 100) + .setDuration(200) + .addAllLogs(createClientSpanTags(startTimestamp + 100, startTimestamp + 100 + 200).asJavaCollection) + .build() + + val eSpan = Span.newBuilder() + .setSpanId("e") + .setParentSpanId("b") + .setTraceId(traceId) + .setServiceName("y") + .setStartTime(startTimestamp + 300) + .setDuration(300) + .addAllLogs(createClientSpanTags(startTimestamp + 300, startTimestamp + 300 + 300).asJavaCollection) + .build() + + toTrace(aSpan, bSpan, cSpan, bServerSpan, dSpan, eSpan) + } + + /** + * trace spanning multiple services with negative clock skew + * + * ...................................................... x + * a |============================================| + * b |---------------------| + * c |----------------------| + * + * ..................................................... y + * b |===================| + * d |--------| + * e |----------| + * + */ + def buildMultiServiceWithNegativeSkewTrace(): Trace = { + val aSpan = Span.newBuilder() + .setSpanId("a") + .setTraceId(traceId) + .setServiceName("x") + .setStartTime(startTimestamp) + .setDuration(1000) + .addAllLogs(createServerSpanTags(startTimestamp, startTimestamp + 1000).asJavaCollection) + .build() + + val bSpan = Span.newBuilder() + .setSpanId("b") + .setParentSpanId("a") + .setTraceId(traceId) + .setServiceName("x") + .setStartTime(startTimestamp) + .setDuration(500) + .addAllLogs(createClientSpanTags(startTimestamp, startTimestamp + 500).asJavaCollection) + .build() + + val cSpan = Span.newBuilder() + .setSpanId("c") + .setParentSpanId("a") + .setTraceId(traceId) + .setServiceName("x") + .setStartTime(startTimestamp + 500) + .setDuration(500) + .addAllLogs(createClientSpanTags(startTimestamp + 500, startTimestamp + 500 + 500).asJavaCollection) + .build() + + val bServerSpan = Span.newBuilder() + .setSpanId("b") + .setParentSpanId("a") + .setTraceId(traceId) + .setServiceName("y") + .setStartTime(startTimestamp - 100) + .setDuration(500) + .addAllLogs(createServerSpanTags(startTimestamp - 100, startTimestamp - 100 + 500).asJavaCollection) + .build() + + val dSpan = Span.newBuilder() + .setSpanId("d") + .setParentSpanId("b") + .setTraceId(traceId) + .setServiceName("y") + .setStartTime(startTimestamp - 100) + .setDuration(200) + .addAllLogs(createClientSpanTags(startTimestamp - 100, startTimestamp - 100 + 200).asJavaCollection) + .build() + + val eSpan = Span.newBuilder() + .setSpanId("e") + .setParentSpanId("b") + .setTraceId(traceId) + .setServiceName("y") + .setStartTime(startTimestamp + 100) + .setDuration(300) + .addAllLogs(createClientSpanTags(startTimestamp + 100, startTimestamp + 100 + 300).asJavaCollection) + .build() + + toTrace(aSpan, bSpan, cSpan, bServerSpan, dSpan, eSpan) + } + + /** + * trace spanning multiple services with multi-level clock skew + * + * ...................................................... x + * a |============================================| + * b |--------------------------------------------| + * + * ..................................................... y + * b |============================================| + * c |--------------------------------------------| + * + * ..................................................... z + * c |============================================| + * d |--------------------------------------------| + * + */ + def buildMultiLevelSkewTrace(): Trace = { + val aSpan = Span.newBuilder() + .setSpanId("a") + .setTraceId(traceId) + .setServiceName("x") + .setStartTime(startTimestamp) + .setDuration(1000) + .addAllLogs(createServerSpanTags(startTimestamp, startTimestamp + 1000).asJavaCollection) + .build() + + val bSpan = Span.newBuilder() + .setSpanId("b") + .setParentSpanId("a") + .setTraceId(traceId) + .setServiceName("x") + .setStartTime(startTimestamp) + .setDuration(1000) + .addAllLogs(createClientSpanTags(startTimestamp, startTimestamp + 1000).asJavaCollection) + .build() + + val bServerSpan = Span.newBuilder() + .setSpanId("b") + .setParentSpanId("a") + .setTraceId(traceId) + .setServiceName("y") + .setStartTime(startTimestamp - 100) + .setDuration(1000) + .addAllLogs(createServerSpanTags(startTimestamp - 100, startTimestamp - 100 + 1000).asJavaCollection) + .build() + + val cSpan = Span.newBuilder() + .setSpanId("c") + .setParentSpanId("b") + .setTraceId(traceId) + .setServiceName("y") + .setStartTime(startTimestamp - 100) + .setDuration(1000) + .addAllLogs(createClientSpanTags(startTimestamp - 100, startTimestamp -100 + 1000).asJavaCollection) + .build() + + val cServerSpan = Span.newBuilder() + .setSpanId("c") + .setParentSpanId("b") + .setTraceId(traceId) + .setServiceName("z") + .setStartTime(startTimestamp + 500) + .setDuration(1000) + .addAllLogs(createServerSpanTags(startTimestamp + 500, startTimestamp + 500 + 1000).asJavaCollection) + .build() + + val dSpan = Span.newBuilder() + .setSpanId("d") + .setParentSpanId("c") + .setTraceId(traceId) + .setServiceName("z") + .setStartTime(startTimestamp + 500) + .setDuration(1000) + .addAllLogs(createClientSpanTags(startTimestamp + 500, startTimestamp + 500 + 1000).asJavaCollection) + .build() + + toTrace(aSpan, bSpan, cSpan, bServerSpan, cServerSpan, dSpan) + } +} diff --git a/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/readers/builders/MultiRootTraceBuilder.scala b/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/readers/builders/MultiRootTraceBuilder.scala new file mode 100644 index 000000000..fa3e9e411 --- /dev/null +++ b/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/readers/builders/MultiRootTraceBuilder.scala @@ -0,0 +1,61 @@ +package com.expedia.www.haystack.trace.reader.unit.readers.builders + +import com.expedia.open.tracing.Span +import com.expedia.open.tracing.api.Trace + +import scala.collection.JavaConverters._ + +// helper to create various types of traces for unit testing +trait MultiRootTraceBuilder extends TraceBuilder { + + /** + * trace with multiple root spans + * + * ..................................................... x + * a |=========| b |===================| + * c |-------------------| + * d |------| + * + */ + def buildMultiRootTrace(): Trace = { + val aSpan = Span.newBuilder() + .setSpanId("a") + .setTraceId(traceId) + .setServiceName("x") + .setStartTime(startTimestamp) + .setDuration(300) + .addAllLogs(createServerSpanTags(startTimestamp, startTimestamp + 300).asJavaCollection) + .build() + + val bSpan = Span.newBuilder() + .setSpanId("b") + .setTraceId(traceId) + .setServiceName("x") + .setStartTime(startTimestamp + 500) + .setDuration(500) + .addAllLogs(createServerSpanTags(startTimestamp + 500, startTimestamp + 500 + 500).asJavaCollection) + .build() + + val cSpan = Span.newBuilder() + .setSpanId("c") + .setParentSpanId("b") + .setTraceId(traceId) + .setServiceName("x") + .setStartTime(startTimestamp + 500) + .setDuration(500) + .addAllLogs(createClientSpanTags(startTimestamp + 500, startTimestamp + 500 + 500).asJavaCollection) + .build() + + val dSpan = Span.newBuilder() + .setSpanId("d") + .setParentSpanId("b") + .setTraceId(traceId) + .setServiceName("x") + .setStartTime(startTimestamp + 750) + .setDuration(200) + .addAllLogs(createClientSpanTags(startTimestamp + 750, startTimestamp + 750 + 200).asJavaCollection) + .build() + + toTrace(aSpan, bSpan, cSpan, dSpan) + } +} diff --git a/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/readers/builders/MultiServerSpanTraceBuilder.scala b/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/readers/builders/MultiServerSpanTraceBuilder.scala new file mode 100644 index 000000000..e35c11b21 --- /dev/null +++ b/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/readers/builders/MultiServerSpanTraceBuilder.scala @@ -0,0 +1,62 @@ +package com.expedia.www.haystack.trace.reader.unit.readers.builders + +import com.expedia.open.tracing.Span +import com.expedia.open.tracing.api.Trace + +import scala.collection.JavaConverters._ + +// helper to create various types of traces for unit testing +trait MultiServerSpanTraceBuilder extends TraceBuilder { + /** + * trace with multiple root spans + * + * ..................................................... x + * a |============================| + * b |----------------------------| + * ..................................................... y + * b |==========| b |========| + * + */ + def buildMultiServerSpanForAClientSpanTrace(): Trace = { + val aSpan = Span.newBuilder() + .setSpanId("a") + .setTraceId(traceId) + .setServiceName("x") + .setStartTime(startTimestamp) + .setDuration(1000) + .addAllLogs(createServerSpanTags(startTimestamp, startTimestamp + 1000).asJavaCollection) + .build() + + val bSpan = Span.newBuilder() + .setSpanId("b") + .setParentSpanId("a") + .setTraceId(traceId) + .setServiceName("x") + .setStartTime(startTimestamp) + .setDuration(1000) + .addAllLogs(createClientSpanTags(startTimestamp, startTimestamp + 1000).asJavaCollection) + .build() + + val bFirstServerSpan = Span.newBuilder() + .setSpanId("b") + .setParentSpanId("a") + .setTraceId(traceId) + .setServiceName("y") + .setStartTime(startTimestamp) + .setDuration(500) + .addAllLogs(createClientSpanTags(startTimestamp, startTimestamp + 500).asJavaCollection) + .build() + + val bSecondServerSpan = Span.newBuilder() + .setSpanId("b") + .setParentSpanId("a") + .setTraceId(traceId) + .setServiceName("z") + .setStartTime(startTimestamp + 500) + .setDuration(500) + .addAllLogs(createClientSpanTags(startTimestamp + 500, startTimestamp + 500 + 500).asJavaCollection) + .build() + + toTrace(aSpan, bSpan, bFirstServerSpan, bSecondServerSpan) + } +} diff --git a/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/readers/builders/TraceBuilder.scala b/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/readers/builders/TraceBuilder.scala new file mode 100644 index 000000000..93dada8d2 --- /dev/null +++ b/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/readers/builders/TraceBuilder.scala @@ -0,0 +1,40 @@ +package com.expedia.www.haystack.trace.reader.unit.readers.builders + +import com.expedia.open.tracing.api.Trace +import com.expedia.open.tracing.{Log, Span, Tag} + +import scala.collection.JavaConverters._ + +// helper to create various types of traces for unit testing +trait TraceBuilder { + val startTimestamp = 150000000000l + val traceId = "traceId" + + protected def toTrace(spans: Span*): Trace = Trace.newBuilder().setTraceId(traceId).addAllChildSpans(spans.asJavaCollection).build + + protected def createServerSpanTags(start: Long, end: Long) = List( + Log.newBuilder() + .setTimestamp(start) + .addFields(Tag.newBuilder().setKey("event").setVStr("sr").build()) + .build(), + Log.newBuilder() + .setTimestamp(end) + .addFields(Tag.newBuilder().setKey("event").setVStr("ss").build()) + .build() + ) + + protected def createClientSpanTags(start: Long, end: Long) = List( + Log.newBuilder() + .setTimestamp(start) + .addFields(Tag.newBuilder().setKey("event").setVStr("cs").build()) + .build(), + Log.newBuilder() + .setTimestamp(end) + .addFields(Tag.newBuilder().setKey("event").setVStr("cr").build()) + .build() + ) + + protected def getSpanById(trace: Trace, spanId: String): Span = trace.getChildSpansList.asScala.find(_.getSpanId == spanId).get + + protected def getSpanById(spans: Seq[Span], spanId: String): Span = spans.find(_.getSpanId == spanId).get +} diff --git a/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/readers/builders/ValidTraceBuilder.scala b/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/readers/builders/ValidTraceBuilder.scala new file mode 100644 index 000000000..700ba5864 --- /dev/null +++ b/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/readers/builders/ValidTraceBuilder.scala @@ -0,0 +1,175 @@ +package com.expedia.www.haystack.trace.reader.unit.readers.builders + +import com.expedia.open.tracing.Span +import com.expedia.open.tracing.api.Trace + +import scala.collection.JavaConverters._ + +// helper to create various types of traces for unit testing +trait ValidTraceBuilder extends TraceBuilder { + /** + * simple liner trace with a sequence of sequential spans + * + * ..................................................... x + * a |==================================| + * b |-------------------| + * c |------| + * d |---| + * + */ + def buildSimpleLinerTrace(): Trace = { + val aSpan = Span.newBuilder() + .setSpanId("a") + .setTraceId(traceId) + .setServiceName("x") + .setStartTime(startTimestamp) + .setDuration(1000) + .addAllLogs(createServerSpanTags(startTimestamp, startTimestamp + 1000).asJavaCollection) + .build() + + val bSpan = Span.newBuilder() + .setSpanId("b") + .setParentSpanId("a") + .setTraceId(traceId) + .setServiceName("x") + .setStartTime(startTimestamp + 50) + .setDuration(500) + .addAllLogs(createClientSpanTags(startTimestamp + 50, startTimestamp + 50 + 500).asJavaCollection) + .build() + + val cSpan = Span.newBuilder() + .setSpanId("c") + .setParentSpanId("a") + .setTraceId(traceId) + .setServiceName("x") + .setStartTime(startTimestamp + 550) + .setDuration(200) + .addAllLogs(createClientSpanTags(startTimestamp + 550, startTimestamp + 550 + 200).asJavaCollection) + .build() + + val dSpan = Span.newBuilder() + .setSpanId("d") + .setParentSpanId("a") + .setTraceId(traceId) + .setServiceName("x") + .setStartTime(startTimestamp + 750) + .setDuration(200) + .addAllLogs(createClientSpanTags(startTimestamp + 750, startTimestamp + 750 + 200).asJavaCollection) + .build() + + toTrace(aSpan, bSpan, cSpan, dSpan) + } + + /** + * trace spanning multiple services, assume network delta to be 20ms + * + * ...................................................... w + * a |============================================| + * b |---------------------| + * c |----------------------| + * + * ..................................................... x + * b |==================| + * d |--------| + * e |----------------| + * + * ..................................................... y + * c |====================| + * f |----------| + * + * ..................................................... y + * f |========| + */ + def buildMultiServiceTrace(): Trace = { + val aSpan = Span.newBuilder() + .setSpanId("a") + .setTraceId(traceId) + .setServiceName("w") + .setStartTime(startTimestamp) + .setDuration(1000) + .addAllLogs(createServerSpanTags(startTimestamp, startTimestamp + 1000).asJavaCollection) + .build() + + val bSpan = Span.newBuilder() + .setSpanId("b") + .setParentSpanId("a") + .setTraceId(traceId) + .setServiceName("w") + .setStartTime(startTimestamp) + .setDuration(500) + .addAllLogs(createClientSpanTags(startTimestamp, startTimestamp + 500).asJavaCollection) + .build() + + val cSpan = Span.newBuilder() + .setSpanId("c") + .setParentSpanId("a") + .setTraceId(traceId) + .setServiceName("w") + .setStartTime(startTimestamp + 500) + .setDuration(500) + .addAllLogs(createClientSpanTags(startTimestamp + 500, startTimestamp + 500 + 500).asJavaCollection) + .build() + + val bServerSpan = Span.newBuilder() + .setSpanId("b") + .setParentSpanId("a") + .setTraceId(traceId) + .setServiceName("x") + .setStartTime(startTimestamp + 20) + .setDuration(460) + .addAllLogs(createServerSpanTags(startTimestamp + 20, startTimestamp + 20 + 460).asJavaCollection) + .build() + + val dSpan = Span.newBuilder() + .setSpanId("d") + .setParentSpanId("b") + .setTraceId(traceId) + .setServiceName("x") + .setStartTime(startTimestamp + 20) + .setDuration(200) + .addAllLogs(createClientSpanTags(startTimestamp + 20, startTimestamp + 20 + 200).asJavaCollection) + .build() + + val eSpan = Span.newBuilder() + .setSpanId("e") + .setParentSpanId("b") + .setTraceId(traceId) + .setServiceName("x") + .setStartTime(startTimestamp + 20) + .setDuration(400) + .addAllLogs(createClientSpanTags(startTimestamp + 20, startTimestamp + 20 + 400).asJavaCollection) + .build() + + val cServerSpan = Span.newBuilder() + .setSpanId("c") + .setParentSpanId("a") + .setTraceId(traceId) + .setServiceName("y") + .setStartTime(startTimestamp + 520) + .setDuration(460) + .addAllLogs(createServerSpanTags(startTimestamp + 520, startTimestamp + 520 + 460).asJavaCollection) + .build() + + val fSpan = Span.newBuilder() + .setSpanId("f") + .setParentSpanId("c") + .setTraceId(traceId) + .setServiceName("y") + .setStartTime(startTimestamp + 520) + .setDuration(100) + .addAllLogs(createClientSpanTags(startTimestamp + 520, startTimestamp + 520 + 100).asJavaCollection) + .build() + + val fServerSpan = Span.newBuilder() + .setSpanId("f") + .setParentSpanId("c") + .setTraceId(traceId) + .setServiceName("z") + .setStartTime(startTimestamp + 540) + .setDuration(100) + .addAllLogs(createServerSpanTags(startTimestamp + 540, startTimestamp + 540 + 50).asJavaCollection) + .build() + + toTrace(aSpan, bSpan, cSpan, bServerSpan, dSpan, eSpan, cServerSpan, fSpan, fServerSpan) + } +} diff --git a/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/readers/transformers/ClientServerEventLogTransformerSpec.scala b/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/readers/transformers/ClientServerEventLogTransformerSpec.scala new file mode 100644 index 000000000..87d131229 --- /dev/null +++ b/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/readers/transformers/ClientServerEventLogTransformerSpec.scala @@ -0,0 +1,93 @@ +/* + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expedia.www.haystack.trace.reader.unit.readers.transformers + +import com.expedia.open.tracing.{Log, Span, Tag} +import com.expedia.www.haystack.trace.commons.utils.SpanMarkers +import com.expedia.www.haystack.trace.reader.readers.transformers.ClientServerEventLogTransformer +import com.expedia.www.haystack.trace.reader.unit.BaseUnitTestSpec + +import scala.collection.JavaConverters._ + +class ClientServerEventLogTransformerSpec extends BaseUnitTestSpec { + describe("client server event log transformer") { + it("should add event logs in the span using span.kind") { + val span_1 = Span.newBuilder() + .setTraceId("trace-id-1") + .setDuration(100) + .setStartTime(10000) + .addTags(Tag.newBuilder().setType(Tag.TagType.STRING).setKey(SpanMarkers.SPAN_KIND_TAG_KEY).setVStr("client")) + .build() + val span_2 = Span.newBuilder() + .setTraceId("trace-id-2") + .setDuration(200) + .setStartTime(20000) + .addTags(Tag.newBuilder().setType(Tag.TagType.STRING).setKey(SpanMarkers.SPAN_KIND_TAG_KEY).setVStr("server")) + .build() + val transformer = new ClientServerEventLogTransformer + val transformedSpans = transformer.transform(Seq(span_1, span_2)) + transformedSpans.length shouldBe 2 + + val headSpan = transformedSpans.head + headSpan.getTraceId shouldBe "trace-id-1" + headSpan.getDuration shouldBe 100l + headSpan.getStartTime shouldBe 10000l + headSpan.getTagsList.asScala.find(tag => tag.getKey == SpanMarkers.SPAN_KIND_TAG_KEY).get.getVStr shouldBe "client" + headSpan.getLogsList.size() shouldBe 2 + headSpan.getLogs(0).getTimestamp shouldBe 10000l + headSpan.getLogs(1).getTimestamp shouldBe 10100l + headSpan.getLogs(0).getFieldsList.asScala.count(tag => tag.getKey == SpanMarkers.LOG_EVENT_TAG_KEY && tag.getVStr == SpanMarkers.CLIENT_SEND_EVENT) shouldBe 1 + headSpan.getLogs(1).getFieldsList.asScala.count(tag => tag.getKey == SpanMarkers.LOG_EVENT_TAG_KEY && tag.getVStr == SpanMarkers.CLIENT_RECV_EVENT) shouldBe 1 + + val lastSpan = transformedSpans(1) + lastSpan.getTraceId shouldBe "trace-id-2" + lastSpan.getDuration shouldBe 200 + lastSpan.getStartTime shouldBe 20000 + lastSpan.getTagsList.asScala.find(tag => tag.getKey == SpanMarkers.SPAN_KIND_TAG_KEY).get.getVStr shouldBe "server" + lastSpan.getLogsList.size() shouldBe 2 + lastSpan.getLogs(0).getTimestamp shouldBe 20000 + lastSpan.getLogs(1).getTimestamp shouldBe 20200l + lastSpan.getLogs(0).getFieldsList.asScala.count(tag => tag.getKey == SpanMarkers.LOG_EVENT_TAG_KEY && tag.getVStr == SpanMarkers.SERVER_RECV_EVENT) shouldBe 1 + lastSpan.getLogs(1).getFieldsList.asScala.count(tag => tag.getKey == SpanMarkers.LOG_EVENT_TAG_KEY && tag.getVStr == SpanMarkers.SERVER_SEND_EVENT) shouldBe 1 + } + + it("should not add anything if span.kind is absent") { + val span = Span.newBuilder() + .setTraceId("trace-id-1") + .setDuration(100) + .setStartTime(10000) + .build() + val transformedSpans = new ClientServerEventLogTransformer().transform(Seq(span)) + transformedSpans.size shouldBe 1 + transformedSpans.head shouldEqual span + } + + it("should not add log tags if they are already present") { + val span = Span.newBuilder() + .setTraceId("trace-id-1") + .setDuration(100) + .setStartTime(10000) + .addTags(Tag.newBuilder().setType(Tag.TagType.STRING).setKey(SpanMarkers.SPAN_KIND_TAG_KEY).setVStr("client")) + .addLogs(Log.newBuilder().setTimestamp(10000).addFields(Tag.newBuilder().setKey("event").setVStr("cr"))) + .addLogs(Log.newBuilder().setTimestamp(10100).addFields(Tag.newBuilder().setKey("event").setVStr("cs"))) + .build() + val transformedSpans = new ClientServerEventLogTransformer().transform(Seq(span)) + transformedSpans.size shouldBe 1 + transformedSpans.head shouldEqual span + } + } +} diff --git a/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/readers/transformers/ClockSkewFromParentTransformerSpec.scala b/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/readers/transformers/ClockSkewFromParentTransformerSpec.scala new file mode 100644 index 000000000..64527260f --- /dev/null +++ b/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/readers/transformers/ClockSkewFromParentTransformerSpec.scala @@ -0,0 +1,99 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.expedia.www.haystack.trace.reader.unit.readers.transformers + +import com.expedia.open.tracing.Span +import com.expedia.www.haystack.trace.commons.utils.SpanUtils +import com.expedia.www.haystack.trace.reader.readers.transformers.ClockSkewFromParentTransformer +import com.expedia.www.haystack.trace.reader.readers.utils.{MutableSpanForest, SpanTree} +import com.expedia.www.haystack.trace.reader.unit.BaseUnitTestSpec + +class ClockSkewFromParentTransformerSpec extends BaseUnitTestSpec { + describe("ClockSkewFromParentTransformerSpec") { + it("should not make any adjustments if parent/child spans are properly aligned") { + val span_1 = createSpan("", "span_1", 100L, 200L) + val span_2 = createSpan(span_1.getSpanId, "span_2", 125L, 175L) + + val transformer = new ClockSkewFromParentTransformer() + val spans = transformer.transform(MutableSpanForest(List(span_1, span_2))) + validateSpans(spans, 2) + } + + it("should shift child span's startTime if the endTime exceeds the parent span") { + val span_1 = createSpan("", "span_1", 100L, 200L) + val span_2 = createSpan(span_1.getSpanId, "span_2", 175L, 225L) + + val transformer = new ClockSkewFromParentTransformer() + val spans = transformer.transform(MutableSpanForest(List(span_1, span_2))) + validateSpans(spans, 2) + } + + it("should shift child span's startTime if the startTime precedes the parent span") { + val span_1 = createSpan("", "span_1", 100L, 200L) + val span_2 = createSpan(span_1.getSpanId, "span_2", 75L, 125L) + + val transformer = new ClockSkewFromParentTransformer() + val spans = transformer.transform(MutableSpanForest(List(span_1, span_2))) + validateSpans(spans, 2) + } + + it("should shift both the startTime and the endTime if the child span is completely outside of the parent spans timeframe") { + val span_1 = createSpan("", "span_1", 100L, 200L) + val span_2 = createSpan(span_1.getSpanId, "span_2", 275L, 325L) + + val transformer = new ClockSkewFromParentTransformer() + val spans = transformer.transform(MutableSpanForest(List(span_1, span_2))) + validateSpans(spans, 2) + } + + it("should shift multiple children correctly") { + val span_1 = createSpan("", "span_1", 100L, 200L) + val span_2 = createSpan(span_1.getSpanId, "span_2", 275L, 325L) + val span_3 = createSpan(span_2.getSpanId, "span_3", 375, 400L) + + val transformer = new ClockSkewFromParentTransformer() + val spans = transformer.transform(MutableSpanForest(List(span_1, span_2, span_3))) + validateSpans(spans, 3) + } + + it("should handle a single span with no shift") { + val span_1 = createSpan("", "span_1", 100L, 200L) + + val transformer = new ClockSkewFromParentTransformer() + val spans = transformer.transform(MutableSpanForest(List(span_1))) + spans.getUnderlyingSpans.size shouldBe 1 + spans.getUnderlyingSpans.head.getStartTime shouldBe 100L + spans.getUnderlyingSpans.head.getDuration shouldBe 100L + } + } + + def validateSpans(spans: MutableSpanForest, size: Int): Unit = { + spans.getUnderlyingSpans.size shouldBe size + spans.getAllTrees.foreach(spanTree => validateSpanTree(spanTree)) + } + + def validateSpanTree(spanTree: SpanTree): Unit = { + spanTree.children.foreach(child => { + spanTree.span.getStartTime should be <= child.span.getStartTime + SpanUtils.getEndTime(child.span) should be <= SpanUtils.getEndTime(spanTree.span) + }) + spanTree.children.foreach(child => validateSpanTree(child)) + } + + def createSpan(parentId: String, spanId: String, startTime: Long, endTime: Long): Span = { + Span.newBuilder().setTraceId("traceId").setParentSpanId(parentId).setSpanId(spanId).setStartTime(startTime).setDuration(endTime - startTime).setServiceName("another-service").build() + } +} diff --git a/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/readers/transformers/ClockSkewTransformerSpec.scala b/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/readers/transformers/ClockSkewTransformerSpec.scala new file mode 100644 index 000000000..d4eee68c5 --- /dev/null +++ b/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/readers/transformers/ClockSkewTransformerSpec.scala @@ -0,0 +1,175 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expedia.www.haystack.trace.reader.unit.readers.transformers + +import com.expedia.open.tracing.{Log, Span, Tag} +import com.expedia.www.haystack.trace.reader.readers.transformers.ClockSkewTransformer +import com.expedia.www.haystack.trace.reader.readers.utils.MutableSpanForest +import com.expedia.www.haystack.trace.reader.unit.BaseUnitTestSpec + +class ClockSkewTransformerSpec extends BaseUnitTestSpec { + + private def createTraceWithoutMergedSpans(timestamp: Long) = { + // creating a trace with this timeline structure- + // a -> b(-50) -> e(-100) + // -> c(+500) + // -> d(-100) + + val traceId = "traceId" + + val spanA = Span.newBuilder() + .setSpanId("a") + .setTraceId(traceId) + .setStartTime(timestamp) + .setDuration(1000) + .build() + + val spanB = Span.newBuilder() + .setSpanId("b") + .setParentSpanId("a") + .setTraceId(traceId) + .setStartTime(spanA.getStartTime - 50) + .setDuration(100) + .build() + + val spanC = Span.newBuilder() + .setSpanId("c") + .setParentSpanId("a") + .setTraceId(traceId) + .setStartTime(spanA.getStartTime + 500) + .setDuration(100) + .build() + + val spanD = Span.newBuilder() + .setSpanId("d") + .setParentSpanId("a") + .setTraceId(traceId) + .setStartTime(spanA.getStartTime - 100) + .setDuration(100) + .build() + + val spanE = Span.newBuilder() + .setSpanId("e") + .setParentSpanId("b") + .setTraceId(traceId) + .setStartTime(spanB.getStartTime - 100) + .setDuration(100) + .build() + + List(spanA, spanB, spanC, spanD, spanE) + } + + private def createSpansWithClientAndServer(timestamp: Long) = { + val traceId = "traceId" + val skewedSpanId = "spanId" + val serviceName = "serviceNam" + val tag = Tag.newBuilder().setKey("tag").setVBool(true).build() + val log = Log.newBuilder().setTimestamp(System.currentTimeMillis).addFields(tag).build() + + val partialSpan = Span.newBuilder() + .setSpanId(skewedSpanId) + .setTraceId(traceId) + .setServiceName(serviceName) + .setStartTime(timestamp + 2000) + .setDuration(1000) + .addTags(tag) + .addLogs(Log.newBuilder() + .setTimestamp(timestamp) + .addFields(Tag.newBuilder().setKey("event").setVStr("cs").build()) + .build()) + .addLogs(Log.newBuilder() + .setTimestamp(timestamp + 1000) + .addFields(Tag.newBuilder().setKey("event").setVStr("cr").build()) + .build()) + .addLogs(Log.newBuilder() + .setTimestamp(timestamp + 2000) + .addFields(Tag.newBuilder().setKey("event").setVStr("sr").build()) + .build()) + .addLogs(Log.newBuilder() + .setTimestamp(timestamp + 2000 + 400) + .addFields(Tag.newBuilder().setKey("event").setVStr("ss").build()) + .build()) + .build() + + val aChildSpan = Span.newBuilder() + .setSpanId("a") + .setParentSpanId(skewedSpanId) + .setTraceId(traceId) + .setServiceName(serviceName) + .setStartTime(timestamp + 2500) + .setDuration(400) + .addTags(tag) + .build() + + val bChildSpan = Span.newBuilder() + .setSpanId("b") + .setParentSpanId(skewedSpanId) + .setTraceId(traceId) + .setServiceName(serviceName) + .setStartTime(timestamp + 2700) + .setDuration(400) + .addTags(tag) + .build() + + val cSpan = Span.newBuilder() + .setSpanId("c") + .setParentSpanId("b") + .setTraceId(traceId) + .setServiceName("otherService") + .setStartTime(timestamp + 100) + .setDuration(400) + .addTags(tag) + .build() + + List(aChildSpan, bChildSpan, cSpan, partialSpan) + } + + describe("ClockSkewTransformer") { + it("should not change clock skew if there are no merged spans") { + Given("trace with skewed spans") + val timestamp = 150000000000l + val spanForest = MutableSpanForest(createTraceWithoutMergedSpans(timestamp)) + + When("invoking transform") + val transformedSpans = new ClockSkewTransformer().transform(spanForest).getUnderlyingSpans + + Then("return spans without fixing skew") + transformedSpans.length should be(5) + transformedSpans.find(_.getSpanId == "a").get.getStartTime should be(timestamp) + transformedSpans.find(_.getSpanId == "b").get.getStartTime should be(timestamp - 50) + transformedSpans.find(_.getSpanId == "c").get.getStartTime should be(timestamp + 500) + transformedSpans.find(_.getSpanId == "d").get.getStartTime should be(timestamp - 100) + transformedSpans.find(_.getSpanId == "e").get.getStartTime should be(timestamp - 150) + } + + it("should fix clock skew if there merged spans with skew") { + Given("trace with skewed spans") + val timestamp = 150000000000l + val spanForest = MutableSpanForest(createSpansWithClientAndServer(timestamp)) + + When("invoking transform") + val transformedSpans = new ClockSkewTransformer().transform(spanForest).getUnderlyingSpans + + Then("return spans without fixing skew") + transformedSpans.length should be(4) + transformedSpans.find(_.getSpanId == "spanId").get.getStartTime should be(timestamp + 300) + transformedSpans.find(_.getSpanId == "a").get.getStartTime should be(timestamp + 300 + 500) + transformedSpans.find(_.getSpanId == "b").get.getStartTime should be(timestamp + 300 + 700) + transformedSpans.find(_.getSpanId == "c").get.getStartTime should be(timestamp + 100) + } + } +} diff --git a/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/readers/transformers/DeDuplicateSpanTransformerSpec.scala b/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/readers/transformers/DeDuplicateSpanTransformerSpec.scala new file mode 100644 index 000000000..eb0b9fc53 --- /dev/null +++ b/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/readers/transformers/DeDuplicateSpanTransformerSpec.scala @@ -0,0 +1,46 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expedia.www.haystack.trace.reader.unit.readers.transformers + +import com.expedia.open.tracing.Span +import com.expedia.www.haystack.trace.reader.readers.transformers.DeDuplicateSpanTransformer +import com.expedia.www.haystack.trace.reader.unit.BaseUnitTestSpec + +class DeDuplicateSpanTransformerSpec extends BaseUnitTestSpec { + + describe("dedup span transformer") { + it("should remove all the duplicate spans") { + val span_1 = Span.newBuilder().setTraceId("traceId").setSpanId("span_1").setServiceName("test-service").build() + val dup_span_1 = Span.newBuilder().setTraceId("traceId").setSpanId("span_1").setServiceName("test-service").build() + + val span_1_1 = Span.newBuilder().setTraceId("traceId").setSpanId("span_1").setServiceName("test-service-2").build() + + val span_2 = Span.newBuilder().setTraceId("traceId").setSpanId("span_2").setServiceName("another-service").build() + val dup_span_2 = Span.newBuilder().setTraceId("traceId").setSpanId("span_2").setServiceName("another-service").build() + + val transformer = new DeDuplicateSpanTransformer() + var dedupSpans = transformer.transform(List(span_1, span_2, dup_span_2, dup_span_1, span_1_1)) + dedupSpans.size shouldBe 3 + dedupSpans.map(sp => sp.getServiceName) should contain allOf("test-service", "another-service", "test-service-2") + dedupSpans.map(sp => sp.getSpanId) should contain allOf("span_1", "span_2") + + dedupSpans = transformer.transform(List(span_1, span_1, span_2, dup_span_2)) + dedupSpans.size shouldBe 2 + dedupSpans.map(sp => sp.getSpanId) should contain allOf("span_1", "span_2") + } + } +} \ No newline at end of file diff --git a/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/readers/transformers/InfrastructureTagTransformerSpec.scala b/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/readers/transformers/InfrastructureTagTransformerSpec.scala new file mode 100644 index 000000000..e121b289d --- /dev/null +++ b/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/readers/transformers/InfrastructureTagTransformerSpec.scala @@ -0,0 +1,56 @@ +package com.expedia.www.haystack.trace.reader.unit.readers.transformers + +import com.expedia.open.tracing.{Span, Tag} +import com.expedia.www.haystack.trace.reader.readers.transformers.InfrastructureTagTransformer +import com.expedia.www.haystack.trace.reader.readers.utils.AuxiliaryTags +import com.expedia.www.haystack.trace.reader.unit.BaseUnitTestSpec + +import scala.collection.JavaConverters._ + +class InfrastructureTagTransformerSpec extends BaseUnitTestSpec { + + private val infraTags = Seq( + Tag.newBuilder().setKey(AuxiliaryTags.INFRASTRUCTURE_PROVIDER).setVStr("aws").setType(Tag.TagType.STRING).build(), + Tag.newBuilder().setKey(AuxiliaryTags.INFRASTRUCTURE_LOCATION).setVStr("us-west-2").setType(Tag.TagType.STRING).build() + ) + + private val randomTags = Seq(Tag.newBuilder().setKey("error").setVBool(false).setType(Tag.TagType.BOOL).build()) + + + describe("infrastructure tag transformer") { + it("should add missing infrastructure tags in a service spans if any of it contains so") { + // first tag in the sequence contains infrastructure tags, following span doesn't contain from the same service doesn't contain. + val svc1_span1 = Span.newBuilder().setTraceId("traceId").setSpanId("span_1").setServiceName("service_1").addAllTags(randomTags.asJava).addAllTags(infraTags.asJava).build() + val svc1_span2 = Span.newBuilder().setTraceId("traceId").setSpanId("span_2").setParentSpanId("span_1").setServiceName("service_1").build() + + // none of the tags from this service contains infrastructure information + val svc2_span_1 = Span.newBuilder().setTraceId("traceId").setSpanId("span_3").setServiceName("service_2").addAllTags(randomTags.asJava).build() + val svc2_span_2 = Span.newBuilder().setTraceId("traceId").setSpanId("span_4").setParentSpanId("span_3").setServiceName("service_2").build() + + + // first tag in the sequence doesn't contain infrastructure tags, following span from the same service contains. + val svc3_span1 = Span.newBuilder().setTraceId("traceId").setSpanId("span_5").setServiceName("service_3").build() + val svc3_span2 = Span.newBuilder().setTraceId("traceId").setSpanId("span_6").setParentSpanId("span_5").setServiceName("service_3").addAllTags(randomTags.asJava).addAllTags(infraTags.asJava).build() + + val transformedSpans = new InfrastructureTagTransformer() + .transform(Seq(svc1_span1, svc1_span2, svc2_span_1, svc2_span_2, svc3_span1, svc3_span2)) + + transformedSpans.size shouldBe 6 + transformedSpans.find(_.getSpanId == "span_1").get.getTagsList should contain allElementsOf infraTags + transformedSpans.find(_.getSpanId == "span_1").get.getTagsList should contain allElementsOf randomTags + + transformedSpans.find(_.getSpanId == "span_2").get.getTagsCount shouldBe infraTags.size + transformedSpans.find(_.getSpanId == "span_2").get.getTagsList should contain allElementsOf infraTags + + transformedSpans.find(_.getSpanId == "span_3").get.getTagsCount shouldBe 1 + transformedSpans.find(_.getSpanId == "span_3").get.getTagsList should contain allElementsOf randomTags + transformedSpans.find(_.getSpanId == "span_4").get.getTagsCount shouldBe 0 + + transformedSpans.find(_.getSpanId == "span_5").get.getTagsCount shouldBe infraTags.size + transformedSpans.find(_.getSpanId == "span_5").get.getTagsList should contain allElementsOf infraTags + transformedSpans.find(_.getSpanId == "span_6").get.getTagsList should contain allElementsOf infraTags + transformedSpans.find(_.getSpanId == "span_6").get.getTagsList should contain allElementsOf randomTags + + } + } +} diff --git a/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/readers/transformers/InvalidParentTransformerSpec.scala b/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/readers/transformers/InvalidParentTransformerSpec.scala new file mode 100644 index 000000000..2bb0e292e --- /dev/null +++ b/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/readers/transformers/InvalidParentTransformerSpec.scala @@ -0,0 +1,94 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expedia.www.haystack.trace.reader.unit.readers.transformers + +import com.expedia.open.tracing.Span +import com.expedia.www.haystack.trace.reader.readers.transformers.InvalidParentTransformer +import com.expedia.www.haystack.trace.reader.readers.utils.MutableSpanForest +import com.expedia.www.haystack.trace.reader.unit.BaseUnitTestSpec + +class InvalidParentTransformerSpec extends BaseUnitTestSpec { + describe("InvalidParentTransformer") { + it("should mark root as parent for spans with invalid parent ids") { + Given("trace having spans with invalid parent ids") + val spans = List( + Span.newBuilder() + .setSpanId("a") + .build(), + Span.newBuilder() + .setSpanId("b") + .setParentSpanId("b") + .build(), + Span.newBuilder() + .setTraceId("traceId") + .setSpanId("c") + .setParentSpanId("traceId") + .build() + ) + + When("invoking transform") + val transformedSpanTree = new InvalidParentTransformer().transform(MutableSpanForest(spans)) + val transformedSpans = transformedSpanTree.getUnderlyingSpans + + Then("mark root to be parent of spans with invalid parent id") + transformedSpans.length should be(3) + + val aSpan = transformedSpans.find(_.getSpanId == "a") + aSpan.get.getParentSpanId should be("") + + val bSpan = transformedSpans.find(_.getSpanId == "b") + bSpan.get.getParentSpanId should be("a") + + val cSpan = transformedSpans.find(_.getSpanId == "c") + cSpan.get.getParentSpanId should be("a") + + transformedSpanTree.getAllTrees.size shouldBe 1 + } + + it("should mark root as parent for spans with parent ids that are not in the trace") { + Given("trace having spans with invalid parent ids") + val spans = List( + Span.newBuilder() + .setSpanId("a") + .build(), + Span.newBuilder() + .setSpanId("b") + .setParentSpanId("x") + .build(), + Span.newBuilder() + .setSpanId("c") + .setParentSpanId("b") + .build() + ) + + When("invoking transform") + val transformedSpans = new InvalidParentTransformer().transform(MutableSpanForest(spans)).getUnderlyingSpans + + Then("mark root to be parent of spans with invalid parent id") + transformedSpans.length should be(3) + + val aSpan = transformedSpans.find(_.getSpanId == "a") + aSpan.get.getParentSpanId should be("") + + val bSpan = transformedSpans.find(_.getSpanId == "b") + bSpan.get.getParentSpanId should be("a") + + val cSpan = transformedSpans.find(_.getSpanId == "c") + cSpan.get.getParentSpanId should be("b") + } + } +} diff --git a/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/readers/transformers/InvalidRootTransformerSpec.scala b/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/readers/transformers/InvalidRootTransformerSpec.scala new file mode 100644 index 000000000..f9e38f52c --- /dev/null +++ b/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/readers/transformers/InvalidRootTransformerSpec.scala @@ -0,0 +1,209 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expedia.www.haystack.trace.reader.unit.readers.transformers + +import com.expedia.open.tracing.{Span, Tag} +import com.expedia.www.haystack.trace.commons.utils.SpanUtils +import com.expedia.www.haystack.trace.reader.readers.transformers.InvalidRootTransformer +import com.expedia.www.haystack.trace.reader.readers.utils.MutableSpanForest +import com.expedia.www.haystack.trace.reader.unit.BaseUnitTestSpec + +import scala.collection.JavaConverters._ + +class InvalidRootTransformerSpec extends BaseUnitTestSpec { + describe("InvalidRootTransformer") { + it("should mark first span as root when there are multiple roots") { + Given("trace with multiple roots ") + val spanForest = MutableSpanForest(Seq( + Span.newBuilder() + .setSpanId("a") + .setServiceName("sa") + .setStartTime(150000000000l + 300) + .build(), + Span.newBuilder() + .setSpanId("b") + .setServiceName("sb") + .setStartTime(150000000000l) + .build(), + Span.newBuilder() + .setSpanId("c") + .setServiceName("sc") + .setStartTime(150000000000l + 150) + .build() + )) + + When("invoking transform") + val transformedSpans = new InvalidRootTransformer().transform(spanForest).getUnderlyingSpans + + Then("pick first span as root and mark second's parent to be root") + transformedSpans.length should be(4) + + val root = transformedSpans.filter(_.getParentSpanId.isEmpty) + root.size should be(1) + root.head.getServiceName shouldEqual "sb" + root.head.getOperationName shouldEqual "auto-generated" + root.head.getStartTime shouldBe 150000000000l + root.head.getDuration shouldBe 300l + + val others = transformedSpans.filter(!_.getParentSpanId.isEmpty) + others.foreach(span => span.getParentSpanId should be(root.head.getSpanId)) + } + + it("should not generate any autogenerated for a complete tree but without a parent span existing") { + Given("trace with multiple roots ") + val spanForest = MutableSpanForest(Seq( + Span.newBuilder() + .setSpanId("a") + .setParentSpanId("b") + .setServiceName("sa") + .setStartTime(150000000000l + 300) + .build(), + Span.newBuilder() + .setSpanId("b") + .setParentSpanId("d") + .setServiceName("sb") + .setStartTime(150000000000l) + .build(), + Span.newBuilder() + .setSpanId("c") + .setParentSpanId("b") + .setServiceName("sc") + .setStartTime(150000000000l + 150) + .build() + )) + + When("invoking transform") + val transformedSpans = new InvalidRootTransformer().transform(spanForest).getUnderlyingSpans + + Then("pick first span as root and mark second's parent to be root") + transformedSpans.length should be(3) + + val root = transformedSpans.filter(_.getParentSpanId.isEmpty) + root.size should be(1) + root.head.getServiceName shouldEqual "sb" + root.head.getStartTime shouldBe 150000000000l + + val others = transformedSpans.filter(!_.getParentSpanId.isEmpty) + others.foreach(span => span.getParentSpanId should be(root.head.getSpanId)) + } + + it("should mark first span as root when there are no roots") { + Given("trace with multiple roots ") + val spanForest = MutableSpanForest(Seq( + Span.newBuilder() + .setSpanId("a") + .setParentSpanId("x") + .setStartTime(150000000000l + 300) + .build(), + Span.newBuilder() + .setSpanId("b") + .setParentSpanId("x") + .setStartTime(150000000000l) + .build(), + Span.newBuilder() + .setSpanId("c") + .setParentSpanId("x") + .setStartTime(150000000000l + 150) + .build() + )) + + When("invoking transform") + val transformedSpans = new InvalidRootTransformer().transform(spanForest).getUnderlyingSpans + + Then("pick first span as root and mark second's parent to be root") + transformedSpans.length should be(3) + + val root = transformedSpans.filter(_.getParentSpanId.isEmpty) + root.size should be(1) + root.head.getSpanId should be("b") + } + + it("should mark loopback span as root when there are no roots") { + Given("trace with multiple roots ") + val spanForest = MutableSpanForest(Seq( + Span.newBuilder() + .setSpanId("a") + .setParentSpanId("x") + .setStartTime(150000000000l + 300) + .build(), + Span.newBuilder() + .setSpanId("b") + .setParentSpanId("x") + .setStartTime(150000000000l) + .build(), + Span.newBuilder() + .setSpanId("c") + .setParentSpanId("c") + .setStartTime(150000000000l + 150) + .build() + )) + + When("invoking transform") + val transformedSpans = new InvalidRootTransformer().transform(spanForest).getUnderlyingSpans + + Then("pick first span as root and mark second's parent to be root") + transformedSpans.length should be(3) + + val root = transformedSpans.filter(_.getParentSpanId.isEmpty) + root.size should be(1) + root.head.getSpanId should be("c") + } + + it("should create an autogenerated span using the span tree with earliest timestamp if multiple trees exist with a root having an empty parentSpanId") { + Given("trace with multiple roots ") + val spanForest = MutableSpanForest(Seq( + Span.newBuilder() + .setSpanId("a") + .setServiceName("aService") + .setParentSpanId("") + .addTags(Tag.newBuilder().setKey(SpanUtils.URL_TAG_KEY).setVStr("/anotherurl").setType(Tag.TagType.STRING)) + .setStartTime(150000000000l + 300) + .build(), + Span.newBuilder() + .setSpanId("b") + .setParentSpanId("") + .setServiceName("bService") + .addTags(Tag.newBuilder().setKey(SpanUtils.URL_TAG_KEY).setVStr("/someurl").setType(Tag.TagType.STRING)) + .setStartTime(150000000000l) + .build(), + Span.newBuilder() + .setSpanId("c") + .setServiceName("cService") + .addTags(Tag.newBuilder().setKey(SpanUtils.URL_TAG_KEY).setVStr("/anotherurl").setType(Tag.TagType.STRING)) + .setParentSpanId("") + .setStartTime(150000000000l + 150) + .build() + )) + + When("invoking transform") + val transformedSpans = new InvalidRootTransformer().transform(spanForest).getUnderlyingSpans + + Then("pick earliest span tree as basis for autogenerated span") + transformedSpans.length should be(4) + + val root = transformedSpans.filter(_.getParentSpanId.isEmpty) + root.size should be(1) + root.head.getSpanId should not be oneOf("a", "b", "c") + root.head.getStartTime shouldBe 150000000000l + root.head.getOperationName shouldEqual "auto-generated" + root.head.getServiceName shouldEqual "bService" + val urlTag = root.head.getTagsList.asScala.find(_.getKey == SpanUtils.URL_TAG_KEY) + urlTag.isEmpty shouldBe false + urlTag.get.getVStr shouldEqual "/someurl" + } + } +} diff --git a/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/readers/transformers/OrphanedTraceTransformerSpec.scala b/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/readers/transformers/OrphanedTraceTransformerSpec.scala new file mode 100644 index 000000000..2c24d44e6 --- /dev/null +++ b/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/readers/transformers/OrphanedTraceTransformerSpec.scala @@ -0,0 +1,67 @@ +package com.expedia.www.haystack.trace.reader.unit.readers.transformers + +import com.expedia.open.tracing.{Span, Tag} +import com.expedia.www.haystack.trace.commons.utils.SpanMarkers +import com.expedia.www.haystack.trace.reader.readers.transformers.{OrphanedTraceTransformer, OrphanedTraceTransformerConstants} +import com.expedia.www.haystack.trace.reader.readers.utils.MutableSpanForest +import com.expedia.www.haystack.trace.reader.unit.BaseUnitTestSpec + +class OrphanedTraceTransformerSpec extends BaseUnitTestSpec { + describe("OrphanedTraceTransformerTest") { + it("should return full list of spans if there is a root span already") { + val span_1 = Span.newBuilder().setTraceId("traceId").setSpanId("traceId").setServiceName("another-service").build() + val span_2 = Span.newBuilder().setTraceId("traceId").setSpanId("span_2").setParentSpanId(span_1.getSpanId).setServiceName("test-service").build() + val span_3 = Span.newBuilder().setTraceId("traceId").setSpanId("span_3").setParentSpanId(span_1.getSpanId).setServiceName("another-service").build() + + val transformer = new OrphanedTraceTransformer() + val spanForest = MutableSpanForest(Seq(span_1, span_2, span_3)) + val spans = transformer.transform(spanForest).getUnderlyingSpans + spans.size shouldBe 3 + spans should contain(span_1) + spans should contain(span_2) + spans should contain(span_3) + } + + it("should return the full list of spans plus a generated root span if there is no root span already") { + val span_1 = Span.newBuilder().setTraceId("traceId").setOperationName(SpanMarkers.AUTOGEN_OPERATION_NAME).setServiceName("test-service") + .setSpanId("traceId").setStartTime(10000).setDuration(10100) + .addTags(Tag.newBuilder().setKey(SpanMarkers.AUTOGEN_REASON_TAG).setVStr(OrphanedTraceTransformerConstants.AUTO_GEN_REASON)) + .addTags(Tag.newBuilder().setKey(SpanMarkers.AUTOGEN_SPAN_ID_TAG).setVStr("traceId")) + .addTags(Tag.newBuilder().setKey(SpanMarkers.AUTOGEN_FLAG_TAG).setVBool(true).setType(Tag.TagType.BOOL)).build() + val span_2 = Span.newBuilder().setTraceId("traceId").setSpanId("span_2").setParentSpanId(span_1.getSpanId).setStartTime(10000).setDuration(10).setServiceName("test-service").build() + val span_3 = Span.newBuilder().setTraceId("traceId").setSpanId("span_3").setParentSpanId(span_1.getSpanId).setStartTime(20000).setDuration(100).setServiceName("another-service").build() + + val transformer = new OrphanedTraceTransformer() + val spanForest = MutableSpanForest(Seq(span_2, span_3)) + val spans = transformer.transform(spanForest).getUnderlyingSpans + spans.size shouldBe 3 + spans should contain(span_2) + spans should contain(span_3) + spans should contain(span_1) + } + + it("should fail if there are multiple different orphaned parent ids") { + val span_1 = Span.newBuilder().setTraceId("traceId").setSpanId("traceId").setServiceName("another-service").build() + val span_2 = Span.newBuilder().setTraceId("traceId").setSpanId("span_2").setParentSpanId(span_1.getSpanId).setServiceName("test-service").build() + val span_3 = Span.newBuilder().setTraceId("traceId").setSpanId("span_3").setParentSpanId(span_1.getSpanId).setServiceName("another-service").build() + val span_4 = Span.newBuilder().setTraceId("traceId").setSpanId("span_4").setParentSpanId(span_1.getSpanId).setServiceName("another-service").build() + val span_5 = Span.newBuilder().setTraceId("traceId").setSpanId("span_5").setParentSpanId(span_4.getSpanId).setServiceName("another-service").build() + + val transformer = new OrphanedTraceTransformer() + val spanForest = MutableSpanForest(Seq(span_2, span_3, span_5)) + val spans = transformer.transform(spanForest).getUnderlyingSpans + spans.size shouldBe 0 + } + + it("should fail if there is a missing span in between the root span and orphaned span") { + val span_1 = Span.newBuilder().setTraceId("traceId").setSpanId("traceId").setServiceName("another-service").build() + val span_4 = Span.newBuilder().setTraceId("traceId").setSpanId("span_4").setParentSpanId(span_1.getSpanId).setServiceName("another-service").build() + val span_5 = Span.newBuilder().setTraceId("traceId").setSpanId("span_5").setParentSpanId(span_4.getSpanId).setServiceName("another-service").build() + + val transformer = new OrphanedTraceTransformer() + val spanForest = MutableSpanForest(Seq(span_5)) + val spans = transformer.transform(spanForest).getUnderlyingSpans + spans.size shouldBe 0 + } + } +} diff --git a/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/readers/transformers/PartialSpanTransformerSpec.scala b/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/readers/transformers/PartialSpanTransformerSpec.scala new file mode 100644 index 000000000..37529fad8 --- /dev/null +++ b/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/readers/transformers/PartialSpanTransformerSpec.scala @@ -0,0 +1,262 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expedia.www.haystack.trace.reader.unit.readers.transformers + +import com.expedia.open.tracing.{Log, Span, Tag} +import com.expedia.www.haystack.trace.reader.readers.transformers.PartialSpanTransformer +import com.expedia.www.haystack.trace.reader.readers.utils.TagExtractors._ +import com.expedia.www.haystack.trace.reader.readers.utils.{AuxiliaryTags, MutableSpanForest} +import com.expedia.www.haystack.trace.reader.unit.BaseUnitTestSpec +import com.expedia.www.haystack.trace.reader.unit.readers.builders.ValidTraceBuilder + +import scala.collection.JavaConverters._ + +class PartialSpanTransformerSpec extends BaseUnitTestSpec with ValidTraceBuilder { + + private def createSpansWithClientAndServer(timestamp: Long) = { + val traceId = "traceId" + val partialSpanId = "partialSpanId" + val parentSpanId = "parentSpanId" + val tag = Tag.newBuilder().setKey("tag").setVBool(true).build() + + val partialClientSpan = Span.newBuilder() + .setSpanId(partialSpanId) + .setParentSpanId(parentSpanId) + .setTraceId(traceId) + .setServiceName("clientService") + .setStartTime(timestamp) + .setDuration(1000) + .addTags(tag) + .addLogs(Log.newBuilder() + .setTimestamp(System.currentTimeMillis) + .addFields(Tag.newBuilder().setKey("event").setVStr("cr").build()) + .build()) + .addLogs(Log.newBuilder() + .setTimestamp(System.currentTimeMillis) + .addFields(Tag.newBuilder().setKey("event").setVStr("cs").build()) + .build()) + .build() + + val partialServerSpan = Span.newBuilder() + .setSpanId(partialSpanId) + .setParentSpanId(parentSpanId) + .setTraceId(traceId) + .setServiceName("serverService") + .setStartTime(timestamp + 20) + .setDuration(980) + .addTags(tag) + .addLogs(Log.newBuilder() + .setTimestamp(System.currentTimeMillis) + .addFields(Tag.newBuilder().setKey("event").setVStr("sr").build()) + .build()) + .addLogs(Log.newBuilder() + .setTimestamp(System.currentTimeMillis) + .addFields(Tag.newBuilder().setKey("event").setVStr("ss").build()) + .build()) + .build() + + List(partialServerSpan, partialClientSpan) + } + + private def createMultiplePartialSpans(timestamp: Long) = { + val traceId = "traceId" + val partialSpanId = "partialSpanId" + val parentSpanId = "parentSpanId" + val tag = Tag.newBuilder().setKey("tag").setVBool(true).build() + + val partialClientSpan = Span.newBuilder() + .setSpanId(partialSpanId) + .setParentSpanId(parentSpanId) + .setTraceId(traceId) + .setServiceName("clientService") + .setStartTime(timestamp) + .setDuration(1000) + .addTags(tag) + .addLogs(Log.newBuilder() + .setTimestamp(System.currentTimeMillis) + .addFields(Tag.newBuilder().setKey("event").setVStr("cr").build()) + .build()) + .addLogs(Log.newBuilder() + .setTimestamp(System.currentTimeMillis) + .addFields(Tag.newBuilder().setKey("event").setVStr("cs").build()) + .build()) + .build() + + val firstPartialServerSpan = Span.newBuilder() + .setSpanId(partialSpanId) + .setParentSpanId(parentSpanId) + .setTraceId(traceId) + .setServiceName("serverService") + .setStartTime(timestamp + 20) + .setDuration(960) + .addTags(tag) + .addLogs(Log.newBuilder() + .setTimestamp(System.currentTimeMillis) + .addFields(Tag.newBuilder().setKey("event").setVStr("sr").build()) + .build()) + .addLogs(Log.newBuilder() + .setTimestamp(System.currentTimeMillis) + .addFields(Tag.newBuilder().setKey("event").setVStr("ss").build()) + .build()) + .build() + + val secondPartialServerSpan = Span.newBuilder() + .setSpanId(partialSpanId) + .setParentSpanId(parentSpanId) + .setTraceId(traceId) + .setServiceName("serverService") + .setStartTime(timestamp + 980) + .setDuration(10) + .addTags(tag) + .addLogs(Log.newBuilder() + .setTimestamp(System.currentTimeMillis) + .addFields(Tag.newBuilder().setKey("event").setVStr("sr").build()) + .build()) + .addLogs(Log.newBuilder() + .setTimestamp(System.currentTimeMillis) + .addFields(Tag.newBuilder().setKey("event").setVStr("ss").build()) + .build()) + .build() + + List(partialClientSpan, secondPartialServerSpan, firstPartialServerSpan) + } + + private def createNonPartialSpans(timestamp: Long) = { + val traceId = "traceId" + val tag = Tag.newBuilder().setKey("tag").setVBool(true).build() + + val span1 = Span.newBuilder() + .setSpanId("span1") + .setParentSpanId("x") + .setTraceId(traceId) + .setServiceName("span1Service") + .setStartTime(timestamp) + .setDuration(1000) + .addTags(tag) + .addLogs(Log.newBuilder() + .setTimestamp(System.currentTimeMillis) + .addFields(Tag.newBuilder().setKey("event").setVStr("cr").build()) + .build()) + .addLogs(Log.newBuilder() + .setTimestamp(System.currentTimeMillis) + .addFields(Tag.newBuilder().setKey("event").setVStr("cs").build()) + .build()) + .build() + + val span2 = Span.newBuilder() + .setSpanId("span2") + .setParentSpanId("x") + .setTraceId(traceId) + .setServiceName("span2Service") + .setStartTime(timestamp + 20) + .setDuration(980) + .addTags(tag) + .addLogs(Log.newBuilder() + .setTimestamp(System.currentTimeMillis) + .addFields(Tag.newBuilder().setKey("event").setVStr("sr").build()) + .build()) + .addLogs(Log.newBuilder() + .setTimestamp(System.currentTimeMillis) + .addFields(Tag.newBuilder().setKey("event").setVStr("ss").build()) + .build()) + .build() + + val span3 = Span.newBuilder() + .setSpanId("span3") + .setParentSpanId("x") + .setTraceId(traceId) + .setServiceName("span3Service") + .setStartTime(timestamp + 980) + .setDuration(10) + .addTags(tag) + .addLogs(Log.newBuilder() + .setTimestamp(System.currentTimeMillis) + .addFields(Tag.newBuilder().setKey("event").setVStr("sr").build()) + .build()) + .addLogs(Log.newBuilder() + .setTimestamp(System.currentTimeMillis) + .addFields(Tag.newBuilder().setKey("event").setVStr("ss").build()) + .build()) + .build() + + List(span1, span2, span3) + } + + describe("PartialSpanTransformer") { + it("should merge two partial spans with right event sequencing") { + Given("trace with partial spans") + val timestamp = 150000000000l + val spans = createSpansWithClientAndServer(timestamp) + + When("invoking transform") + val mergedSpans = new PartialSpanTransformer().transform(MutableSpanForest(spans)).getUnderlyingSpans + + Then("return partial spans merged with server span being primary") + mergedSpans.length should be(1) + mergedSpans.head.getStartTime should be(timestamp + 20) + mergedSpans.head.getTagsCount should be(17) + mergedSpans.head.getLogsCount should be(4) + mergedSpans.head.getServiceName should be("serverService") + } + + it("should merge multiple partial spans with first server span as primary") { + Given("trace with multiple partial spans") + val timestamp = 150000000000l + val spans = createMultiplePartialSpans(timestamp) + + When("invoking transform") + val mergedSpans = new PartialSpanTransformer().transform(MutableSpanForest(spans)).getUnderlyingSpans + + Then("return partial spans merged with first server span as primary") + mergedSpans.length should be(1) + mergedSpans.head.getStartTime should be(timestamp + 20) + mergedSpans.head.getTagsCount should be(19) + mergedSpans.head.getLogsCount should be(6) + mergedSpans.head.getServiceName should be("serverService") + } + + it("should not merge if there are no partial spans to merge") { + Given("trace without partial spans") + val timestamp = 150000000000l + val spans = createNonPartialSpans(timestamp) + + When("invoking transform") + val mergedSpans = new PartialSpanTransformer().transform(MutableSpanForest(spans)).getUnderlyingSpans + + Then("return partial spans merged") + mergedSpans.length should be(3) + } + + it("should add auxiliary tags") { + Given("trace with partial spans") + val spans = buildMultiServiceTrace().getChildSpansList.asScala + + When("invoking transform") + val mergedSpans = new PartialSpanTransformer().transform(MutableSpanForest(spans)).getUnderlyingSpans + + Then("return partial spans merged with auxiliary tags") + mergedSpans.size should be(6) + val bSpan = getSpanById(mergedSpans, "b") + bSpan.getStartTime should be(startTimestamp + 20) + bSpan.getServiceName should be("x") + + extractTagLongValue(bSpan, AuxiliaryTags.NETWORK_DELTA) should be(40) + extractTagStringValue(bSpan, AuxiliaryTags.CLIENT_SERVICE_NAME) should be("w") + extractTagStringValue(bSpan, AuxiliaryTags.SERVER_SERVICE_NAME) should be("x") + } + } +} diff --git a/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/readers/transformers/ServerClientSpanMergeTransformerSpec.scala b/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/readers/transformers/ServerClientSpanMergeTransformerSpec.scala new file mode 100644 index 000000000..a182be0ef --- /dev/null +++ b/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/readers/transformers/ServerClientSpanMergeTransformerSpec.scala @@ -0,0 +1,254 @@ +/* + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expedia.www.haystack.trace.reader.unit.readers.transformers + + +import com.expedia.open.tracing.{Span, Tag} +import com.expedia.www.haystack.trace.commons.utils.SpanMarkers +import com.expedia.www.haystack.trace.reader.readers.transformers.ServerClientSpanMergeTransformer +import com.expedia.www.haystack.trace.reader.readers.utils.{AuxiliaryTags, MutableSpanForest} +import com.expedia.www.haystack.trace.reader.unit.BaseUnitTestSpec +import com.expedia.www.haystack.trace.reader.unit.readers.builders.ValidTraceBuilder + +import scala.collection.JavaConverters._ + +class ServerClientSpanMergeTransformerSpec extends BaseUnitTestSpec with ValidTraceBuilder { + + private def createProducerAndConsumerSpanKinds(): List[Span] = { + val traceId = "traceId" + + val timestamp = System.currentTimeMillis() * 1000 + + val producerSpan = Span.newBuilder() + .setSpanId("sa") + .setTraceId(traceId) + .setServiceName("aSvc") + .addTags(Tag.newBuilder().setKey("span.kind").setVStr("producer")) + .setStartTime(timestamp + 100) + .setDuration(1000) + .build() + + val consumerASpan = Span.newBuilder() + .setSpanId("sb1") + .setParentSpanId("sa") + .setTraceId(traceId) + .setServiceName("bSvc") + .addTags(Tag.newBuilder().setKey("span.kind").setVStr("consumer")) + .setStartTime(timestamp + 400) + .setDuration(1000) + .build() + + val consumerBSpan = Span.newBuilder() + .setSpanId("sb2") + .setParentSpanId("sb1") + .setTraceId(traceId) + .setServiceName("bSvc") + .addTags(Tag.newBuilder().setKey("span.kind").setVStr("producer")) + .setStartTime(timestamp + 1000) + .setDuration(1000) + .build() + List(producerSpan, consumerASpan, consumerBSpan) + } + + private def createSpansWithClientAndServer(): List[Span] = { + val traceId = "traceId" + + val timestamp = System.currentTimeMillis() * 1000 + + val serverSpanA = Span.newBuilder() + .setSpanId("sa") + .setTraceId(traceId) + .setServiceName("aSvc") + .setStartTime(timestamp + 100) + .setDuration(1000) + .build() + + val clientSpanA = Span.newBuilder() + .setSpanId("ca") + .setParentSpanId("sa") + .setTraceId(traceId) + .setServiceName("aSvc") + .setStartTime(timestamp + 100) + .setDuration(1000) + .build() + + val serverSpanB = Span.newBuilder() + .setSpanId("sb") + .setParentSpanId("ca") + .setServiceName("bSvc") + .setTraceId(traceId) + .setStartTime(timestamp + 200) + .setDuration(100) + .build() + + val clientSpanB_1 = Span.newBuilder() + .setSpanId("cb1") + .setParentSpanId("sb") + .setServiceName("bSvc") + .setTraceId(traceId) + .setStartTime(timestamp + 300) + .setDuration(100) + .build() + + val clientSpanB_2 = Span.newBuilder() + .setSpanId("cb2") + .setParentSpanId("sb") + .setServiceName("bSvc") + .setStartTime(timestamp + 400) + .setTraceId(traceId) + .setDuration(100) + .build() + + val serverSpanC_1 = Span.newBuilder() + .setSpanId("sc1") + .setParentSpanId("cb1") + .setServiceName("cSvc") + .setTraceId(traceId) + .setStartTime(timestamp + 500) + .setDuration(100) + .build() + + val serverSpanC_2 = Span.newBuilder() + .setSpanId("sc2") + .setParentSpanId("cb2") + .setServiceName("cSvc") + .setTraceId(traceId) + .setStartTime(timestamp + 600) + .setDuration(100) + .build() + + val serverSpanC_3 = Span.newBuilder() + .setSpanId("sc3") + .setParentSpanId("p1") + .setServiceName("cSvc") + .addTags(Tag.newBuilder().setKey(SpanMarkers.SPAN_KIND_TAG_KEY).setVStr(SpanMarkers.SERVER_SPAN_KIND)) + .setTraceId(traceId) + .setStartTime(timestamp + 600) + .setDuration(100) + .build() + + val serverSpanD_1 = Span.newBuilder() + .setSpanId("sd1") + .setParentSpanId("sc3") + .setServiceName("dSvc") + .addTags(Tag.newBuilder().setKey(SpanMarkers.SPAN_KIND_TAG_KEY).setVStr(SpanMarkers.SERVER_SPAN_KIND)) + .setTraceId(traceId) + .setStartTime(timestamp + 600) + .setDuration(100) + .build() + + val serverSpanD_2 = Span.newBuilder() + .setSpanId("sd2") + .setParentSpanId("sc3") + .setServiceName("dSvc") + .addTags(Tag.newBuilder().setKey(SpanMarkers.SPAN_KIND_TAG_KEY).setVStr(SpanMarkers.CLIENT_SPAN_KIND)) + .setTraceId(traceId) + .setStartTime(timestamp + 600) + .setDuration(100) + .build() + + val serverSpanE_1 = Span.newBuilder() + .setSpanId("se1") + .setParentSpanId("sd2") + .setServiceName("eSvc") + .addTags(Tag.newBuilder().setKey(SpanMarkers.SPAN_KIND_TAG_KEY).setVStr(SpanMarkers.SERVER_SPAN_KIND)) + .setTraceId(traceId) + .setStartTime(timestamp + 600) + .setDuration(100) + .build() + + List(serverSpanA, clientSpanA, serverSpanB, clientSpanB_1, clientSpanB_2, serverSpanC_1, serverSpanC_2, serverSpanC_3, serverSpanD_1, serverSpanD_2, serverSpanE_1) + } + + describe("ServerClientSpanMergeTransformer") { + it("should merge the server client spans") { + Given("a sequence of spans of a given trace") + val spans = createSpansWithClientAndServer() + + When("invoking transform") + val mergedSpans = + new ServerClientSpanMergeTransformer().transform(MutableSpanForest(spans)) + + val underlyingSpans = mergedSpans.getUnderlyingSpans + + Then("return partial spans merged with server span being primary") + underlyingSpans.length should be(7) + underlyingSpans.foreach(span => span.getTraceId shouldBe traceId) + underlyingSpans.head.getSpanId shouldBe "sa" + underlyingSpans.head.getParentSpanId shouldBe "" + + underlyingSpans.apply(1).getSpanId shouldBe "sb" + underlyingSpans.apply(1).getParentSpanId shouldBe "sa" + underlyingSpans.apply(1).getServiceName shouldBe "bSvc" + getTag(underlyingSpans.apply(1), AuxiliaryTags.IS_MERGED_SPAN).getVBool shouldBe true + underlyingSpans.apply(1).getLogsCount shouldBe 4 + + underlyingSpans.apply(2).getSpanId shouldBe "sc1" + underlyingSpans.apply(2).getParentSpanId shouldBe "sb" + underlyingSpans.apply(2).getServiceName shouldBe "cSvc" + getTag(underlyingSpans.apply(2), AuxiliaryTags.IS_MERGED_SPAN).getVBool shouldBe true + underlyingSpans.apply(2).getLogsCount shouldBe 4 + + underlyingSpans.apply(3).getSpanId shouldBe "sc2" + underlyingSpans.apply(3).getParentSpanId shouldBe "sb" + underlyingSpans.apply(3).getServiceName shouldBe "cSvc" + getTag(underlyingSpans.apply(3), AuxiliaryTags.IS_MERGED_SPAN).getVBool shouldBe true + underlyingSpans.apply(3).getLogsCount shouldBe 4 + + underlyingSpans.apply(4).getSpanId shouldBe "sc3" + getTag(underlyingSpans.apply(4), AuxiliaryTags.IS_MERGED_SPAN) shouldBe null + + underlyingSpans.apply(5).getSpanId shouldBe "sd1" + getTag(underlyingSpans.apply(5), AuxiliaryTags.IS_MERGED_SPAN) shouldBe null + + underlyingSpans.apply(6).getSpanId shouldBe "se1" + underlyingSpans.apply(6).getServiceName shouldBe "eSvc" + getTag(underlyingSpans.apply(6), AuxiliaryTags.IS_MERGED_SPAN).getVBool shouldBe true + getTag(underlyingSpans.apply(6), AuxiliaryTags.CLIENT_SERVICE_NAME).getVStr shouldBe "dSvc" + getTag(underlyingSpans.apply(6), AuxiliaryTags.CLIENT_SPAN_ID).getVStr shouldBe "sd2" + getTag(underlyingSpans.apply(6), AuxiliaryTags.CLIENT_OPERATION_NAME).getVStr shouldBe empty + + mergedSpans.countTrees shouldBe 2 + val spanTree = mergedSpans.getAllTrees.head + spanTree.span shouldBe underlyingSpans.head + spanTree.children.size shouldBe 1 + spanTree.children.head.children.size shouldBe 2 + spanTree.children.head.span shouldBe underlyingSpans.apply(1) + spanTree.children.head.children.map(_.span) should contain allOf(underlyingSpans.apply(2), underlyingSpans.apply(3)) + spanTree.children.head.children.foreach(tree => tree.children.size shouldBe 0) + } + + it ("should not merge producer and consumer parent-child spans") { + Given("a sequence of spans of a given trace") + val spans = createProducerAndConsumerSpanKinds() + + When("invoking transform") + val mergedSpans = + new ServerClientSpanMergeTransformer().transform(MutableSpanForest(spans)) + + val underlyingSpans = mergedSpans.getUnderlyingSpans + underlyingSpans.size shouldBe 3 + underlyingSpans.foreach(sp => getTag(sp, AuxiliaryTags.IS_MERGED_SPAN) shouldBe null) + } + } + + + + private def getTag(span: Span, tagKey: String): Tag = { + span.getTagsList.asScala.find(tag => tag.getKey.equals(tagKey)).orNull + } +} \ No newline at end of file diff --git a/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/readers/transformers/SortSpanTransformerSpec.scala b/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/readers/transformers/SortSpanTransformerSpec.scala new file mode 100644 index 000000000..729fd5323 --- /dev/null +++ b/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/readers/transformers/SortSpanTransformerSpec.scala @@ -0,0 +1,89 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expedia.www.haystack.trace.reader.unit.readers.transformers + +import com.expedia.open.tracing.Span +import com.expedia.www.haystack.trace.reader.readers.transformers.SortSpanTransformer +import com.expedia.www.haystack.trace.reader.readers.utils.MutableSpanForest +import com.expedia.www.haystack.trace.reader.unit.BaseUnitTestSpec + +class SortSpanTransformerSpec extends BaseUnitTestSpec { + + def createSpans(timestamp: Long): List[Span] = { + val traceId = "traceId" + + val spanA = Span.newBuilder() + .setSpanId("a") + .setTraceId(traceId) + .setStartTime(timestamp) + .setDuration(1000) + .build() + + val spanB = Span.newBuilder() + .setSpanId("b") + .setParentSpanId("a") + .setTraceId(traceId) + .setStartTime(timestamp + 50) + .setDuration(100) + .build() + + val spanC = Span.newBuilder() + .setSpanId("c") + .setParentSpanId("a") + .setTraceId(traceId) + .setStartTime(timestamp + 100) + .setDuration(100) + .build() + + val spanD = Span.newBuilder() + .setSpanId("d") + .setParentSpanId("a") + .setTraceId(traceId) + .setStartTime(timestamp + 200) + .setDuration(100) + .build() + + val spanE = Span.newBuilder() + .setSpanId("e") + .setParentSpanId("b") + .setTraceId(traceId) + .setStartTime(timestamp + 300) + .setDuration(100) + .build() + + List(spanE, spanB, spanC, spanA, spanD) + } + + describe("SortSpanTransformer") { + it("should sort spans in natural order") { + Given("trace with spans") + val timestamp = 150000000000l + val spans = createSpans(timestamp) + + When("invoking transform") + val transformedSpans = new SortSpanTransformer().transform(MutableSpanForest(spans)).getUnderlyingSpans + + Then("return spans in sorted order") + transformedSpans.length should be(5) + transformedSpans.head.getSpanId should be("a") + transformedSpans(1).getSpanId should be("b") + transformedSpans(2).getSpanId should be("c") + transformedSpans(3).getSpanId should be("d") + transformedSpans(4).getSpanId should be("e") + } + } +} diff --git a/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/readers/validators/ParentIdValidatorSpec.scala b/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/readers/validators/ParentIdValidatorSpec.scala new file mode 100644 index 000000000..20d8541a2 --- /dev/null +++ b/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/readers/validators/ParentIdValidatorSpec.scala @@ -0,0 +1,77 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expedia.www.haystack.trace.reader.unit.readers.validators + +import com.expedia.open.tracing.Span +import com.expedia.open.tracing.api.Trace +import com.expedia.www.haystack.trace.reader.exceptions.InvalidTraceException +import com.expedia.www.haystack.trace.reader.readers.validators.ParentIdValidator +import com.expedia.www.haystack.trace.reader.unit.BaseUnitTestSpec + +class ParentIdValidatorSpec extends BaseUnitTestSpec { + val TRACE_ID = "traceId" + + describe("ParentIdValidator") { + it("should fail for traces with spans having same id and parent id") { + Given("trace with span having same span and parent id") + val trace = Trace.newBuilder() + .setTraceId(TRACE_ID) + .addChildSpans(Span.newBuilder().setTraceId(TRACE_ID).setSpanId("rootSpanId")) + .addChildSpans(Span.newBuilder().setTraceId(TRACE_ID).setSpanId("spanId").setParentSpanId("spanId")) + .build() + + When("on validate") + val validationResult = new ParentIdValidator().validate(trace) + + Then("fail with InvalidTraceException") + val thrown = the[InvalidTraceException] thrownBy validationResult.get + thrown.getStatus.getDescription shouldEqual "Invalid Trace: same parent and span id found for one ore more span for traceId=traceId" + } + + it("should fail for traces with spans without parents") { + Given("trace with empty traceId") + val trace = Trace.newBuilder() + .setTraceId(TRACE_ID) + .addChildSpans(Span.newBuilder().setTraceId(TRACE_ID).setSpanId("a")) + .addChildSpans(Span.newBuilder().setTraceId(TRACE_ID).setSpanId("b").setParentSpanId("x")) + .build() + + When("on validate") + val validationResult = new ParentIdValidator().validate(trace) + + Then("fail with InvalidTraceException") + val thrown = the[InvalidTraceException] thrownBy validationResult.get + thrown.getStatus.getDescription shouldEqual "Invalid Trace: spans without valid parent found for traceId=traceId" + } + + it("should accept valid traces") { + Given("trace with valid spans") + val trace = Trace.newBuilder() + .setTraceId(TRACE_ID) + .addChildSpans(Span.newBuilder().setTraceId(TRACE_ID).setSpanId("a")) + .addChildSpans(Span.newBuilder().setTraceId(TRACE_ID).setSpanId("b").setParentSpanId("a")) + .addChildSpans(Span.newBuilder().setTraceId(TRACE_ID).setSpanId("c").setParentSpanId("a")) + .build() + + When("on validate") + val validationResult = new ParentIdValidator().validate(trace) + + Then("accept trace") + noException should be thrownBy validationResult.get + } + } +} diff --git a/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/readers/validators/RootValidatorSpec.scala b/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/readers/validators/RootValidatorSpec.scala new file mode 100644 index 000000000..fcbeabd98 --- /dev/null +++ b/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/readers/validators/RootValidatorSpec.scala @@ -0,0 +1,77 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expedia.www.haystack.trace.reader.unit.readers.validators + +import com.expedia.open.tracing.Span +import com.expedia.open.tracing.api.Trace +import com.expedia.www.haystack.trace.reader.exceptions.InvalidTraceException +import com.expedia.www.haystack.trace.reader.readers.validators.RootValidator +import com.expedia.www.haystack.trace.reader.unit.BaseUnitTestSpec + +class RootValidatorSpec extends BaseUnitTestSpec { + val TRACE_ID = "traceId" + + describe("RootValidator") { + it("should fail for traces with multiple spans as root") { + Given("trace with empty traceId") + val trace = Trace.newBuilder() + .setTraceId("traceId") + .addChildSpans(Span.newBuilder().setTraceId(TRACE_ID).setSpanId("a")) + .addChildSpans(Span.newBuilder().setTraceId(TRACE_ID).setSpanId("b")) + .build() + + When("on validate") + val validationResult = new RootValidator().validate(trace) + + Then("fail with InvalidTraceException") + val thrown = the[InvalidTraceException] thrownBy validationResult.get + thrown.getStatus.getDescription shouldEqual "Invalid Trace: found 2 roots with spanIDs=a,b and traceID=traceId" + } + + it("should fail for traces with no root") { + Given("trace with empty traceId") + val trace = Trace.newBuilder() + .setTraceId("traceId") + .addChildSpans(Span.newBuilder().setTraceId(TRACE_ID).setSpanId("a").setParentSpanId("x")) + .addChildSpans(Span.newBuilder().setTraceId(TRACE_ID).setSpanId("b").setParentSpanId("x")) + .build() + + When("on validate") + val validationResult = new RootValidator().validate(trace) + + Then("fail with InvalidTraceException") + val thrown = the[InvalidTraceException] thrownBy validationResult.get + thrown.getStatus.getDescription shouldEqual "Invalid Trace: found 0 roots with spanIDs= and traceID=traceId" + } + + it("should accept valid traces") { + Given("trace with valid spans") + val trace = Trace.newBuilder() + .setTraceId(TRACE_ID) + .addChildSpans(Span.newBuilder().setTraceId(TRACE_ID).setSpanId("a")) + .addChildSpans(Span.newBuilder().setTraceId(TRACE_ID).setSpanId("b").setParentSpanId("a")) + .addChildSpans(Span.newBuilder().setTraceId(TRACE_ID).setSpanId("c").setParentSpanId("a")) + .build() + + When("on validate") + val validationResult = new RootValidator().validate(trace) + + Then("accept trace") + noException should be thrownBy validationResult.get + } + } +} diff --git a/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/readers/validators/TraceIdValidatorSpec.scala b/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/readers/validators/TraceIdValidatorSpec.scala new file mode 100644 index 000000000..3e22edbe8 --- /dev/null +++ b/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/readers/validators/TraceIdValidatorSpec.scala @@ -0,0 +1,72 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expedia.www.haystack.trace.reader.unit.readers.validators + +import com.expedia.open.tracing.Span +import com.expedia.open.tracing.api.Trace +import com.expedia.www.haystack.trace.reader.exceptions.InvalidTraceException +import com.expedia.www.haystack.trace.reader.readers.validators.TraceIdValidator +import com.expedia.www.haystack.trace.reader.unit.BaseUnitTestSpec + +class TraceIdValidatorSpec extends BaseUnitTestSpec { + describe("TraceIdValidator") { + val TRACE_ID = "traceId" + + it("should fail for traces with empty traceId") { + Given("trace with empty traceId") + val trace = Trace.newBuilder().build() + + When("on validate") + val validationResult = new TraceIdValidator().validate(trace) + + Then("Fail with InvalidTraceException") + val thrown = the[InvalidTraceException] thrownBy validationResult.get + thrown.getStatus.getDescription should include("invalid traceId") + } + + it("should fail for traces with spans having different traceId") { + Given("trace with span having different id") + val trace = Trace.newBuilder() + .setTraceId("traceId") + .addChildSpans(Span.newBuilder().setTraceId("dummy").setSpanId("spanId")) + .build() + + When("on validate") + val validationResult = new TraceIdValidator().validate(trace) + + Then("Fail with InvalidTraceException") + val thrown = the[InvalidTraceException] thrownBy validationResult.get + thrown.getStatus.getDescription should include("span with different traceId") + } + + it("should accept valid traces") { + Given("trace with valid spans") + val trace = Trace.newBuilder() + .setTraceId(TRACE_ID) + .addChildSpans(Span.newBuilder().setTraceId(TRACE_ID).setSpanId("a")) + .addChildSpans(Span.newBuilder().setTraceId(TRACE_ID).setSpanId("b").setParentSpanId("a")) + .addChildSpans(Span.newBuilder().setTraceId(TRACE_ID).setSpanId("c").setParentSpanId("a")) + .build() + + When("on validate") + val validationResult = new TraceIdValidator().validate(trace) + + Then("accept trace") + noException should be thrownBy validationResult.get + } + } +} diff --git a/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/stores/ResponseParserSpec.scala b/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/stores/ResponseParserSpec.scala new file mode 100644 index 000000000..7136c4a90 --- /dev/null +++ b/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/stores/ResponseParserSpec.scala @@ -0,0 +1,119 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expedia.www.haystack.trace.reader.unit.stores + +import com.expedia.www.haystack.trace.reader.stores.ResponseParser +import com.expedia.www.haystack.trace.reader.unit.BaseUnitTestSpec +import com.google.gson.{Gson, JsonParser} +import io.searchbox.core.SearchResult +import scala.concurrent.ExecutionContext.Implicits.global + +class ResponseParserSpec extends BaseUnitTestSpec with ResponseParser { + describe("ResponseParserSpec") { + it("should be able to parse the search result to trace counts") { + + Given("a trace search response") + val result = new SearchResult(new Gson()) + result.setSucceeded(true) + result.setJsonString(getJson()) + result.setJsonObject(new JsonParser().parse(getJson()).getAsJsonObject) + + When("map search result to trace counts") + val traceCounts = mapSearchResultToTraceCounts(result) + + Then("generate a valid query") + traceCounts should not be None + traceCounts.map(traceCounts => traceCounts.getTraceCountCount shouldEqual 11) + } + } + + def getJson(): String = { + """ + |{ + | "took": 41810, + | "timed_out": false, + | "_shards": { + | "total": 240, + | "successful": 240, + | "skipped": 0, + | "failed": 0 + | }, + | "hits": { + | "total": 10052727254, + | "max_score": 0.0, + | "hits": [] + | }, + | "aggregations": { + | "spans": { + | "doc_count": 23138047525, + | "spans": { + | "doc_count": 2604513, + | "__count_per_interval": { + | "buckets": [ + | { + | "key": 1.52690406E15, + | "doc_count": 150949 + | }, + | { + | "key": 1.52690412E15, + | "doc_count": 262163 + | }, + | { + | "key": 1.52690418E15, + | "doc_count": 259394 + | }, + | { + | "key": 1.52690424E15, + | "doc_count": 253247 + | }, + | { + | "key": 1.5269043E15, + | "doc_count": 253589 + | }, + | { + | "key": 1.52690436E15, + | "doc_count": 261232 + | }, + | { + | "key": 1.52690442E15, + | "doc_count": 258264 + | }, + | { + | "key": 1.52690448E15, + | "doc_count": 270179 + | }, + | { + | "key": 1.52690454E15, + | "doc_count": 266545 + | }, + | { + | "key": 1.5269046E15, + | "doc_count": 264921 + | }, + | { + | "key": 1.52690466E15, + | "doc_count": 104084 + | } + | ] + | } + | } + | } + | } + |} + """.stripMargin + } +} diff --git a/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/stores/readers/es/query/ElasticSearchReadResultListenerSpec.scala b/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/stores/readers/es/query/ElasticSearchReadResultListenerSpec.scala new file mode 100644 index 000000000..c9a7107e2 --- /dev/null +++ b/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/stores/readers/es/query/ElasticSearchReadResultListenerSpec.scala @@ -0,0 +1,111 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trace.reader.unit.stores.readers.es.query + +import com.codahale.metrics.{Meter, Timer} +import com.expedia.open.tracing.api.{Field, TracesSearchRequest} +import com.expedia.www.haystack.trace.commons.config.entities.{IndexFieldType, WhitelistIndexFieldConfiguration} +import com.expedia.www.haystack.trace.reader.config.entities.SpansIndexConfiguration +import com.expedia.www.haystack.trace.reader.exceptions.ElasticSearchClientError +import com.expedia.www.haystack.trace.reader.stores.readers.es.ElasticSearchReadResultListener +import com.expedia.www.haystack.trace.reader.stores.readers.es.query.TraceSearchQueryGenerator +import com.expedia.www.haystack.trace.reader.unit.BaseUnitTestSpec +import io.searchbox.core.SearchResult +import org.easymock.EasyMock +import org.json4s.ext.EnumNameSerializer +import org.json4s.{DefaultFormats, Formats} + +import scala.concurrent.Promise + +class ElasticSearchReadResultListenerSpec extends BaseUnitTestSpec { + protected implicit val formats: Formats = DefaultFormats + new EnumNameSerializer(IndexFieldType) + val ES_INDEX_HOUR_BUCKET = 6 + val ES_INDEX_HOUR_TTL = 72 + + private val spansIndexConfiguration = SpansIndexConfiguration( + indexNamePrefix = "haystack-traces", + indexType = "spans", + indexHourTtl = ES_INDEX_HOUR_TTL, + indexHourBucket = ES_INDEX_HOUR_BUCKET, + useRootDocumentStartTime = false) + + + private val searchRequest = { + val generator = new TraceSearchQueryGenerator(spansIndexConfiguration, "spans", WhitelistIndexFieldConfiguration()) + val field = Field.newBuilder().setName("serviceName").setValue("expweb").build() + generator.generate(TracesSearchRequest.newBuilder().setStartTime(1510469157572000l).setEndTime(1510469161172000l).setLimit(40).addFields(field).build(), true) + } + + describe("ElasticSearch Read Result Listener") { + it("should invoke successful promise with search result") { + val promise = mock[Promise[SearchResult]] + val timer = mock[Timer.Context] + val failureMeter = mock[Meter] + val searchResult = mock[SearchResult] + + expecting { + timer.close().once() + searchResult.getResponseCode.andReturn(200).atLeastOnce() + promise.success(searchResult).andReturn(promise).once() + } + + whenExecuting(promise, timer, failureMeter, searchResult) { + val listener = new ElasticSearchReadResultListener(searchRequest, promise, timer, failureMeter) + listener.completed(searchResult) + } + } + + it("should invoke failed promise with exception object if response code is not 2xx ") { + val promise = mock[Promise[SearchResult]] + val timer = mock[Timer.Context] + val failureMeter = mock[Meter] + val searchResult = mock[SearchResult] + + expecting { + timer.close().once() + searchResult.getResponseCode.andReturn(500).atLeastOnce() + searchResult.getJsonString.andReturn("json-string").times(2) + failureMeter.mark() + promise.failure(EasyMock.anyObject(classOf[ElasticSearchClientError])).andReturn(promise).once() + } + + whenExecuting(promise, timer, failureMeter, searchResult) { + val listener = new ElasticSearchReadResultListener(searchRequest, promise, timer, failureMeter) + listener.completed(searchResult) + } + } + + it("should invoke failed promise with exception object if failure is generated") { + val promise = mock[Promise[SearchResult]] + val timer = mock[Timer.Context] + val failureMeter = mock[Meter] + val expectedException = new Exception + + expecting { + timer.close().once() + failureMeter.mark() + promise.failure(expectedException).andReturn(promise).once() + } + + whenExecuting(promise, timer, failureMeter) { + val listener = new ElasticSearchReadResultListener(searchRequest, promise, timer, failureMeter) + listener.failed(expectedException) + } + } + } +} diff --git a/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/stores/readers/es/query/FieldValuesQueryGeneratorSpec.scala b/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/stores/readers/es/query/FieldValuesQueryGeneratorSpec.scala new file mode 100644 index 000000000..9141ae7aa --- /dev/null +++ b/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/stores/readers/es/query/FieldValuesQueryGeneratorSpec.scala @@ -0,0 +1,73 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.expedia.www.haystack.trace.reader.unit.stores.readers.es.query + +import com.expedia.open.tracing.api.{Field, FieldValuesRequest} +import com.expedia.www.haystack.trace.commons.config.entities.WhitelistIndexFieldConfiguration +import com.expedia.www.haystack.trace.reader.config.entities.SpansIndexConfiguration +import com.expedia.www.haystack.trace.reader.stores.readers.es.ESUtils._ +import com.expedia.www.haystack.trace.reader.stores.readers.es.query.FieldValuesQueryGenerator +import com.expedia.www.haystack.trace.reader.unit.BaseUnitTestSpec + +class FieldValuesQueryGeneratorSpec extends BaseUnitTestSpec { + private val indexType = "spans" + private val spansIndexConfiguration = SpansIndexConfiguration( + indexNamePrefix = "haystack-traces", + indexType = indexType, + indexHourTtl = 72, + indexHourBucket = 6, + useRootDocumentStartTime = false) + + describe("FieldValuesQueryGenerator") { + it("should generate valid search queries") { + Given("a trace search request") + val serviceName = "svcName" + val request = FieldValuesRequest + .newBuilder() + .setFieldName("operationName") + .addFilters(Field.newBuilder().setName("serviceName").setValue(serviceName).build()) + .build() + val queryGenerator = new FieldValuesQueryGenerator(spansIndexConfiguration, "spans", new WhitelistIndexFieldConfiguration) + + When("generating query") + val query = queryGenerator.generate(request) + + Then("generate a valid query") + query.getType should be(indexType) + } + + it("should generate caption independent search queries") { + Given("a trace search request") + val serviceField = "serviceName" + val operationField = "operationName" + val serviceName = "svcName" + val request = FieldValuesRequest + .newBuilder() + .setFieldName(operationField) + .addFilters(Field.newBuilder().setName(serviceField).setValue(serviceName).build()) + .build() + val queryGenerator = new FieldValuesQueryGenerator(spansIndexConfiguration, "spans", new WhitelistIndexFieldConfiguration) + + When("generating query") + val query = queryGenerator.generate(request) + + Then("generate a valid query with fields in lowercase") + val queryString = query.toJson + queryString.contains(serviceField.toLowerCase()) should be(true) + queryString.contains(operationField.toLowerCase()) should be(true) + } + } +} \ No newline at end of file diff --git a/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/stores/readers/es/query/ServiceMetadataQueryGeneratorSpec.scala b/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/stores/readers/es/query/ServiceMetadataQueryGeneratorSpec.scala new file mode 100644 index 000000000..d2301514d --- /dev/null +++ b/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/stores/readers/es/query/ServiceMetadataQueryGeneratorSpec.scala @@ -0,0 +1,55 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.expedia.www.haystack.trace.reader.unit.stores.readers.es.query + +import com.expedia.www.haystack.trace.reader.config.entities.ServiceMetadataIndexConfiguration +import com.expedia.www.haystack.trace.reader.stores.readers.es.query.ServiceMetadataQueryGenerator +import com.expedia.www.haystack.trace.reader.unit.BaseUnitTestSpec +import com.google.gson.Gson + +class ServiceMetadataQueryGeneratorSpec extends BaseUnitTestSpec { + private val indexType = "metadata" + private val serviceMetadataIndexConfiguration = ServiceMetadataIndexConfiguration( + enabled = true, + indexName = "service_metadata", + indexType = indexType) + + describe("ServiceMetadataQueryGenerator") { + it("should generate valid aggregation queries for service names") { + Given("a query generator") + val queryGenerator = new ServiceMetadataQueryGenerator(serviceMetadataIndexConfiguration) + + When("asked for aggregated service name") + val query = queryGenerator.generateSearchServiceQuery() + + Then("generate a valid query") + query.getType should be(indexType) + query.getData(new Gson()) shouldEqual "{\n \"size\" : 0,\n \"aggregations\" : {\n \"distinct_services\" : {\n \"terms\" : {\n \"field\" : \"servicename\",\n \"size\" : 10000,\n \"min_doc_count\" : 1,\n \"shard_min_doc_count\" : 0,\n \"show_term_doc_count_error\" : false,\n \"order\" : [\n {\n \"_count\" : \"desc\"\n },\n {\n \"_key\" : \"asc\"\n }\n ]\n }\n }\n }\n}" + query.toString shouldEqual "Search{uri=service_metadata/metadata/_search, method=POST}" + } + + it("should generate valid aggregation queries for operation names") { + Given("a query generator and a service name") + val queryGenerator = new ServiceMetadataQueryGenerator(serviceMetadataIndexConfiguration) + val serviceName = "test_service" + When("asked for aggregated operation names") + val query = queryGenerator.generateSearchOperationQuery(serviceName) + + Then("generate a valid query") + query.getType should be(indexType) + } + } +} diff --git a/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/stores/readers/es/query/TraceCountsQueryGeneratorSpec.scala b/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/stores/readers/es/query/TraceCountsQueryGeneratorSpec.scala new file mode 100644 index 000000000..b19e92473 --- /dev/null +++ b/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/stores/readers/es/query/TraceCountsQueryGeneratorSpec.scala @@ -0,0 +1,230 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expedia.www.haystack.trace.reader.unit.stores.readers.es.query + +import java.util.concurrent.TimeUnit + +import com.expedia.open.tracing.api._ +import com.expedia.www.haystack.trace.commons.clients.es.document.TraceIndexDoc +import com.expedia.www.haystack.trace.commons.config.entities.WhitelistIndexFieldConfiguration +import com.expedia.www.haystack.trace.reader.config.entities.SpansIndexConfiguration +import com.expedia.www.haystack.trace.reader.stores.readers.es.query.TraceCountsQueryGenerator +import com.expedia.www.haystack.trace.reader.unit.BaseUnitTestSpec +import com.expedia.www.haystack.trace.reader.unit.stores.readers.es.query.helper.ExpressionTreeBuilder._ +import com.google.gson.Gson +import io.searchbox.core.Search + +class TraceCountsQueryGeneratorSpec extends BaseUnitTestSpec { + private val ES_INDEX_HOUR_BUCKET = 6 + private val ES_INDEX_HOUR_TTL = 72 + private val INDEX_NAME_PREFIX = "haystack-spans" + private val interval = TimeUnit.SECONDS.toMicros(60) + + private val spansIndexConfiguration = SpansIndexConfiguration( + indexNamePrefix = INDEX_NAME_PREFIX, + indexType = "spans", + indexHourTtl = ES_INDEX_HOUR_TTL, + indexHourBucket = ES_INDEX_HOUR_BUCKET, + useRootDocumentStartTime = true) + + describe("TraceSearchQueryGenerator") { + it("should generate valid search queries") { + Given("a trace search request") + val serviceName = "svcName" + val operationName = "opName" + val startTime = 1529418475791000l // Tuesday, June 19, 2018 2:27:55.791 PM + val endTime = 1529419075791000l // Tuesday, June 19, 2018 2:37:55.791 PM + val request = TraceCountsRequest + .newBuilder() + .addFields(Field.newBuilder().setName(TraceIndexDoc.SERVICE_KEY_NAME).setValue(serviceName).build()) + .addFields(Field.newBuilder().setName(TraceIndexDoc.OPERATION_KEY_NAME).setValue(operationName).build()) + .setStartTime(startTime) + .setEndTime(endTime) + .setInterval(interval) + .build() + val queryGenerator = new TraceCountsQueryGenerator(spansIndexConfiguration, "spans", new WhitelistIndexFieldConfiguration) + + When("generating query") + val query = queryGenerator.generate(request) + Then("generate a valid query") + query.getData(new Gson()).replaceAll("\n", "").replaceAll(" ", "") shouldEqual "{\"size\":0,\"query\":{\"bool\":{\"must\":[{\"range\":{\"starttime\":{\"from\":1529418475791000,\"to\":1529419075791000,\"include_lower\":true,\"include_upper\":true,\"boost\":1.0}}}],\"filter\":[{\"nested\":{\"query\":{\"bool\":{\"filter\":[{\"term\":{\"spans.servicename\":{\"value\":\"svcName\",\"boost\":1.0}}},{\"term\":{\"spans.operationname\":{\"value\":\"opName\",\"boost\":1.0}}}],\"adjust_pure_negative\":true,\"boost\":1.0}},\"path\":\"spans\",\"ignore_unmapped\":false,\"score_mode\":\"none\",\"boost\":1.0}}],\"adjust_pure_negative\":true,\"boost\":1.0}},\"aggregations\":{\"countagg\":{\"histogram\":{\"field\":\"starttime\",\"interval\":6.0E7,\"offset\":0.0,\"order\":{\"_key\":\"asc\"},\"keyed\":false,\"min_doc_count\":0,\"extended_bounds\":{\"min\":1.529418475791E15,\"max\":1.529419075791E15}}}}}" + query.getURI shouldEqual "haystack-spans-2018-06-19-2/spans/_search" + } + + it("should generate valid search queries for bucketed search count") { + Given("a trace search request") + val serviceName = "svcName" + val operationName = "opName" + val startTimeInMicros = 1 + val endTimeInMicros = 1527487220L * 1000 * 1000 // May 28, 2018 6:00:20 AM + val request = TraceCountsRequest + .newBuilder() + .addFields(Field.newBuilder().setName(TraceIndexDoc.SERVICE_KEY_NAME).setValue(serviceName).build()) + .addFields(Field.newBuilder().setName(TraceIndexDoc.OPERATION_KEY_NAME).setValue(operationName).build()) + .setStartTime(startTimeInMicros) + .setEndTime(endTimeInMicros) + .setInterval(interval) + .build() + val queryGenerator = new TraceCountsQueryGenerator(spansIndexConfiguration, "spans", new WhitelistIndexFieldConfiguration) + + When("generating query") + val query = queryGenerator.generate(request) + + Then("generate a valid query") + query.getURI shouldEqual "haystack-spans/spans/_search" + } + + it("should return a valid list of indexes for overlapping time range") { + Given("starttime and endtime") + val startTimeInMicros = 1527501725L * 1000 * 1000 // Monday, May 28, 2018 10:03:36 AM + val endTimeInMicros = 1527512524L * 1000 * 1000 // Monday, May 28, 2018 1:02:04 PM + val queryGenerator = new TraceCountsQueryGenerator(spansIndexConfiguration, "spans", WhitelistIndexFieldConfiguration()) + + When("retrieving index names") + val indexNames = queryGenerator.getESIndexes(startTimeInMicros, endTimeInMicros, INDEX_NAME_PREFIX, ES_INDEX_HOUR_BUCKET, ES_INDEX_HOUR_TTL) + + Then("should get index names") + indexNames should not be null + indexNames.size shouldEqual 2 + indexNames should contain allOf("haystack-spans-2018-05-28-1", "haystack-spans-2018-05-28-2") + } + + it("should return a valid list of indexes") { + Given("starttime and endtime") + val startTimeInMicros = 1527487200L * 1000 * 1000 // May 28, 2018 6:00:00 AM + val endTimeInMicros = 1527508800L * 1000 * 1000 // May 28, 2018 12:00:00 PM + val queryGenerator = new TraceCountsQueryGenerator(spansIndexConfiguration, "spans", WhitelistIndexFieldConfiguration()) + + When("retrieving index names") + val indexNames = queryGenerator.getESIndexes(startTimeInMicros, endTimeInMicros, INDEX_NAME_PREFIX, ES_INDEX_HOUR_BUCKET, ES_INDEX_HOUR_TTL) + + Then("should get index names") + indexNames should not be null + indexNames.size shouldEqual 2 + indexNames should contain allOf("haystack-spans-2018-05-28-1", "haystack-spans-2018-05-28-2") + } + + it("should return only a single index name for time range within same bucket") { + Given("starttime and endtime") + val starttimeInMicros = 1527487100L * 1000 * 1000 // May 28, 2018 5:58:20 AM + val endtimeInMicros = 1527487120L * 1000 * 1000 // May 28, 2018 5:58:40 AM + val queryGenerator = new TraceCountsQueryGenerator(spansIndexConfiguration, "spans", WhitelistIndexFieldConfiguration()) + + When("retrieving index names") + val indexNames = queryGenerator.getESIndexes(starttimeInMicros, endtimeInMicros, INDEX_NAME_PREFIX, ES_INDEX_HOUR_BUCKET, ES_INDEX_HOUR_TTL) + + Then("should get index names") + indexNames should not be null + indexNames.size shouldBe 1 + indexNames.head shouldEqual "haystack-spans-2018-05-28-0" + } + + it("should return index alias (not return specific index) in case endtime minus starttime exceeds index retention") { + Given("starttime and endtime") + val startTimeInMicros = 0 + val endTimeInMicros = 1527487220L * 1000 * 1000 // May 28, 2018 6:00:20 AM + val queryGenerator = new TraceCountsQueryGenerator(spansIndexConfiguration, "spans", WhitelistIndexFieldConfiguration()) + + When("retrieving index names") + val indexNames = queryGenerator.getESIndexes(startTimeInMicros, endTimeInMicros, INDEX_NAME_PREFIX, ES_INDEX_HOUR_BUCKET, ES_INDEX_HOUR_TTL) + + Then("should get index names") + indexNames should not be null + indexNames.size shouldEqual 1 + indexNames.head shouldEqual INDEX_NAME_PREFIX + } + + it("should generate valid count queries for expression tree based search counts") { + Given("a trace count request") + val startTime = 1529418475791000l // Tuesday, June 19, 2018 2:27:55.791 PM + val endTime = 1529419075791000l + val request = TraceCountsRequest + .newBuilder() + .setFilterExpression(operandLevelExpressionTree) + .setStartTime(startTime) + .setEndTime(endTime) + .setInterval(interval) + .build() + + val queryGenerator = new TraceCountsQueryGenerator(spansIndexConfiguration, "spans", WhitelistIndexFieldConfiguration()) + + When("generating query") + val query: Search = queryGenerator.generate(request) + + Then("generate a valid query with fields in lowercase") + query.getData(new Gson()).replaceAll("\n", "").replaceAll(" ", "") shouldEqual + "{\"size\":0,\"query\":{\"bool\":{\"must\":[{\"range\":{\"starttime\":{\"from\":1529418475791000,\"to\":1529419075791000,\"include_lower\":true,\"include_upper\":true,\"boost\":1.0}}}],\"filter\":[{\"nested\":{\"query\":{\"bool\":{\"filter\":[{\"term\":{\"spans.svcname\":{\"value\":\"svcValue\",\"boost\":1.0}}}],\"adjust_pure_negative\":true,\"boost\":1.0}},\"path\":\"spans\",\"ignore_unmapped\":false,\"score_mode\":\"none\",\"boost\":1.0}},{\"nested\":{\"query\":{\"bool\":{\"filter\":[{\"term\":{\"spans.1\":{\"value\":\"1\",\"boost\":1.0}}}],\"adjust_pure_negative\":true,\"boost\":1.0}},\"path\":\"spans\",\"ignore_unmapped\":false,\"score_mode\":\"none\",\"boost\":1.0}},{\"nested\":{\"query\":{\"bool\":{\"filter\":[{\"term\":{\"spans.2\":{\"value\":\"2\",\"boost\":1.0}}}],\"adjust_pure_negative\":true,\"boost\":1.0}},\"path\":\"spans\",\"ignore_unmapped\":false,\"score_mode\":\"none\",\"boost\":1.0}},{\"nested\":{\"query\":{\"bool\":{\"filter\":[{\"term\":{\"spans.3\":{\"value\":\"3\",\"boost\":1.0}}}],\"adjust_pure_negative\":true,\"boost\":1.0}},\"path\":\"spans\",\"ignore_unmapped\":false,\"score_mode\":\"none\",\"boost\":1.0}}],\"adjust_pure_negative\":true,\"boost\":1.0}},\"aggregations\":{\"countagg\":{\"histogram\":{\"field\":\"starttime\",\"interval\":6.0E7,\"offset\":0.0,\"order\":{\"_key\":\"asc\"},\"keyed\":false,\"min_doc_count\":0,\"extended_bounds\":{\"min\":1.529418475791E15,\"max\":1.529419075791E15}}}}}" + query.getURI shouldEqual "haystack-spans-2018-06-19-2/spans/_search" + } + + + it("should generate valid count query for expression tree based searches with span level searches") { + Given("a trace count request") + val startTime = 1529418475791000l // Tuesday, June 19, 2018 2:27:55.791 PM + val endTime = 1529419075791000l + val request = TraceCountsRequest + .newBuilder() + .setFilterExpression(spanLevelExpressionTree) + .setStartTime(startTime) + .setEndTime(endTime) + .setInterval(interval) + .build() + + val queryGenerator = new TraceCountsQueryGenerator(spansIndexConfiguration, "spans", WhitelistIndexFieldConfiguration()) + + When("generating query") + val query: Search = queryGenerator.generate(request) + + Then("generate a valid query") + query.getData(new Gson()).replaceAll("\n", "").replaceAll(" ", "") shouldEqual + "{\"size\":0,\"query\":{\"bool\":{\"must\":[{\"range\":{\"starttime\":{\"from\":1529418475791000,\"to\":1529419075791000,\"include_lower\":true,\"include_upper\":true,\"boost\":1.0}}}],\"filter\":[{\"nested\":{\"query\":{\"bool\":{\"filter\":[{\"term\":{\"spans.1\":{\"value\":\"1\",\"boost\":1.0}}},{\"term\":{\"spans.2\":{\"value\":\"2\",\"boost\":1.0}}},{\"term\":{\"spans.3\":{\"value\":\"3\",\"boost\":1.0}}}],\"adjust_pure_negative\":true,\"boost\":1.0}},\"path\":\"spans\",\"ignore_unmapped\":false,\"score_mode\":\"none\",\"boost\":1.0}},{\"nested\":{\"query\":{\"bool\":{\"filter\":[{\"term\":{\"spans.4\":{\"value\":\"4\",\"boost\":1.0}}},{\"term\":{\"spans.5\":{\"value\":\"5\",\"boost\":1.0}}}],\"adjust_pure_negative\":true,\"boost\":1.0}},\"path\":\"spans\",\"ignore_unmapped\":false,\"score_mode\":\"none\",\"boost\":1.0}},{\"nested\":{\"query\":{\"bool\":{\"filter\":[{\"term\":{\"spans.svcname\":{\"value\":\"svcValue\",\"boost\":1.0}}}],\"adjust_pure_negative\":true,\"boost\":1.0}},\"path\":\"spans\",\"ignore_unmapped\":false,\"score_mode\":\"none\",\"boost\":1.0}},{\"nested\":{\"query\":{\"bool\":{\"filter\":[{\"term\":{\"spans.0\":{\"value\":\"0\",\"boost\":1.0}}}],\"adjust_pure_negative\":true,\"boost\":1.0}},\"path\":\"spans\",\"ignore_unmapped\":false,\"score_mode\":\"none\",\"boost\":1.0}}],\"adjust_pure_negative\":true,\"boost\":1.0}},\"aggregations\":{\"countagg\":{\"histogram\":{\"field\":\"starttime\",\"interval\":6.0E7,\"offset\":0.0,\"order\":{\"_key\":\"asc\"},\"keyed\":false,\"min_doc_count\":0,\"extended_bounds\":{\"min\":1.529418475791E15,\"max\":1.529419075791E15}}}}}" + + query.getURI shouldEqual "haystack-spans-2018-06-19-2/spans/_search" + } + + + it("should generate valid count query for expression tree with duration field types") { + Given("a trace count request") + val queryGenerator = new TraceCountsQueryGenerator(spansIndexConfiguration, "spans", WhitelistIndexFieldConfiguration()) + val startTime = 1529418475791000l // Tuesday, June 19, 2018 2:27:55.791 PM + val endTime = 1529419075791000l + + val requests = Seq(expressionTreeWithDurationFields, anotherExpressionTreeWithDurationFields, oneMoreExpressionTreeWithDurationFields, expressionTreeWithGreaterThanOperator) map { + expression => { + TraceCountsRequest + .newBuilder() + .setFilterExpression(expression) + .setStartTime(startTime) + .setEndTime(endTime) + .setInterval(interval) + .build() + } + } + When("generating query") + val queries: Seq[Search] = requests.map(req => queryGenerator.generate(req)) + + Then("generate a valid query") + queries.map(query => query.getData(new Gson()).replaceAll("\n", "").replaceAll(" ", "")) shouldEqual Seq( + "{\"size\":0,\"query\":{\"bool\":{\"must\":[{\"range\":{\"starttime\":{\"from\":1529418475791000,\"to\":1529419075791000,\"include_lower\":true,\"include_upper\":true,\"boost\":1.0}}}],\"filter\":[{\"nested\":{\"query\":{\"bool\":{\"filter\":[{\"term\":{\"spans.1\":{\"value\":\"1\",\"boost\":1.0}}},{\"term\":{\"spans.2\":{\"value\":\"2\",\"boost\":1.0}}},{\"term\":{\"spans.3\":{\"value\":\"3\",\"boost\":1.0}}}],\"adjust_pure_negative\":true,\"boost\":1.0}},\"path\":\"spans\",\"ignore_unmapped\":false,\"score_mode\":\"none\",\"boost\":1.0}},{\"nested\":{\"query\":{\"bool\":{\"filter\":[{\"term\":{\"spans.4\":{\"value\":\"4\",\"boost\":1.0}}},{\"term\":{\"spans.5\":{\"value\":\"5\",\"boost\":1.0}}}],\"adjust_pure_negative\":true,\"boost\":1.0}},\"path\":\"spans\",\"ignore_unmapped\":false,\"score_mode\":\"none\",\"boost\":1.0}},{\"nested\":{\"query\":{\"bool\":{\"filter\":[{\"term\":{\"spans.svcname\":{\"value\":\"svcValue\",\"boost\":1.0}}}],\"adjust_pure_negative\":true,\"boost\":1.0}},\"path\":\"spans\",\"ignore_unmapped\":false,\"score_mode\":\"none\",\"boost\":1.0}},{\"nested\":{\"query\":{\"bool\":{\"filter\":[{\"range\":{\"spans.duration\":{\"from\":500000,\"to\":null,\"include_lower\":false,\"include_upper\":true,\"boost\":1.0}}}],\"adjust_pure_negative\":true,\"boost\":1.0}},\"path\":\"spans\",\"ignore_unmapped\":false,\"score_mode\":\"none\",\"boost\":1.0}}],\"adjust_pure_negative\":true,\"boost\":1.0}},\"aggregations\":{\"countagg\":{\"histogram\":{\"field\":\"starttime\",\"interval\":6.0E7,\"offset\":0.0,\"order\":{\"_key\":\"asc\"},\"keyed\":false,\"min_doc_count\":0,\"extended_bounds\":{\"min\":1.529418475791E15,\"max\":1.529419075791E15}}}}}", + "{\"size\":0,\"query\":{\"bool\":{\"must\":[{\"range\":{\"starttime\":{\"from\":1529418475791000,\"to\":1529419075791000,\"include_lower\":true,\"include_upper\":true,\"boost\":1.0}}}],\"filter\":[{\"nested\":{\"query\":{\"bool\":{\"filter\":[{\"term\":{\"spans.1\":{\"value\":\"1\",\"boost\":1.0}}},{\"term\":{\"spans.2\":{\"value\":\"2\",\"boost\":1.0}}},{\"term\":{\"spans.3\":{\"value\":\"3\",\"boost\":1.0}}}],\"adjust_pure_negative\":true,\"boost\":1.0}},\"path\":\"spans\",\"ignore_unmapped\":false,\"score_mode\":\"none\",\"boost\":1.0}},{\"nested\":{\"query\":{\"bool\":{\"filter\":[{\"term\":{\"spans.4\":{\"value\":\"4\",\"boost\":1.0}}},{\"term\":{\"spans.5\":{\"value\":\"5\",\"boost\":1.0}}}],\"adjust_pure_negative\":true,\"boost\":1.0}},\"path\":\"spans\",\"ignore_unmapped\":false,\"score_mode\":\"none\",\"boost\":1.0}},{\"nested\":{\"query\":{\"bool\":{\"filter\":[{\"term\":{\"spans.svcname\":{\"value\":\"svcValue\",\"boost\":1.0}}}],\"adjust_pure_negative\":true,\"boost\":1.0}},\"path\":\"spans\",\"ignore_unmapped\":false,\"score_mode\":\"none\",\"boost\":1.0}},{\"nested\":{\"query\":{\"bool\":{\"filter\":[{\"range\":{\"spans.duration\":{\"from\":null,\"to\":180000000,\"include_lower\":true,\"include_upper\":false,\"boost\":1.0}}}],\"adjust_pure_negative\":true,\"boost\":1.0}},\"path\":\"spans\",\"ignore_unmapped\":false,\"score_mode\":\"none\",\"boost\":1.0}}],\"adjust_pure_negative\":true,\"boost\":1.0}},\"aggregations\":{\"countagg\":{\"histogram\":{\"field\":\"starttime\",\"interval\":6.0E7,\"offset\":0.0,\"order\":{\"_key\":\"asc\"},\"keyed\":false,\"min_doc_count\":0,\"extended_bounds\":{\"min\":1.529418475791E15,\"max\":1.529419075791E15}}}}}", + "{\"size\":0,\"query\":{\"bool\":{\"must\":[{\"range\":{\"starttime\":{\"from\":1529418475791000,\"to\":1529419075791000,\"include_lower\":true,\"include_upper\":true,\"boost\":1.0}}}],\"filter\":[{\"nested\":{\"query\":{\"bool\":{\"filter\":[{\"term\":{\"spans.1\":{\"value\":\"1\",\"boost\":1.0}}},{\"term\":{\"spans.2\":{\"value\":\"2\",\"boost\":1.0}}},{\"term\":{\"spans.3\":{\"value\":\"3\",\"boost\":1.0}}}],\"adjust_pure_negative\":true,\"boost\":1.0}},\"path\":\"spans\",\"ignore_unmapped\":false,\"score_mode\":\"none\",\"boost\":1.0}},{\"nested\":{\"query\":{\"bool\":{\"filter\":[{\"term\":{\"spans.4\":{\"value\":\"4\",\"boost\":1.0}}},{\"term\":{\"spans.5\":{\"value\":\"5\",\"boost\":1.0}}}],\"adjust_pure_negative\":true,\"boost\":1.0}},\"path\":\"spans\",\"ignore_unmapped\":false,\"score_mode\":\"none\",\"boost\":1.0}},{\"nested\":{\"query\":{\"bool\":{\"filter\":[{\"term\":{\"spans.svcname\":{\"value\":\"svcValue\",\"boost\":1.0}}}],\"adjust_pure_negative\":true,\"boost\":1.0}},\"path\":\"spans\",\"ignore_unmapped\":false,\"score_mode\":\"none\",\"boost\":1.0}},{\"nested\":{\"query\":{\"bool\":{\"filter\":[{\"range\":{\"spans.duration\":{\"from\":null,\"to\":2000000,\"include_lower\":true,\"include_upper\":false,\"boost\":1.0}}}],\"adjust_pure_negative\":true,\"boost\":1.0}},\"path\":\"spans\",\"ignore_unmapped\":false,\"score_mode\":\"none\",\"boost\":1.0}}],\"adjust_pure_negative\":true,\"boost\":1.0}},\"aggregations\":{\"countagg\":{\"histogram\":{\"field\":\"starttime\",\"interval\":6.0E7,\"offset\":0.0,\"order\":{\"_key\":\"asc\"},\"keyed\":false,\"min_doc_count\":0,\"extended_bounds\":{\"min\":1.529418475791E15,\"max\":1.529419075791E15}}}}}", + "{\"size\":0,\"query\":{\"bool\":{\"must\":[{\"range\":{\"starttime\":{\"from\":1529418475791000,\"to\":1529419075791000,\"include_lower\":true,\"include_upper\":true,\"boost\":1.0}}}],\"filter\":[{\"nested\":{\"query\":{\"bool\":{\"filter\":[{\"term\":{\"spans.1\":{\"value\":\"1\",\"boost\":1.0}}},{\"term\":{\"spans.2\":{\"value\":\"2\",\"boost\":1.0}}},{\"term\":{\"spans.3\":{\"value\":\"3\",\"boost\":1.0}}}],\"adjust_pure_negative\":true,\"boost\":1.0}},\"path\":\"spans\",\"ignore_unmapped\":false,\"score_mode\":\"none\",\"boost\":1.0}},{\"nested\":{\"query\":{\"bool\":{\"filter\":[{\"term\":{\"spans.4\":{\"value\":\"4\",\"boost\":1.0}}},{\"term\":{\"spans.5\":{\"value\":\"5\",\"boost\":1.0}}}],\"adjust_pure_negative\":true,\"boost\":1.0}},\"path\":\"spans\",\"ignore_unmapped\":false,\"score_mode\":\"none\",\"boost\":1.0}},{\"nested\":{\"query\":{\"bool\":{\"filter\":[{\"term\":{\"spans.svcname\":{\"value\":\"svcValue\",\"boost\":1.0}}}],\"adjust_pure_negative\":true,\"boost\":1.0}},\"path\":\"spans\",\"ignore_unmapped\":false,\"score_mode\":\"none\",\"boost\":1.0}},{\"nested\":{\"query\":{\"bool\":{\"filter\":[{\"range\":{\"spans.duration\":{\"from\":240000,\"to\":null,\"include_lower\":false,\"include_upper\":true,\"boost\":1.0}}}],\"adjust_pure_negative\":true,\"boost\":1.0}},\"path\":\"spans\",\"ignore_unmapped\":false,\"score_mode\":\"none\",\"boost\":1.0}}],\"adjust_pure_negative\":true,\"boost\":1.0}},\"aggregations\":{\"countagg\":{\"histogram\":{\"field\":\"starttime\",\"interval\":6.0E7,\"offset\":0.0,\"order\":{\"_key\":\"asc\"},\"keyed\":false,\"min_doc_count\":0,\"extended_bounds\":{\"min\":1.529418475791E15,\"max\":1.529419075791E15}}}}}") + + queries.map(query => query.getURI).toSet shouldEqual Set("haystack-spans-2018-06-19-2/spans/_search") + } + } +} diff --git a/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/stores/readers/es/query/TraceSearchQueryGeneratorSpec.scala b/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/stores/readers/es/query/TraceSearchQueryGeneratorSpec.scala new file mode 100644 index 000000000..c49403195 --- /dev/null +++ b/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/stores/readers/es/query/TraceSearchQueryGeneratorSpec.scala @@ -0,0 +1,189 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.expedia.www.haystack.trace.reader.unit.stores.readers.es.query + +import com.expedia.open.tracing.api.{Field, TracesSearchRequest} +import com.expedia.www.haystack.trace.commons.clients.es.document.TraceIndexDoc +import com.expedia.www.haystack.trace.commons.config.entities.WhitelistIndexFieldConfiguration +import com.expedia.www.haystack.trace.reader.config.entities.SpansIndexConfiguration +import com.expedia.www.haystack.trace.reader.stores.readers.es.ESUtils._ +import com.expedia.www.haystack.trace.reader.stores.readers.es.query.TraceSearchQueryGenerator +import com.expedia.www.haystack.trace.reader.unit.BaseUnitTestSpec +import com.expedia.www.haystack.trace.reader.unit.stores.readers.es.query.helper.ExpressionTreeBuilder._ +import com.google.gson.Gson +import io.searchbox.core.Search +import org.scalatest.BeforeAndAfterEach + +class TraceSearchQueryGeneratorSpec extends BaseUnitTestSpec with BeforeAndAfterEach { + private val spansIndexConfiguration = SpansIndexConfiguration( + indexNamePrefix = "haystack-traces", + indexType = "spans", + indexHourTtl = 72, + indexHourBucket = 6, + useRootDocumentStartTime = false) + + var timezone: String = _ + + override def beforeEach() { + timezone = System.getProperty("user.timezone") + System.setProperty("user.timezone", "CST") + } + + override def afterEach(): Unit = { + System.setProperty("user.timezone", timezone) + } + + describe("TraceSearchQueryGenerator") { + it("should generate valid search queries") { + Given("a trace search request") + val serviceName = "svcName" + val operationName = "opName" + val request = TracesSearchRequest + .newBuilder() + .addFields(Field.newBuilder().setName(TraceIndexDoc.SERVICE_KEY_NAME).setValue(serviceName).build()) + .addFields(Field.newBuilder().setName("operation").setValue(operationName).build()) + .setStartTime(1) + .setEndTime(System.currentTimeMillis() * 1000) + .setLimit(10) + .build() + val queryGenerator = new TraceSearchQueryGenerator(spansIndexConfiguration, "spans", WhitelistIndexFieldConfiguration()) + + When("generating query") + val query = queryGenerator.generate(request) + + Then("generate a valid query") + query.getType should be("spans") + } + + it("should generate caption independent search queries") { + Given("a trace search request") + val fieldKey = "svcName" + val fieldValue = "opName" + val request = TracesSearchRequest + .newBuilder() + .addFields(Field.newBuilder().setName(fieldKey).setValue(fieldValue).build()) + .setStartTime(1) + .setEndTime(System.currentTimeMillis() * 1000) + .setLimit(10) + .build() + val queryGenerator = new TraceSearchQueryGenerator(spansIndexConfiguration, "spans", new WhitelistIndexFieldConfiguration) + + When("generating query") + val query: Search = queryGenerator.generate(request) + + Then("generate a valid query with fields in lowercase") + query.toJson.contains(fieldKey.toLowerCase()) should be(true) + } + + it("should generate valid search queries for expression tree based searches") { + Given("a trace search request") + + val request = TracesSearchRequest + .newBuilder() + .setFilterExpression(operandLevelExpressionTree) + .setStartTime(1) + .setEndTime(System.currentTimeMillis() * 1000) + .setLimit(10) + .build() + + val queryGenerator = new TraceSearchQueryGenerator(spansIndexConfiguration, "spans", WhitelistIndexFieldConfiguration()) + + When("generating query") + val query: Search = queryGenerator.generate(request) + + Then("generate a valid query with fields in lowercase") + query.toJson.contains(fieldKey.toLowerCase()) should be(true) + } + + it("should generate valid search queries for expression tree based searches with span level searches") { + Given("a trace search request") + + val startTime = 1531454400L * 1000 * 1000 // July 13, 2018 04:00:00 AM (in microSec) + val endTime = 1531476000L * 1000 * 1000 // July 13, 2018 10:00:00 AM (in microSec) + + val request = TracesSearchRequest + .newBuilder() + .setFilterExpression(spanLevelExpressionTree) + .setStartTime(startTime) + .setEndTime(endTime) + .setLimit(10) + .build() + + val queryGenerator = new TraceSearchQueryGenerator(spansIndexConfiguration, "spans", WhitelistIndexFieldConfiguration()) + + When("generating query") + val query: Search = queryGenerator.generate(request) + + Then("generate a valid query with fields in lowercase") + query.toJson.contains(fieldKey.toLowerCase()) should be(true) + query.getIndex shouldBe "haystack-traces-2018-07-13-0,haystack-traces-2018-07-13-1" + } + + it("should use UTC when determining which indexes to read") { + Given("the system timezone is NOT UTC") + System.setProperty("user.timezone", "CST") + + When("getting the indexes") + val esIndexes = new TraceSearchQueryGenerator(spansIndexConfiguration, "spans", WhitelistIndexFieldConfiguration()).getESIndexes(1530806291394000L, 1530820646394000L, "haystack-traces", 4, 24) + + Then("they are correct based off of UTC") + esIndexes shouldBe Vector("haystack-traces-2018-07-05-3", "haystack-traces-2018-07-05-4") + } + + it("should query the mentioned index rather that calculated one") { + Given("a trace search request") + + val request = TracesSearchRequest + .newBuilder() + .setFilterExpression(spanLevelExpressionTree) + .setStartTime(1) + .setEndTime(System.currentTimeMillis() * 1000) + .setLimit(10) + .build() + + val queryGenerator = new TraceSearchQueryGenerator(spansIndexConfiguration, "spans", WhitelistIndexFieldConfiguration()) + + When("generating query") + val query: Search = queryGenerator.generate(request, useSpecificIndices = false) + + Then("generate a valid query with given index name") + query.toJson.contains(fieldKey.toLowerCase()) should be(true) + query.getIndex shouldBe "haystack-traces" + } + + it("should generate valid count query for expression tree with duration field types") { + Given("a trace count request") + val queryGenerator = new TraceSearchQueryGenerator(spansIndexConfiguration, "spans", WhitelistIndexFieldConfiguration()) + val requests = Seq(expressionTreeWithDurationFields) map { + expression => { + TracesSearchRequest + .newBuilder() + .setFilterExpression(expression) + .setStartTime(1) + .setEndTime(1100 * 1000 * 1000) + .setLimit(10) + .build() + } + } + When("generating query") + val queries: Seq[Search] = requests.map(req => queryGenerator.generate(req, useSpecificIndices = false)) + + Then("generate a valid query") + queries.map(query => query.getData(new Gson()).replaceAll("\n", "").replaceAll(" ", "")) shouldEqual Seq( + "{\"size\":10,\"query\":{\"bool\":{\"must\":[{\"nested\":{\"query\":{\"range\":{\"spans.starttime\":{\"from\":1,\"to\":1100000000,\"include_lower\":true,\"include_upper\":true,\"boost\":1.0}}},\"path\":\"spans\",\"ignore_unmapped\":false,\"score_mode\":\"none\",\"boost\":1.0}}],\"filter\":[{\"nested\":{\"query\":{\"bool\":{\"filter\":[{\"term\":{\"spans.1\":{\"value\":\"1\",\"boost\":1.0}}},{\"term\":{\"spans.2\":{\"value\":\"2\",\"boost\":1.0}}},{\"term\":{\"spans.3\":{\"value\":\"3\",\"boost\":1.0}}}],\"adjust_pure_negative\":true,\"boost\":1.0}},\"path\":\"spans\",\"ignore_unmapped\":false,\"score_mode\":\"none\",\"boost\":1.0}},{\"nested\":{\"query\":{\"bool\":{\"filter\":[{\"term\":{\"spans.4\":{\"value\":\"4\",\"boost\":1.0}}},{\"term\":{\"spans.5\":{\"value\":\"5\",\"boost\":1.0}}}],\"adjust_pure_negative\":true,\"boost\":1.0}},\"path\":\"spans\",\"ignore_unmapped\":false,\"score_mode\":\"none\",\"boost\":1.0}},{\"nested\":{\"query\":{\"bool\":{\"filter\":[{\"term\":{\"spans.svcname\":{\"value\":\"svcValue\",\"boost\":1.0}}}],\"adjust_pure_negative\":true,\"boost\":1.0}},\"path\":\"spans\",\"ignore_unmapped\":false,\"score_mode\":\"none\",\"boost\":1.0}},{\"nested\":{\"query\":{\"bool\":{\"filter\":[{\"range\":{\"spans.duration\":{\"from\":500000,\"to\":null,\"include_lower\":false,\"include_upper\":true,\"boost\":1.0}}}],\"adjust_pure_negative\":true,\"boost\":1.0}},\"path\":\"spans\",\"ignore_unmapped\":false,\"score_mode\":\"none\",\"boost\":1.0}}],\"adjust_pure_negative\":true,\"boost\":1.0}},\"sort\":[{\"spans.starttime\":{\"order\":\"desc\",\"nested_path\":\"spans\"}}]}") + } + } +} diff --git a/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/stores/readers/es/query/helper/ExpressionTreeBuilder.scala b/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/stores/readers/es/query/helper/ExpressionTreeBuilder.scala new file mode 100644 index 000000000..1789cf4eb --- /dev/null +++ b/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/stores/readers/es/query/helper/ExpressionTreeBuilder.scala @@ -0,0 +1,103 @@ +/* + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expedia.www.haystack.trace.reader.unit.stores.readers.es.query.helper + +import com.expedia.open.tracing.api.ExpressionTree.Operator +import com.expedia.open.tracing.api.{ExpressionTree, Field, Operand} + +object ExpressionTreeBuilder { + + val fieldKey = "svcName" + val fieldValue = "svcValue" + + private val spanLevelTreeFirst = ExpressionTree + .newBuilder() + .setOperator(Operator.AND) + .setIsSpanLevelExpression(true) + .addOperands(Operand.newBuilder().setField(Field.newBuilder().setName("1").setValue("1"))) + .addOperands(Operand.newBuilder().setField(Field.newBuilder().setName("2").setValue("2"))) + .addOperands(Operand.newBuilder().setField(Field.newBuilder().setName("3").setValue("3"))) + .build() + + private val spanLevelTreeSecond = ExpressionTree + .newBuilder() + .setOperator(Operator.AND) + .setIsSpanLevelExpression(true) + .addOperands(Operand.newBuilder().setField(Field.newBuilder().setName("4").setValue("4"))) + .addOperands(Operand.newBuilder().setField(Field.newBuilder().setName("5").setValue("5"))) + .build() + + + val operandLevelExpressionTree: ExpressionTree = ExpressionTree + .newBuilder() + .setOperator(Operator.AND) + .addOperands(Operand.newBuilder().setField(Field.newBuilder().setName(fieldKey).setValue(fieldValue))) + .addOperands(Operand.newBuilder().setField(Field.newBuilder().setName("1").setValue("1"))) + .addOperands(Operand.newBuilder().setField(Field.newBuilder().setName("2").setValue("2"))) + .addOperands(Operand.newBuilder().setField(Field.newBuilder().setName("3").setValue("3"))) + .build() + + val spanLevelExpressionTree: ExpressionTree = ExpressionTree + .newBuilder() + .setOperator(Operator.AND) + .setIsSpanLevelExpression(true) + .addOperands(Operand.newBuilder().setField(Field.newBuilder().setName(fieldKey).setValue(fieldValue))) + .addOperands(Operand.newBuilder().setField(Field.newBuilder().setName("0").setValue("0"))) + .addOperands(Operand.newBuilder().setExpression(spanLevelTreeFirst)) + .addOperands(Operand.newBuilder().setExpression(spanLevelTreeSecond)) + .build() + + val expressionTreeWithDurationFields: ExpressionTree = ExpressionTree + .newBuilder() + .setOperator(Operator.AND) + .setIsSpanLevelExpression(true) + .addOperands(Operand.newBuilder().setField(Field.newBuilder().setName(fieldKey).setValue(fieldValue))) + .addOperands(Operand.newBuilder().setField(Field.newBuilder().setName("duration").setValue("500000").setOperator(Field.Operator.GREATER_THAN))) + .addOperands(Operand.newBuilder().setExpression(spanLevelTreeFirst)) + .addOperands(Operand.newBuilder().setExpression(spanLevelTreeSecond)) + .build() + + val anotherExpressionTreeWithDurationFields: ExpressionTree = ExpressionTree + .newBuilder() + .setOperator(Operator.AND) + .setIsSpanLevelExpression(true) + .addOperands(Operand.newBuilder().setField(Field.newBuilder().setName(fieldKey).setValue(fieldValue))) + .addOperands(Operand.newBuilder().setField(Field.newBuilder().setName("duration").setOperator(Field.Operator.LESS_THAN).setValue("180000000"))) + .addOperands(Operand.newBuilder().setExpression(spanLevelTreeFirst)) + .addOperands(Operand.newBuilder().setExpression(spanLevelTreeSecond)) + .build() + + val oneMoreExpressionTreeWithDurationFields: ExpressionTree = ExpressionTree + .newBuilder() + .setOperator(Operator.AND) + .setIsSpanLevelExpression(true) + .addOperands(Operand.newBuilder().setField(Field.newBuilder().setName(fieldKey).setValue(fieldValue).setOperator(Field.Operator.EQUAL))) + .addOperands(Operand.newBuilder().setField(Field.newBuilder().setName("duration").setOperator(Field.Operator.LESS_THAN).setValue("2000000"))) + .addOperands(Operand.newBuilder().setExpression(spanLevelTreeFirst)) + .addOperands(Operand.newBuilder().setExpression(spanLevelTreeSecond)) + .build() + + val expressionTreeWithGreaterThanOperator: ExpressionTree = ExpressionTree + .newBuilder() + .setOperator(Operator.AND) + .setIsSpanLevelExpression(true) + .addOperands(Operand.newBuilder().setField(Field.newBuilder().setName(fieldKey).setValue(fieldValue).setOperator(Field.Operator.EQUAL))) + .addOperands(Operand.newBuilder().setField(Field.newBuilder().setName("duration").setOperator(Field.Operator.GREATER_THAN).setValue("240000"))) + .addOperands(Operand.newBuilder().setExpression(spanLevelTreeFirst)) + .addOperands(Operand.newBuilder().setExpression(spanLevelTreeSecond)) + .build() +} diff --git a/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/stores/readers/grpc/ReadSpansResponseListenerSpec.scala b/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/stores/readers/grpc/ReadSpansResponseListenerSpec.scala new file mode 100644 index 000000000..569d87b24 --- /dev/null +++ b/traces/reader/src/test/scala/com/expedia/www/haystack/trace/reader/unit/stores/readers/grpc/ReadSpansResponseListenerSpec.scala @@ -0,0 +1,122 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expedia.www.haystack.trace.reader.unit.stores.readers.grpc + +import java.util.concurrent.Future + +import com.codahale.metrics.{Meter, Timer} +import com.expedia.open.tracing.Span +import com.expedia.open.tracing.api.Trace +import com.expedia.open.tracing.backend.{ReadSpansResponse, TraceRecord} +import com.expedia.open.tracing.buffer.SpanBuffer +import com.expedia.www.haystack.trace.commons.packer.NoopPacker +import com.expedia.www.haystack.trace.reader.stores.readers.grpc.ReadSpansResponseListener +import com.expedia.www.haystack.trace.reader.unit.BaseUnitTestSpec +import com.google.protobuf.ByteString +import io.grpc.{Status, StatusException} +import org.easymock.EasyMock + +import scala.collection.JavaConverters._ +import scala.concurrent.Promise + +class ReadSpansResponseListenerSpec extends BaseUnitTestSpec { + val packer = new NoopPacker[SpanBuffer] + + describe("read span response listener for raw traces") { + it("should read the trace-records, de-serialized spans and return the complete trace") { + val mockReadResult = mock[Future[ReadSpansResponse]] + + val promise = mock[Promise[Seq[Trace]]] + val failureMeter = mock[Meter] + val tracesFailures = mock[Meter] + val timer = mock[Timer.Context] + + val span_1 = Span.newBuilder().setTraceId("TRACE_ID1").setSpanId("SPAN_ID_1") + val spanBuffer_1 = packer.apply(SpanBuffer.newBuilder().setTraceId("TRACE_ID1").addChildSpans(span_1).build()) + val traceRecord_1 = TraceRecord.newBuilder() + .setTraceId("TRACE_ID1") + .setTimestamp(System.currentTimeMillis()) + .setSpans(ByteString.copyFrom(spanBuffer_1.packedDataBytes)) + .build() + val span_2 = Span.newBuilder().setTraceId("TRACE_ID1").setSpanId("SPAN_ID_2") + val spanBuffer_2 = packer.apply(SpanBuffer.newBuilder().setTraceId("TRACE_ID1").addChildSpans(span_2).build()) + val traceRecord_2 = TraceRecord.newBuilder() + .setTraceId("TRACE_ID1") + .setTimestamp(System.currentTimeMillis()) + .setSpans(ByteString.copyFrom(spanBuffer_2.packedDataBytes)) + .build() + + val span_3 = Span.newBuilder().setTraceId("TRACE_ID3").setSpanId("SPAN_ID_3") + val spanBuffer_3 = packer.apply(SpanBuffer.newBuilder().setTraceId("TRACE_ID3").addChildSpans(span_3).build()) + val traceRecord_3 = TraceRecord.newBuilder() + .setTraceId("TRACE_ID3") + .setTimestamp(System.currentTimeMillis()) + .setSpans(ByteString.copyFrom(spanBuffer_3.packedDataBytes)) + .build() + + val readSpanResponse = ReadSpansResponse.newBuilder().addAllRecords(List(traceRecord_1, traceRecord_2, traceRecord_3).asJava).build() + val capturedTraces = EasyMock.newCapture[Seq[Trace]]() + val capturedMeter = EasyMock.newCapture[Int]() + expecting { + timer.close() + tracesFailures.mark(EasyMock.capture(capturedMeter)) + mockReadResult.get().andReturn(readSpanResponse) + promise.success(EasyMock.capture(capturedTraces)).andReturn(promise) + } + + whenExecuting(mockReadResult, promise, tracesFailures, failureMeter, timer) { + val listener = new ReadSpansResponseListener(mockReadResult, promise, timer, failureMeter, tracesFailures, 2) + listener.run() + val traceIdSpansMap: Map[String, Set[String]] = capturedTraces.getValue.map(capturedTrace => + capturedTrace.getTraceId -> capturedTrace.getChildSpansList.asScala.map(_.getSpanId).toSet).toMap + + traceIdSpansMap("TRACE_ID1") shouldEqual Set("SPAN_ID_1", "SPAN_ID_2") + traceIdSpansMap("TRACE_ID3") shouldEqual Set("SPAN_ID_3") + + capturedMeter.getValue shouldEqual 0 + } + } + + + + it("should return an exception for empty traceId") { + val mockReadResult = mock[Future[ReadSpansResponse]] + val promise = mock[Promise[Seq[Trace]]] + val failureMeter = mock[Meter] + val tracesFailures = mock[Meter] + val timer = mock[Timer.Context] + val readSpansResponse = ReadSpansResponse.newBuilder().build() + val capturedException = EasyMock.newCapture[StatusException]() + val capturedMeter = EasyMock.newCapture[Int]() + expecting { + timer.close() + failureMeter.mark() + tracesFailures.mark(EasyMock.capture(capturedMeter)) + mockReadResult.get().andReturn(readSpansResponse) + promise.failure(EasyMock.capture(capturedException)).andReturn(promise) + } + + whenExecuting(mockReadResult, promise, failureMeter, tracesFailures, timer) { + val listener = new ReadSpansResponseListener(mockReadResult, promise, timer, failureMeter, tracesFailures, 0) + listener.run() + capturedException.getValue.getStatus.getCode shouldEqual Status.NOT_FOUND.getCode + capturedMeter.getValue shouldEqual 0 + } + } + + } +} diff --git a/trends/.gitignore b/trends/.gitignore new file mode 100644 index 000000000..0071b9e08 --- /dev/null +++ b/trends/.gitignore @@ -0,0 +1,11 @@ +*.log +*.ipr +*.iws +.classpath +.project +target/ +lib/ +logs/ +**/.idea/ +*.iml +*.DS_Store diff --git a/trends/.mvn/wrapper/maven-wrapper.jar b/trends/.mvn/wrapper/maven-wrapper.jar new file mode 100755 index 0000000000000000000000000000000000000000..08ebbb67f088c53eac9a4e2cb019b93f69a1e49c GIT binary patch literal 48336 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 H*Ps3mB7Q<; literal 0 HcmV?d00001 diff --git a/trends/.mvn/wrapper/maven-wrapper.properties b/trends/.mvn/wrapper/maven-wrapper.properties new file mode 100755 index 000000000..a5fcc1192 --- /dev/null +++ b/trends/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.5.2/apache-maven-3.5.2-bin.zip \ No newline at end of file diff --git a/trends/.travis.yml b/trends/.travis.yml new file mode 100644 index 000000000..f86c8adb6 --- /dev/null +++ b/trends/.travis.yml @@ -0,0 +1,32 @@ +sudo: required +dist: trusty + +language: java + +jdk: + - oraclejdk8 + +services: + - docker + +env: + global: + - BRANCH=${TRAVIS_BRANCH} + - TAG=${TRAVIS_TAG} + - SHA=${TRAVIS_COMMIT} + +cache: + directories: + - $HOME/.m2 + +script: + # build, create docker image + # upload to dockerhub only for master(non PR) and tag scenario + - if ([ "$TRAVIS_BRANCH" == "master" ] && [ "$TRAVIS_PULL_REQUEST" == "false" ]) || [ -n "$TRAVIS_TAG" ]; then make release; else make all; fi + +after_success: + - bash <(curl -s https://codecov.io/bash) || echo 'Codecov failed to upload' + +notifications: + email: + - haystack-notifications@expedia.com diff --git a/trends/CONTRIBUTING.md b/trends/CONTRIBUTING.md new file mode 100644 index 000000000..fe9cb7577 --- /dev/null +++ b/trends/CONTRIBUTING.md @@ -0,0 +1,14 @@ +## Bugs +We use Github Issues for our bug reporting. Please make sure the bug isn't already listed before opening a new issue. + +## Development +All work on Haystack happens directly on Github. Core Haystack team members will review opened pull requests. + +## Requests +If you see a feature that you would like to be added, please open an issue in the respective repository or in the general Haystack repo. + +## Contributing to Documentation +To contribute to documentation, you can directly modify the corresponding .md files in the docs directory under the base haystack repository, and submit a pull request. Once your PR is merged, the documentation is automatically built and deployed to https://expediadotcom.github.io/haystack. + +## License +By contributing to Haystack, you agree that your contributions will be licensed under its Apache License. diff --git a/trends/LICENSE b/trends/LICENSE new file mode 100644 index 000000000..9f133f5cd --- /dev/null +++ b/trends/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2017 Expedia, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/trends/Makefile b/trends/Makefile new file mode 100644 index 000000000..8fc31e38d --- /dev/null +++ b/trends/Makefile @@ -0,0 +1,36 @@ +.PHONY: all build_transformer build_aggregator span-timeseries-transformer timeseries-aggregator release + +PWD := $(shell pwd) +MAVEN := ./mvnw + +clean: + ${MAVEN} clean + +build: clean + ${MAVEN} install package + +all: clean build_transformer build_aggregator span-timeseries-transformer timeseries-aggregator + +report-coverage: + ${MAVEN} scoverage:report-only + +build_transformer: + ${MAVEN} package -DfinalName=haystack-span-timeseries-transformer -pl span-timeseries-transformer -am + +span-timeseries-transformer: + $(MAKE) -C span-timeseries-transformer all + +timeseries-aggregator: + $(MAKE) -C timeseries-aggregator all + +build_aggregator: + ${MAVEN} package -DfinalName=haystack-timeseries-aggregator -pl timeseries-aggregator -am + +# build all and release +release: clean build_transformer build_aggregator + cd span-timeseries-transformer && $(MAKE) release + cd timeseries-aggregator && $(MAKE) release + ./.travis/deploy.sh + + + diff --git a/trends/README.md b/trends/README.md new file mode 100644 index 000000000..2676525f0 --- /dev/null +++ b/trends/README.md @@ -0,0 +1,77 @@ +[![Build Status](https://travis-ci.org/ExpediaDotCom/haystack-trends.svg?branch=master)](https://travis-ci.org/ExpediaDotCom/haystack-trends) +[![codecov](https://codecov.io/gh/ExpediaDotCom/haystack-trends/branch/master/graph/badge.svg)](https://codecov.io/gh/ExpediaDotCom/haystack-trends) + +# Haystack Trends + +haystack-trends contains the required modules for trending the spans pushed to haystack. We currently plan to compute three trends for each +combination `service_name` and `operation_name` contained in the spans (refer to the [span schema](https://github.com/ExpediaDotCom/haystack-idl/blob/master/proto/span.proto) for details of the fields in the span ) + +1. success_count `[1min, 5min, 15min, 1hour]` +2. failure_count `[1min, 5min, 15min, 1hour]` +3. duration `[mean, median, std-dev, 99 percentile, 95 percentile]` + +> *Note:* If an error tag is present and has a value of true, then the span will be treated as a failure. In all other scenarios, it will be treated as a success. + +More trends can be computed by adding a [transformer](https://github.com/ExpediaDotCom/haystack-trends/tree/master/span-timeseries-transformer/src/main/scala/com/expedia/www/haystack/trends/transformer) +to create the metric point and adding an [aggregation-rule](https://github.com/ExpediaDotCom/haystack-trends/tree/master/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/aggregation/rules) for it + +## Required Reading + +In order to understand the haystack-trends one must be familiar with the [haystack](https://github.com/ExpediaDotCom/haystack) project. Its written in kafka-streams(http://docs.confluent.io/current/streams/index.html) +and hence some prior knowledge of kafka-streams would be useful. + +## Technical Details +![High Level Block Diagram](documents/diagrams/haystack_trends.png) + + +Haystack trends is a collection of modules which reads spans and pushes aggregated metric points to kafka, each module runs as individual apps and talk to each other via kafka. + +* [span-timeseries-transformer](https://github.com/ExpediaDotCom/haystack-trends/tree/master/span-timeseries-transformer) - this app is responsible +for reading spans, converting them to metric points and pushing raw metric points to kafka partitioned by metric-key + +* [timeseries-aggregator](https://github.com/ExpediaDotCom/haystack-trends/tree/master/timeseries-aggregator) - this app is responsible +for reading metric points, aggregating them based on rules and pushing the aggregated metric points to kafka + +The timeseries metric points are metrictank complient and can be directly consumed by [metrictank](https://github.com/grafana/metrictank), one can write their own serde if they want to push the metrics out in some other timeseries format. + +Sample [MetricPoint](https://github.com/ExpediaDotCom/haystack-trends/blob/master/commons/src/main/scala/com/expedia/www/haystack/trends/commons/entities/MetricPoint.scala) : +```json +{ + "type": "count", + "metric": "duration", + "tags": { + "client": "expweb", + "operationName": "getOffers" + }, + "epochTimeInSeconds": 1492641000, + "value": 420.02 +} +``` + +The raw and aggregated metric points are of the same json schema but are pushed to different kafka topics + +## Building + +#### Prerequisite: + +* Make sure you have Java 1.8 +* Make sure you have docker 1.13 or higher + +#### Build + +You can choose to build the individual subdirectories if you're working on any specific sub-app but in case you are making changes to the contract +such as span or metric point which would effect multiple modules you should run + +``` +make all +``` +This would build all the individual apps and including unit tests, jar + docker image build and run integration tests for haystack-trends. + + +#### Integration Test + +If you are developing and just want to run integration tests +``` +make integration_test + +``` diff --git a/trends/Release.md b/trends/Release.md new file mode 100644 index 000000000..d9ceb9724 --- /dev/null +++ b/trends/Release.md @@ -0,0 +1,10 @@ +#Releasing +Currently we publish the repo to docker hub and nexus central repository. + +#How to release and publish + +* Git tagging: + +```git tag -a 1.x.x -m "Release description..."``` + +Or you can also tag using UI: https://github.com/ExpediaDotCom/haystack-trends/releases \ No newline at end of file diff --git a/trends/deployment/scripts/publish-to-docker-hub.sh b/trends/deployment/scripts/publish-to-docker-hub.sh new file mode 100755 index 000000000..0ff8e3bf4 --- /dev/null +++ b/trends/deployment/scripts/publish-to-docker-hub.sh @@ -0,0 +1,43 @@ +#!/bin/bash + +set -e + +QUALIFIED_DOCKER_IMAGE_NAME=$DOCKER_ORG/$DOCKER_IMAGE_NAME +echo "DOCKER_ORG=$DOCKER_ORG, DOCKER_IMAGE_NAME=$DOCKER_IMAGE_NAME, QUALIFIED_DOCKER_IMAGE_NAME=$QUALIFIED_DOCKER_IMAGE_NAME" +echo "BRANCH=$BRANCH, TAG=$TAG, SHA=$SHA" + +# login +docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD + +# Add tags +if [[ $TAG =~ ([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then + echo "releasing semantic versions" + + unset MAJOR MINOR PATCH + MAJOR="${BASH_REMATCH[1]}" + MINOR="${BASH_REMATCH[2]}" + PATCH="${BASH_REMATCH[3]}" + + # for tag, add MAJOR, MAJOR.MINOR, MAJOR.MINOR.PATCH and latest as tag + # publish image with tags + docker tag $DOCKER_IMAGE_NAME $QUALIFIED_DOCKER_IMAGE_NAME:$MAJOR + docker push $QUALIFIED_DOCKER_IMAGE_NAME:$MAJOR + + docker tag $DOCKER_IMAGE_NAME $QUALIFIED_DOCKER_IMAGE_NAME:$MAJOR.$MINOR + docker push $QUALIFIED_DOCKER_IMAGE_NAME:$MAJOR.$MINOR + + docker tag $DOCKER_IMAGE_NAME $QUALIFIED_DOCKER_IMAGE_NAME:$MAJOR.$MINOR.$PATCH + docker push $QUALIFIED_DOCKER_IMAGE_NAME:$MAJOR.$MINOR.$PATCH + + docker tag $DOCKER_IMAGE_NAME $QUALIFIED_DOCKER_IMAGE_NAME:latest + docker push $QUALIFIED_DOCKER_IMAGE_NAME:latest + +elif [[ "$BRANCH" == "master" ]]; then + echo "releasing master branch" + + # for 'master' branch, add SHA as tags + docker tag $DOCKER_IMAGE_NAME $QUALIFIED_DOCKER_IMAGE_NAME:$SHA + + # publish image with tags + docker push $QUALIFIED_DOCKER_IMAGE_NAME +fi diff --git a/trends/deployment/terraform/main.tf b/trends/deployment/terraform/main.tf new file mode 100644 index 000000000..4469e4333 --- /dev/null +++ b/trends/deployment/terraform/main.tf @@ -0,0 +1,74 @@ +locals { + external_metric_tank_enabled = "${var.metrictank["external_hostname"] != "" && var.metrictank["external_kafka_broker_hostname"] != ""? "true" : "false"}" +} + +//metrictank for haystack-apps +module "metrictank" { + source = "metrictank" + replicas = "${var.metrictank["instances"]}" + cassandra_address = "${var.cassandra_hostname}:${var.cassandra_port}" + tag_support = "${var.metrictank["tag_support"]}" + kafka_address = "${var.kafka_hostname}:${var.kafka_port}" + namespace = "${var.app_namespace}" + graphite_address = "${var.graphite_hostname}:${var.graphite_port}" + enabled = "${local.external_metric_tank_enabled == "true" ? "false" : "true" }" + memory_limit = "${var.metrictank["memory_limit"]}" + memory_request = "${var.metrictank["memory_request"]}" + cpu_limit = "${var.metrictank["cpu_limit"]}" + cpu_request = "${var.metrictank["cpu_request"]}" + node_selecter_label = "${var.node_selector_label}" + kubectl_executable_name = "${var.kubectl_executable_name}" + kubectl_context_name = "${var.kubectl_context_name}" + env_vars = "${var.metrictank["environment_overrides"]}" + +} +module "span-timeseries-transformer" { + source = "span-timeseries-transformer" + image = "expediadotcom/haystack-span-timeseries-transformer:${var.trends["version"]}" + replicas = "${var.trends["span_timeseries_transformer_instances"]}" + namespace = "${var.app_namespace}" + kafka_endpoint = "${var.kafka_hostname}:${var.kafka_port}" + graphite_hostname = "${var.graphite_hostname}" + graphite_port = "${var.graphite_port}" + graphite_enabled = "${var.graphite_enabled}" + node_selecter_label = "${var.node_selector_label}" + enabled = "${var.trends["enabled"]}" + kubectl_executable_name = "${var.kubectl_executable_name}" + kubectl_context_name = "${var.kubectl_context_name}" + cpu_limit = "${var.trends["span_timeseries_transformer_cpu_limit"]}" + cpu_request = "${var.trends["span_timeseries_transformer_cpu_request"]}" + memory_limit = "${var.trends["span_timeseries_transformer_memory_limit"]}" + memory_request = "${var.trends["span_timeseries_transformer_memory_request"]}" + jvm_memory_limit = "${var.trends["span_timeseries_transformer_jvm_memory_limit"]}" + env_vars = "${var.trends["span_timeseries_transformer_environment_overrides"]}" + kafka_num_stream_threads = "${var.trends["span_timeseries_transformer_kafka_num_stream_threads"]}" + metricpoint_encoder_type = "${var.trends["metricpoint_encoder_type"]}" +} +module "timeseries-aggregator" { + source = "timeseries-aggregator" + image = "expediadotcom/haystack-timeseries-aggregator:${var.trends["version"]}" + replicas = "${var.trends["timeseries_aggregator_instances"]}" + namespace = "${var.app_namespace}" + kafka_endpoint = "${var.kafka_hostname}:${var.kafka_port}" + graphite_hostname = "${var.graphite_hostname}" + graphite_port = "${var.graphite_port}" + graphite_enabled = "${var.graphite_enabled}" + enable_external_kafka_producer = "${local.external_metric_tank_enabled}" + enable_metrics_sink = "${var.trends["timeseries_aggregator_enable_metrics_sink"]}" + external_kafka_producer_endpoint = "${var.metrictank["external_kafka_broker_hostname"]}:${var.metrictank["external_kafka_broker_port"]}" + node_selecter_label = "${var.node_selector_label}" + enabled = "${var.trends["enabled"]}" + kubectl_executable_name = "${var.kubectl_executable_name}" + kubectl_context_name = "${var.kubectl_context_name}" + cpu_limit = "${var.trends["timeseries_aggregator_cpu_limit"]}" + cpu_request = "${var.trends["timeseries_aggregator_cpu_request"]}" + memory_limit = "${var.trends["timeseries_aggregator_memory_limit"]}" + memory_request = "${var.trends["timeseries_aggregator_memory_request"]}" + jvm_memory_limit = "${var.trends["timeseries_aggregator_jvm_memory_limit"]}" + env_vars = "${var.trends["timeseries_aggregator_environment_overrides"]}" + metricpoint_encoder_type = "${var.trends["metricpoint_encoder_type"]}" + histogram_max_value = "${var.trends["timeseries_aggregator_histogram_max_value"]}" + histogram_precision = "${var.trends["timeseries_aggregator_histogram_precision"]}" + histogram_value_unit = "${var.trends["timeseries_aggregator_histogram_value_unit"]}" + additionalTags = "${var.trends["timeseries_aggregator_additional_tags"]}" +} diff --git a/trends/deployment/terraform/metrictank/main.tf b/trends/deployment/terraform/metrictank/main.tf new file mode 100644 index 000000000..18dcf9301 --- /dev/null +++ b/trends/deployment/terraform/metrictank/main.tf @@ -0,0 +1,48 @@ +locals { + app_name = "metrictank" + service_port = 6060 + container_port = 6060 + deployment_yaml_file_path = "${path.module}/templates/deployment_yaml.tpl" + image = "grafana/metrictank:0.10.1" + count = "${var.enabled == "true" ? 1:0}" + +} + +data "template_file" "deployment_yaml" { + template = "${file("${local.deployment_yaml_file_path}")}" + vars { + app_name = "${local.app_name}" + namespace = "${var.namespace}" + gra = "${var.graphite_address}" + graphite_address = "${var.graphite_address}" + node_selecter_label = "${var.node_selecter_label}" + kafka_address = "${var.kafka_address}" + cassandra_address = "${var.cassandra_address}" + tag_support = "${var.tag_support}" + replicas = "${var.replicas}" + image = "${local.image}" + memory_limit = "${var.memory_limit}" + memory_request = "${var.memory_request}" + cpu_limit = "${var.cpu_limit}" + cpu_request = "${var.cpu_request}" + service_port = "${local.service_port}" + container_port = "${local.container_port}" + env_vars= "${indent(9,"${var.env_vars}")}" + } +} + + +resource "null_resource" "kubectl_apply" { + triggers { + template = "${data.template_file.deployment_yaml.rendered}" + } + provisioner "local-exec" { + command = "echo '${data.template_file.deployment_yaml.rendered}' | ${var.kubectl_executable_name} apply -f - --context ${var.kubectl_context_name}" + } + + provisioner "local-exec" { + command = "echo '${data.template_file.deployment_yaml.rendered}' | ${var.kubectl_executable_name} delete -f - --context ${var.kubectl_context_name}" + when = "destroy" + } + count = "${local.count}" +} diff --git a/trends/deployment/terraform/metrictank/outputs.tf b/trends/deployment/terraform/metrictank/outputs.tf new file mode 100644 index 000000000..396c57294 --- /dev/null +++ b/trends/deployment/terraform/metrictank/outputs.tf @@ -0,0 +1,7 @@ +output "metrictank_hostname" { + value = "${local.app_name}" +} + +output "metrictank_port" { + value = "${local.service_port}" +} \ No newline at end of file diff --git a/trends/deployment/terraform/metrictank/templates/deployment_yaml.tpl b/trends/deployment/terraform/metrictank/templates/deployment_yaml.tpl new file mode 100644 index 000000000..02e3628a4 --- /dev/null +++ b/trends/deployment/terraform/metrictank/templates/deployment_yaml.tpl @@ -0,0 +1,66 @@ +# ------------------- Deployment ------------------- # + +kind: Deployment +apiVersion: apps/v1beta2 +metadata: + labels: + k8s-app: ${app_name} + name: ${app_name} + namespace: ${namespace} +spec: + replicas: ${replicas} + revisionHistoryLimit: 10 + selector: + matchLabels: + k8s-app: ${app_name} + template: + metadata: + labels: + k8s-app: ${app_name} + spec: + containers: + - name: ${app_name} + image: ${image} + resources: + limits: + cpu: ${cpu_limit} + memory: ${memory_limit}Mi + requests: + cpu: ${cpu_request} + memory: ${memory_request}Mi + env: + - name: "MT_HTTP_MULTI_TENANT" + value: "false" + - name: "MT_CARBON_IN_ENABLED" + value: "false" + - name: "MT_KAFKA_MDM_IN_ENABLED" + value: "true" + - name: "MT_CASSANDRA_ADDRS" + value: "${cassandra_address}" + - name: "MT_KAFKA_MDM_IN_BROKERS" + value: "${kafka_address}" + - name: "MT_CASSANDRA_IDX_HOSTS" + value: "${cassandra_address}" + - name: "MT_STATS_ADDR" + value: "${graphite_address}" + - name: "MT_MEMORY_IDX_TAG_SUPPORT" + value: "${tag_support}" + ${env_vars} + nodeSelector: + ${node_selecter_label} + +# ------------------- Service ------------------- # +--- +apiVersion: v1 +kind: Service +metadata: + labels: + k8s-app: ${app_name} + name: ${app_name} + namespace: ${namespace} +spec: + ports: + - port: ${service_port} + targetPort: ${container_port} + selector: + k8s-app: ${app_name} diff --git a/trends/deployment/terraform/metrictank/variables.tf b/trends/deployment/terraform/metrictank/variables.tf new file mode 100644 index 000000000..464c4fe62 --- /dev/null +++ b/trends/deployment/terraform/metrictank/variables.tf @@ -0,0 +1,21 @@ +variable "namespace" {} +variable "replicas" {} +variable "cassandra_address" {} +variable "tag_support" {} +variable "kafka_address" {} +variable "graphite_address" {} +variable "kubectl_executable_name" {} +variable "kubectl_context_name" {} +variable "node_selecter_label"{} +variable "memory_request"{} +variable "memory_limit"{} +variable "cpu_request"{} +variable "cpu_limit"{} +variable "env_vars" {} + +variable "termination_grace_period" { + default = 30 +} +variable "enabled" {} + + diff --git a/trends/deployment/terraform/outputs.tf b/trends/deployment/terraform/outputs.tf new file mode 100644 index 000000000..56eb82148 --- /dev/null +++ b/trends/deployment/terraform/outputs.tf @@ -0,0 +1,7 @@ +output "metrictank_hostname" { + value = "${local.external_metric_tank_enabled == "true" ? var.metrictank["external_hostname"] : module.metrictank.metrictank_hostname}" +} + +output "metrictank_port" { + value = "${local.external_metric_tank_enabled == "true" ? var.metrictank["external_port"] : module.metrictank.metrictank_port}" +} \ No newline at end of file diff --git a/trends/deployment/terraform/span-timeseries-transformer/main.tf b/trends/deployment/terraform/span-timeseries-transformer/main.tf new file mode 100644 index 000000000..470917940 --- /dev/null +++ b/trends/deployment/terraform/span-timeseries-transformer/main.tf @@ -0,0 +1,75 @@ +locals { + app_name = "span-timeseries-transformer" + config_file_path = "${path.module}/templates/span-timeseries-transformer_conf.tpl" + deployment_yaml_file_path = "${path.module}/templates/deployment_yaml.tpl" + count = "${var.enabled?1:0}" + checksum = "${sha1("${data.template_file.config_data.rendered}")}" + configmap_name = "transformer-${local.checksum}" +} + +resource "kubernetes_config_map" "haystack-config" { + metadata { + name = "${local.configmap_name}" + namespace = "${var.namespace}" + } + data { + "span-timeseries-transformer.conf" = "${data.template_file.config_data.rendered}" + } + count = "${local.count}" + +} +data "template_file" "config_data" { + template = "${file("${local.config_file_path}")}" + + vars { + kafka_endpoint = "${var.kafka_endpoint}" + metricpoint_encoder_type = "${var.metricpoint_encoder_type}" + kafka_num_stream_threads = "${var.kafka_num_stream_threads}" + } +} + + +//using kubectl to craete deployment construct since its not natively support by the kubernetes provider +data "template_file" "deployment_yaml" { + template = "${file("${local.deployment_yaml_file_path}")}" + vars { + app_name = "${local.app_name}" + namespace = "${var.namespace}" + graphite_port = "${var.graphite_port}" + graphite_host = "${var.graphite_hostname}" + graphite_enabled = "${var.graphite_enabled}" + node_selecter_label = "${var.node_selecter_label}" + image = "${var.image}" + replicas = "${var.replicas}" + memory_limit = "${var.memory_limit}" + memory_request = "${var.memory_request}" + jvm_memory_limit = "${var.jvm_memory_limit}" + cpu_limit = "${var.cpu_limit}" + cpu_request = "${var.cpu_request}" + configmap_name = "${local.configmap_name}" + env_vars= "${indent(9,"${var.env_vars}")}" + } +} + +resource "null_resource" "kubectl_apply" { + + triggers { + template = "${data.template_file.deployment_yaml.rendered}" + } + + provisioner "local-exec" { + command = "echo '${data.template_file.deployment_yaml.rendered}' | ${var.kubectl_executable_name} apply -f - --context ${var.kubectl_context_name}" + } + + count = "${local.count}" +} + + +resource "null_resource" "kubectl_destroy" { + + provisioner "local-exec" { + command = "echo '${data.template_file.deployment_yaml.rendered}' | ${var.kubectl_executable_name} delete -f - --context ${var.kubectl_context_name}" + when = "destroy" + } + count = "${local.count}" +} diff --git a/trends/deployment/terraform/span-timeseries-transformer/outputs.tf b/trends/deployment/terraform/span-timeseries-transformer/outputs.tf new file mode 100644 index 000000000..e69de29bb diff --git a/trends/deployment/terraform/span-timeseries-transformer/templates/deployment_yaml.tpl b/trends/deployment/terraform/span-timeseries-transformer/templates/deployment_yaml.tpl new file mode 100644 index 000000000..a516ec2e7 --- /dev/null +++ b/trends/deployment/terraform/span-timeseries-transformer/templates/deployment_yaml.tpl @@ -0,0 +1,64 @@ +# ------------------- Deployment ------------------- # + +kind: Deployment +apiVersion: apps/v1beta2 +metadata: + labels: + k8s-app: ${app_name} + name: ${app_name} + namespace: ${namespace} +spec: + replicas: ${replicas} + revisionHistoryLimit: 10 + selector: + matchLabels: + k8s-app: ${app_name} + template: + metadata: + labels: + k8s-app: ${app_name} + spec: + containers: + - name: ${app_name} + image: ${image} + volumeMounts: + # Create on-disk volume to store exec logs + - mountPath: /config + name: config-volume + resources: + limits: + cpu: ${cpu_limit} + memory: ${memory_limit}Mi + requests: + cpu: ${cpu_request} + memory: ${memory_request}Mi + env: + - name: "HAYSTACK_OVERRIDES_CONFIG_PATH" + value: "/config/span-timeseries-transformer.conf" + - name: "HAYSTACK_GRAPHITE_HOST" + value: "${graphite_host}" + - name: "HAYSTACK_GRAPHITE_PORT" + value: "${graphite_port}" + - name: "HAYSTACK_GRAPHITE_ENABLED" + value: "${graphite_enabled}" + - name: "JAVA_XMS" + value: "${jvm_memory_limit}m" + - name: "JAVA_XMX" + value: "${jvm_memory_limit}m" + ${env_vars} + livenessProbe: + exec: + command: + - grep + - "true" + - /app/isHealthy + initialDelaySeconds: 30 + periodSeconds: 5 + failureThreshold: 6 + nodeSelector: + ${node_selecter_label} + volumes: + - name: config-volume + configMap: + name: ${configmap_name} + diff --git a/trends/deployment/terraform/span-timeseries-transformer/templates/span-timeseries-transformer_conf.tpl b/trends/deployment/terraform/span-timeseries-transformer/templates/span-timeseries-transformer_conf.tpl new file mode 100644 index 000000000..1d4560398 --- /dev/null +++ b/trends/deployment/terraform/span-timeseries-transformer/templates/span-timeseries-transformer_conf.tpl @@ -0,0 +1,31 @@ +health.status.path = "/app/isHealthy" + +kafka { + close.timeout.ms = 30000 + + streams { + application.id = "span-timeseries-transformer-v2" + bootstrap.servers = "${kafka_endpoint}" + num.stream.threads = "${kafka_num_stream_threads}" + commit.interval.ms = 3000 + auto.offset.reset = latest + timestamp.extractor = "com.expedia.www.haystack.commons.kstreams.SpanTimestampExtractor" + } + + producer { + topic = "metric-data-points" + } + + consumer { + topic = "proto-spans" + } +} + +// there are three types of encoders that are used on service and operation names: +// 1) periodreplacement: replaces all periods with 3 underscores +// 2) base64: base64 encodes the full name with a padding of _ +// 3) noop: does not perform any encoding +metricpoint.encoder.type = "${metricpoint_encoder_type}" +enable.metricpoint.service.level.generation = false + +blacklist.services = [] diff --git a/trends/deployment/terraform/span-timeseries-transformer/variables.tf b/trends/deployment/terraform/span-timeseries-transformer/variables.tf new file mode 100644 index 000000000..bd7f9b608 --- /dev/null +++ b/trends/deployment/terraform/span-timeseries-transformer/variables.tf @@ -0,0 +1,26 @@ +variable "image" {} +variable "replicas" {} +variable "namespace" {} +variable "kafka_endpoint" {} +variable "graphite_hostname" {} +variable "graphite_port" {} +variable "graphite_enabled" {} + +variable "enabled" {} + +variable "kubectl_executable_name" {} +variable "kubectl_context_name" {} +variable "node_selecter_label"{} +variable "memory_limit"{} +variable "memory_request"{} +variable "metricpoint_encoder_type" {} +variable "jvm_memory_limit"{} +variable "cpu_limit"{} +variable "cpu_request"{} +variable "env_vars" {} + +variable "kafka_num_stream_threads" {} + +variable "termination_grace_period" { + default = 30 +} diff --git a/trends/deployment/terraform/timeseries-aggregator/main.tf b/trends/deployment/terraform/timeseries-aggregator/main.tf new file mode 100644 index 000000000..3f177a7f8 --- /dev/null +++ b/trends/deployment/terraform/timeseries-aggregator/main.tf @@ -0,0 +1,84 @@ +locals { + app_name = "timeseries-aggregator" + config_file_path = "${path.module}/templates/timeseries-aggregator_conf.tpl" + deployment_yaml_file_path = "${path.module}/templates/deployment_yaml.tpl" + count = "${var.enabled?1:0}" + checksum = "${sha1("${data.template_file.config_data.rendered}")}" + configmap_name = "aggregator-${local.checksum}" +} + +data "template_file" "config_data" { + template = "${file("${local.config_file_path}")}" + + vars { + kafka_endpoint = "${var.kafka_endpoint}" + enable_external_kafka_producer = "${var.enable_external_kafka_producer}" + enable_metrics_sink = "${var.enable_metrics_sink?true:false}" + external_kafka_producer_endpoint = "${var.external_kafka_producer_endpoint}" + metricpoint_encoder_type = "${var.metricpoint_encoder_type}" + histogram_max_value = "${var.histogram_max_value}" + histogram_precision = "${var.histogram_precision}" + histogram_value_unit = "${var.histogram_value_unit}" + additionalTags = "${var.additionalTags}" + } +} + + +resource "kubernetes_config_map" "haystack-config" { + metadata { + name = "${local.configmap_name}" + namespace = "${var.namespace}" + } + data { + "timeseries-aggregator.conf" ="${data.template_file.config_data.rendered}" + } + count = "${local.count}" + +} + +//using kubectl to craete deployment construct since its not natively support by the kubernetes provider +data "template_file" "deployment_yaml" { + template = "${file("${local.deployment_yaml_file_path}")}" + vars { + app_name = "${local.app_name}" + namespace = "${var.namespace}" + graphite_port = "${var.graphite_port}" + graphite_host = "${var.graphite_hostname}" + graphite_enabled = "${var.graphite_enabled}" + config = "${data.template_file.config_data.rendered}" + node_selecter_label = "${var.node_selecter_label}" + image = "${var.image}" + replicas = "${var.replicas}" + memory_limit = "${var.memory_limit}" + memory_request = "${var.memory_request}" + jvm_memory_limit = "${var.jvm_memory_limit}" + cpu_limit = "${var.cpu_limit}" + cpu_request = "${var.cpu_request}" + configmap_name = "${local.configmap_name}" + env_vars= "${indent(9,"${var.env_vars}")}" + } +} + + +resource "null_resource" "kubectl_apply" { + + triggers { + template = "${data.template_file.deployment_yaml.rendered}" + } + + provisioner "local-exec" { + command = "echo '${data.template_file.deployment_yaml.rendered}' | ${var.kubectl_executable_name} apply -f - --context ${var.kubectl_context_name}" + } + + count = "${local.count}" +} + + +resource "null_resource" "kubectl_destroy" { + + provisioner "local-exec" { + command = "echo '${data.template_file.deployment_yaml.rendered}' | ${var.kubectl_executable_name} delete -f - --context ${var.kubectl_context_name}" + when = "destroy" + } + count = "${local.count}" +} diff --git a/trends/deployment/terraform/timeseries-aggregator/outputs.tf b/trends/deployment/terraform/timeseries-aggregator/outputs.tf new file mode 100644 index 000000000..e69de29bb diff --git a/trends/deployment/terraform/timeseries-aggregator/templates/deployment_yaml.tpl b/trends/deployment/terraform/timeseries-aggregator/templates/deployment_yaml.tpl new file mode 100644 index 000000000..8dfca7645 --- /dev/null +++ b/trends/deployment/terraform/timeseries-aggregator/templates/deployment_yaml.tpl @@ -0,0 +1,64 @@ +# ------------------- Deployment ------------------- # + +kind: Deployment +apiVersion: apps/v1beta2 +metadata: + labels: + k8s-app: ${app_name} + name: ${app_name} + namespace: ${namespace} +spec: + replicas: ${replicas} + revisionHistoryLimit: 10 + selector: + matchLabels: + k8s-app: ${app_name} + template: + metadata: + labels: + k8s-app: ${app_name} + spec: + containers: + - name: ${app_name} + image: ${image} + volumeMounts: + # Create on-disk volume to store exec logs + - mountPath: /config + name: config-volume + resources: + limits: + cpu: ${cpu_limit} + memory: ${memory_limit}Mi + requests: + cpu: ${cpu_request} + memory: ${memory_request}Mi + env: + - name: "HAYSTACK_OVERRIDES_CONFIG_PATH" + value: "/config/timeseries-aggregator.conf" + - name: "HAYSTACK_GRAPHITE_HOST" + value: "${graphite_host}" + - name: "HAYSTACK_GRAPHITE_PORT" + value: "${graphite_port}" + - name: "HAYSTACK_GRAPHITE_ENABLED" + value: "${graphite_enabled}" + - name: "JAVA_XMS" + value: "${jvm_memory_limit}m" + - name: "JAVA_XMX" + value: "${jvm_memory_limit}m" + ${env_vars} + livenessProbe: + exec: + command: + - grep + - "true" + - /app/isHealthy + initialDelaySeconds: 30 + periodSeconds: 5 + failureThreshold: 6 + nodeSelector: + ${node_selecter_label} + volumes: + - name: config-volume + configMap: + name: ${configmap_name} + diff --git a/trends/deployment/terraform/timeseries-aggregator/templates/timeseries-aggregator_conf.tpl b/trends/deployment/terraform/timeseries-aggregator/templates/timeseries-aggregator_conf.tpl new file mode 100644 index 000000000..e53e40860 --- /dev/null +++ b/trends/deployment/terraform/timeseries-aggregator/templates/timeseries-aggregator_conf.tpl @@ -0,0 +1,73 @@ +health.status.path = "/app/isHealthy" + +kafka { + close.timeout.ms = 30000 + + streams { + application.id = "timeseries-aggregator-v2" + bootstrap.servers = "${kafka_endpoint}" + num.stream.threads = 2 + commit.interval.ms = 5000 + auto.offset.reset = latest + timestamp.extractor = "com.expedia.www.haystack.commons.kstreams.MetricDataTimestampExtractor" + consumer.heartbeat.interval.ms = 30000 + consumer.session.timeout.ms = 100000 + consumer.max.partition.fetch.bytes = 262144 + } + + // For producing data to external kafka: set enable.external.kafka.produce to true and uncomment the props. + // For producing to same kafka: set enable.external.kafka.produce to false and comment the props. + producer { + topics : [ + { + topic: "metrics" + serdeClassName : "com.expedia.www.haystack.commons.kstreams.serde.metricdata.MetricDataSerde" + enabled: ${enable_metrics_sink} + }, + { + topic: "mdm" + serdeClassName : "com.expedia.www.haystack.commons.kstreams.serde.metricdata.MetricTankSerde" + enabled: true + } + ] + enable.external.kafka.produce = ${enable_external_kafka_producer} + external.kafka.topic = "mdm" + props { + bootstrap.servers = "${external_kafka_producer_endpoint}" + retries = 50 + batch.size = 65536 + linger.ms = 250 + } + } + + consumer { + topic = "metric-data-points" + } +} + +state.store { + enable.logging = true + logging.delay.seconds = 60 + + // It is capacity for the trends to be kept in memory before flushing it to state store + cache.size = 3000 + changelog.topic { + cleanup.policy = "compact,delete" + retention.ms = 14400000 // 4Hrs + } +} + + +// there are three types of encoders that are used on service and operation names: +// 1) periodreplacement: replaces all periods with 3 underscores +// 2) base64: base64 encodes the full name with a padding of _ +// 3) noop: does not perform any encoding +metricpoint.encoder.type = "${metricpoint_encoder_type}" + +histogram { + max.value = "${histogram_max_value}" + precision = "${histogram_precision}" + value.unit = "${histogram_value_unit}" // can be micros / millis / seconds +} + +additionalTags = "${additionalTags}" diff --git a/trends/deployment/terraform/timeseries-aggregator/variables.tf b/trends/deployment/terraform/timeseries-aggregator/variables.tf new file mode 100644 index 000000000..e32f1833d --- /dev/null +++ b/trends/deployment/terraform/timeseries-aggregator/variables.tf @@ -0,0 +1,36 @@ +variable "image" {} +variable "replicas" {} +variable "namespace" {} +variable "kafka_endpoint" {} +variable "graphite_hostname" {} +variable "graphite_port" {} +variable "graphite_enabled" {} + +variable "enabled" {} +variable "enable_external_kafka_producer" {} +variable "enable_metrics_sink" { + default = true +} +variable "external_kafka_producer_endpoint" {} +variable "metricpoint_encoder_type" {} +variable "env_vars" {} + +variable "kubectl_executable_name" {} +variable "kubectl_context_name" {} +variable "node_selecter_label" {} +variable "memory_request" {} +variable "memory_limit" {} +variable "jvm_memory_limit"{} + +variable "cpu_request" {} +variable "cpu_limit" {} + +variable "histogram_max_value" {} +variable "histogram_precision" {} +variable "histogram_value_unit" {} + +variable "additionalTags" {} + +variable "termination_grace_period" { + default = 30 +} diff --git a/trends/deployment/terraform/variables.tf b/trends/deployment/terraform/variables.tf new file mode 100644 index 000000000..9939f4cfd --- /dev/null +++ b/trends/deployment/terraform/variables.tf @@ -0,0 +1,24 @@ + +variable "kafka_hostname" {} +variable "kafka_port" {} +variable "cassandra_hostname" {} +variable "cassandra_port" {} +variable "graphite_hostname" {} +variable "graphite_port" {} +variable "graphite_enabled" {} +variable "kubectl_context_name" {} +variable "kubectl_executable_name" {} +variable "app_namespace" {} +variable "node_selector_label"{} + +# trends config +variable "trends" { + type = "map" +} + + +#metrictank +variable "metrictank" { + type = "map" +} + diff --git a/trends/documents/diagrams/haystack_trends.png b/trends/documents/diagrams/haystack_trends.png new file mode 100644 index 0000000000000000000000000000000000000000..af8f578a1c2c2ddf39edd8356771bf36c73ebaa0 GIT binary patch literal 44638 zcmdSBbyQW~*FLI?on2QnA!`8;mnb$*r?4K6A;Pc~YCNk(hO2BB zJju@a->(H;km>O+Oe~DdO#i(%xRn3#EU%cot%DQH*%{nlkc0o9mj91)|MQ%GwO6*V zcd-Yf;ACkcW9I^M0vEd&J@!tJ_1{vnUg?s4pE- z81fYIiVC~l_~i@#)hMEnPc!csl9K}&UOj9r z=5yX$;&b<7v)|y_%=A_ycXWie3K7=Up5@VT6-F-c=8*bWr|NKXLq5GwtjbKG!ULGLjKjGJ=&1pj~6Ru~&{dp2!&vRl#C|8RfN zMsMY*@}E(F54szxT9;Pc_x=pyP&<`0&Fhn25^I z?<&vd{%VueWlK50;-GQ2d}S|D*RFw=Pph1ir{%}0DwKyfgm_@gF1!#utTx|@CNq3;vVe_ftOzZia zb)N1wyXtOy>t4%(3@R85X7r$G=u6pj*xJNyzoZw*vw?ecvFtz4^!MhlZNPIggL=2L zCer5nN)Xmg0?y~Mk^Aj}EJjT|M+s;L5)2ieohH7p)PK+Ccfd>2f(*CV1Wg0&^F?>V ztD_E7v5ca(1^vA-ehBmFOz_^5>YB)|KYq*|j(!a$bYF0E>c%tgwJIs2ie9iB z&;R6gzxtoG36`jE8JhARmq^CvOqf*i@1G_Td2MB{Z|1-=uQ4=k<-q923zV4NDj3av zKxEXKDp1N|r3`(XHN#o(Ho9hKX`Hq?`5gM#x^XwFU zUE3+CNZc)NQoO%hjruanjs#{IRfUxntld}I77w^5e%IRtd2b68Pr3>0d)bEhO}fN* z?H0aAGuDoXieNrYf?OCjgQ$V4knFoWu-HY|uKVdK)aKff47_AUY~IkuM-uV%e~l7+ zbF=?#S#M}ON$BDBZRIARfR_um(yMKg>Sp@$Wc!x0ME;8fXOU+EJdOV~A;3(?iM79# zf^o=LgumDuvb?P-)vRTsAUIjX8$ z3RsYab9L6##UcszZFg?viCMnqv(Y^xLLctTRJYo!9K<;5Q_{bN{l}KQgK$@|Q}DaO ztD*_Vg=~$uN-GIonFh$uIbE$M1^f8nb7kHf){e>2Jr@m)$VZq{iqk05`P9eN5sZq; z85YfFgUc49Yv020dfsV(p=6IFbrQVQ+Y#wZmoXU;Is~Kp;~o+^LbFBJY2JOPdCo%y) z#-=}_3UbS{tELS!uz#(6#$)y&X3nP{fjV7Tn~Q(>KgM5L;zua#4SnV|L{0o%QC{LJ z9U-$ow%Wl`8#5XA!=)DEpYaSn62{hj;Z}OjSjOi9XIBR+yNA$g+DYFdg=tbn@t=fz zq4Zt(gwsxMzsI%xR{id4)*zL6vTs@4R4lL_9E0LqwxvX2oEB+#f4G)BObR;$_qg@{ z@jSo*1<0dW#LJK}twM51zr^wW8ohmLdGnbEbzYLH>YYh)5i?;#n|uhl;%uBTXOtXM zi;I)y)O^BO6as&i-?gO!W&Bj(FIH5}Y@|>VxzI2&DvE4Mg|xl6sh~}se|fWbPB`bb z6p=~Yk&JZJd;h&MBQdEIXIYUhD-ow6PEQFO|M{NSwo_bUdf0N281U?6{|f>e zw4kn0wLfYC228D)HXrwEn>{ktymYQ=n@w9T-5!tsAOGH0bnI^Av&fO?$nGOR2dq#E zmY%`Pf7T3*G(0qI5T{o>UCZo4Y?91>m(ag=e>^>$;A?~Gq$Q<=|6KAo(+gz3J6XSZ zcv(M>6dtS3{h`#;WcUN}Tbjfkj`1Hb!Ojjh+f0z5#rb#gWR>r6`q0FF18acTDiiuV zs$vwlL;6nhD6Arv1noQdX3<|@OEGWn0Gm-!>*LBt>~kd2aMX@iLbq{4^i=9gFFG#s zJK=8gtxi^x!sd6h(C8vZC)#%_n_3SLeqb}l3Y17vIIJ}s;+1l9!Or92o}}LJ@g2~2 zU!*L^^7HZ~nTpy3YvOr0fIM)m9!su|V2Kg_`r?p8F(gD5N8cr8!Jz}$X*(~eKPQaw zjS~N{_zaihpH8&IIJB?gs${aNR42g74gmX0*#7=}Z}x5L%|X+NRUko^`}Xy21&L_b zuXLWbc8%LqctUZ~=dt~Rs8q*X;fV%QVou^5X(xISm`wtIpCiVb7Q6DZ7LdfiUWLzfw#>@5LlQ=<@eRWx>58lE(p}g-_Q? zoLE1PD?>gbRbf6BVfkBNJfvl2+|#Q=juY4RK88CVm@C zvv1`QSIa?xIr<10wFe5D2BQ$YQAp!Va9r)~OJ*@yOE!r_sL$xEShn~YA&Qx#@0K#s z7DFy23)+b`Y-afjT2LLf-q?wBSULg7(T`*3Lpqek^^IQXaXAE{S*VI`?iB1m)WV*; z=(jW1W{@}df`TTjJCiFD$E*Ubzem_5d{G^m9~;VV0f!!x9)r!G6KG^PfSN)p#7S>^ zR@y5?;!8mvxTgnTm>6frAIp=KV3MFc4}#LK0vUq7UY=O&rHJ&E!dLYbj}XO@^f&(h zo}w>#;<}+ktH6w@Ee#+teiu$ARUACeOETbHQ%d7x$VK|(4#>1 zoJ}+;ncn%Nk9xqsd!G_*px$;q2zIgPj#?kKnc<$v3?Jc3UZ1kq=vs0jkJ~C4goK9y zS--zIB&p4`5aXFb6}p95?2^XsnP?GKk$O_0)Y&caMvshsl?o2!oiSF8TscwK(58&P z_-D!$;XMSubHlktJ|QJ?rG29^@_EPS*7Ox>KuLLHu?;}HOsfW_qbbX?$ani%+0-dhvm5$(GpGl9z zZPVevSqkIDA4e<%jH7^KF0@b>~Y$whSyNI8Lc;@@c?-%>D1fM z4$ed(qI*`Bn1(C_HLTvy=}I-p2-vVCq$lF6Wf5 zi**U^s^2F+yL%{d2skCmdX;g8ECr21mlo75-YK@GFA?pzP!APmv7)~^{oa)QhNS-zdc{_6C zdw+KJOUfgZ33L$K?LuZ$Ev7?-E(YKktfv_7>3{Ym52gVg#mVWyt*eG{V2O9Jp1H>=Z7coW6~4{fi?n}_MV<& z>p43?=+a}CjSV56H+z&8h!lH9kIsCgplR-_>BA*HLUf0QOQc_zN^(w3Qiigq4;e$; z3%Ae;cl_A@NLvNQzBY_cdEGtB!-mWOY_BZ3r3Q+()>5sdC z8VXED;}is011hYiEq$LOwem9y&3WOF1BKO5PbpFYmA%prUNkDx2H$~oA|^dZ(I-#4 zqa7){M6NtpLo@41@=A&9&wQjXF=_OzsJxx7=J?5~=do+aTmAlz6ts!vmQHQ9^L}y+ z%i{~sA7`ckdKh`Z>xXR*C=t_ZI-B{LOP@)9_&xv&B0HY1(BWz|mMb&z&dK&x$cV;M zrxZX<`YF2I&4dd`C&K$OUD-`iWXDkZo`J4u+F>_VrlrUPnT=UD7*nf<;O?<)iJVnE ztW+0IZuMp+DtBGN`}P|v{P)%cR0~?t1pm7;O(<(Z!o}^i)ceY`bD`Rstcj@-R_DTp zrE7GefCW+|pJufy?|&ex+$4AjIssqcp!niBPI{NDSJ1ef+hB2GqksL%zw_vok&uSQ^-4jDpxv1`O3ZT(`W)78-^KObOknHf4t4 zv0C51edjd>#j2C=s0TRZw5hVSCT$P*-bL;4Nt?FQA8*pUc|3)jZ3S?}3=DV=)fmu| zy-Q0Hk-g5wM1&1(B+O0v>*4a3I$!+>V_E~wAO;w7 z#n!uXxuFMDzgtJMnTORVp+2TIVX_A`)+>$^&50*h~vqOa5+yK`-Sv`McXhn^Fal6!0#6XOpWMAddw)^Y4R)0CH zalK5gi&OA&c!I5$(ACV%)j`1pZVkpvj)uLOZ|e{>uv;%m9yk2h1rHn^**nuQ_6SMb zQJSi#{YzGpZX{0tGfn4eEl)8IXo@f?V`S0NMn~IN_iPV7-`(G|$#VOn4h*I&51+ui%~?vtE310++JAspUqYuIjZzz3<#i~PvBkJ2!|5nrv^t} zTh%&GdUN~8FsT97Ud4%@Hn@4qkw(|=Hos5ieK0F0v&k#+*3#{ZB>SjwDpd_}qqcpp zbj{Ve9~)Vy#vWi!Vn7%~n=I|JGhxXH_trk+>{$Z&kCw}cw@TZE?SHP(8w&52ai&l! zubz#HduxYZSY7qq$o7%{?yO$34@iBc+}}CYn`=o$n$Wx_1=u1@1IGaN%uVX;nuo*c zc}AFQkX58((X=>eX}^Kz-(%19sd@Z?&vf;%m`eO&jV{}&f`eLvYZ!3tE!auiRi^FO zohM-K;#1hnU+BfIue$oS_@Vu76u92{S`A|N;-O>X04>&SWaNCzf$N4Be(^zK!etG5 zz(Yd`;KS8o@w3I{JQrOx`>{0B>Ba0XRhPVXxp?l>s1s&?r)p>{4_EKkli?T~75E(Y z9XfSe9`*{}?2$i4l_N-E8$)Yf;vra@R&uwiRLC9jr%PVDWew6c`L52|D6xZqb)q<# z$j9E}<(k=9Nf*oLg(v_uO_HwN5Cy!3_mDXnV>S*IP=C1J>~}WtPjcH=edD1~hW<`< zoOtn``s)D>&(&I7N4P~uUds=;{Vv;0xmOLQMG-E$Qw;C4(>P;Yhxrcq>oREl;{vYmL#ScJJ4o^Slt1Mn21s%;Kjsb-v)~$v&;E)pXEk zeMNtIe|Ph8e#-5-i{0^$xaWF;=2~S33Z2jdx)fn{ePu--px9u$W#1BiW$+CkGBtnU z?Ebn|J-`?TI6#J4R{#kIsM2i6((D?GkO(;GCpJ@woCjHcB4eF7)Lm_u-G)9V_o(SQ8>rt6ri3 z9@G?{Wj&Mfm86Meyc{Q3x~ZOrx0Y)a!cOx@FTeH^@@OTE4(%{Dz@@jKA59jq^9Ufl z&WxS14k7d@?5!D=^n;%66C#!g@iIvqv`U%Y5Q#Tlyl%Kl+(HW1G7kDt_l_l2-C|Qq z+n=t9WS^g*tkkdms+P-jXTrfysD$h25wVki$PgMI{R>3rFaDf^h%ERf)B7PtiWq_7 z5QHP~=9!+42$&263{@`sbLzZqdo+N5lsZ}oxoQV|DPDiN_d&yojzTiC$fB|@2wc{S ztlO(vfe#QSW{8Rxj3D4rd_q4H(z88I*^ETv<1nbrXAkm0Z zjS2XAY2W4tHm>$WMUjvd?7BtJ4_U4*C6g;FLDbIaW%-($2xP^&P6-3vp#6USw2D0f ztc~AwA0>Y6FtPds_R17|wo($*ejrfHI#JAIJmo%T-bTbqcW7# zTT+~7Q?c>fH=*>fHH}6FefB{ZA@Kd|iuj8UYSq8$9E*71UJ`q}OSY))!q$ba{rzt=tGn2v+)HPznPQGFO&IJC zB8-((f#Xh0-g*Dw-^=LxtJ8-5Wtp!tQ38*^`zBVjUcW*60hX$q&_t&CHx0Qy+Jbb9 z+qO(VPL7pmR(#R_gk%rQ{Pn|_Q1YugVm>#UEySDhrUPCO>4w+ZmiTwiewa#ts9G3r zE8XIv8R9%5RXn%|YmFgm9=N>F_vlIAyD=j0=GX%JsOU<+aK5JwmG_WY+ovTH@TU2h z&ggWhLMF#ZUVSe~=6yDm&Ft&G=v=YIY`mt{9FL}Mz?kKSKXgg#71tX|9k!R>%CBjM zZt26+Ow)eycU-*nMrSQ1So~bgmNIt?esB~wX$PLTgEWb@<2QtDzo7?E`s>1EZJHGq zNO)y*f>)=;Df6;$_Ze^bMCBJ8y z6%z7M*o$ZSlBGS8_x?Ehi1)=znmse1X=dMz+ytDWKQ&ODEV{Lo9tbkD<>ue_@$f%C zz+%|bB&OiK(#XgSYd$US_2orhe>Q2QsQD+fx(p^4vz~-m$1@I-P}%&Q!=yX9#n)(| z+x#)K@$2F5=GT`TG45Ke_mb=sdHJ$PZFh=lbQD%%)F zl+)_d^6#QUnUN5SxCrE$7*i$~;#pG`qFg;0ze+`+oX7VNd)wf?_-3+SJFW=FB5L9Z zxI?S8JngXx!!~B!hc6#dUQvwf;gQdXcFojB<$AN=M`-xsxo;c3e&LXy40JwIaG-cnLrDJ#*;G zGH3@tU8aTjvAxPXM%<(He=Z3EMp!1or_kF&mb-_zmigt2jc|!{hcX4cgQA}95!<(3 znazR$TmuF?77XD_r_WPyq&B7qEisC`+qsns-WA2?=7^Z@d(C?ZNiwB~JxymmM)eZ8 z&4~DcF#erpL-zeB?i+Je)Z-o9S_|~ZPf|}GH)JoMdNL)h@f|>xG3dQj$gk1VXI2_b z%+@0BMq;z^MC8OeF>eoi!B#LL6G^e6*cJP`Sk$B8lq7mWk6K+ID{!S5oG~s<4j=n` zG-g;>G;S?!pQ z4)XIYGL)k$EQ63d`%7{X>VSe|Yfe7nEsDF1=oW9mOyco7K$vs81fkf?-#X^ccp9@X7138D4uZw);T(p~zHSxVNy@Ooy& zH?t(v@iyeqvVl05xfnl##-%CeXJch|v~)|;gy;U`#`>^lQE~7@P7b-I4viR>NA2(P zz`2ILM~Q8Cq@D61xSpYLcvB2Q<8IZo?rWoD1zQx@X35L0-+qN3V9(N-jZ2mf{5dbk zri^UTj|w_?TOKuh^j7edAk=X^j;Xyf6EH5Da_+1G61ig z=J!WubJtnamRf=p)3v$CN|3V2(Wb)~HL}u6Lief(?0m)Rs>yn?XsS~ct9cGCdk zUAWVV6mESCf|tufE(%1@d$|}syx<*8Da9fk<-r`wR}eq9BYbnRW)Wka`I_hAHsna( zs{!OY#XV64DOjEJrFDqe#=fbXT>Vm0*&N;RxQ*Q69@LmtkxOBvB}-V%nyl~MI)Cko z%5D%0O^Vv%qrD5TQlVDRAme3DUCEp>=vtb-)2NCWXp}0wk9==Ssp5o2bvh#@a(DJ4 zY7MZ-t`UH?ZIqmucZatMcoJux{63B<;%qt0zo%+doUw^)%$OC=e27C_QRuDd#78ps zh5S}FS7I@prTZHRwz`TY1JiKD&O1@_>@>nDuGGSh&hGpj!t}IJ1LWr{vps7z0@(S^ z>UHKY0R~(6BZfYChd^mQ?5a0!)spGe=wt0ks=7XuWJz^AMgS`n0T;lTzJ7YkFG{$2 z5@#JQ`?ntC6-032W|Ek z^&5MOy)d&J+7XQS_N50=%vsTX&4#vj4BD=NB=#l@zi!s<4)TfZt^<5c5ztUeJX->69f2yC!BF%Wj#|T*|3W)X8$P^h6EE zXprT|2qrAe#xyK#Vt_sAEEIM2S7!k(-%t4wOv?xS#v)U6BE!(1`-Z9<9ZW=RPyn$MOMO)`+p zl|V$NdX-U%=`v=z`6~u_15k}^iQ@3GvK0d3^8;f7M%CKOj8EW0u8>72=vV8e4G2O= z=cYhLlWlDUm&n$1PMPz#!!bAV>tAdy@+FVe2+~Fr#fjsmeIMA)zW+OylJ#2_;x0;J zNke@LBMT<+^ANG1U)@~YjTB}9+p#*iJU>fLg{;z7il}(%c`QVx#>fgTQ{Iofzo#4h zMz)tqu;5}*s)NRlU{(;WKfrh67vBja#z0oqGWZj^^ql`g1`jhU;^JE9D7wgG<&cpR z>eXB7d4rh#xwNhk!5}glALpiAJ8&lJ4N!*!iZ(jvxK$aj{31QvgW(ZNf>|cc2%(gonCFzS(ye zTnbVnlP^pm>ZOgNZ!v-_lHT4jcVFn8bveo$+q!uFhoqOp?!xb`qDB zNbBRj^^=&jYuY!RxBZ4psiS$#{DugJaW6u~XC?QnS(6Fk#<|hjS_C?Rmiho{$M)7$n^KDCW@j6IKES-p9 zAZ9Sf9?#5Sd#8)aPp!=97WSQdMsALkDLt|=X5u>9z66YKDcVu?QR(lK_DJD7=$EhF zIdP^PA8cU?fuVvkdK?!pKIL`4CeO{&juXx#^`dI{>UO=OmA&Y@2}zBC1c;P>l-L?XVh(lOOL)$D8H!GkR-di`I1Zoal!3(2UaE;MnQ zTP*pu1s`6hHmybPSh8uk0r{>$=Um#H3j_}nME}aGPA$3~B@zYbkP7eF6Q+GFOV3-2 zKKhLIz4r3AnEmH=HpMLLm=VX-uVSk!&*2(&8OaFj=~@3Ms+dp2q(RRl819BKT%VCU zO)u^|C?RY+ud@H?vqi@%qZj<1F@aYQrdU0KyK)i2TN>HB);T?~xlu%BZsc2X@-x`k zI8=(~eiy7KVBC}~Af~4=yPee|<>eRO6w!MNR%m@R53=#w1Ye39;gR6dTG=NOlcY08 zwIm30stBP@qhQazyMyHW?Ht|SkmPLfuJNA~&gC)S@QB%u>V>YIsx-JxW6gS5PjXu= z=~x%L{jDip{U}wpaVuEfQ;_wYfGl-MiY*>V>b|(kICPkH-x_d{G9(EMb(Yh+8`uH; z<>*(^ocLcB&Ifg!;dijXq`8Jwd^>*;fZM>2dYRuuPj zz*p7ot7(kU->TIdJD=jfn9AtOoGFK2PJfP7{1$}FsE&WeQRm$kQxZo6n~Vs;&AJrf z!Kqs!gnwsl_(U0pz{;`vX}TBzP7k$-)2-~!zM8A;>blP&It@EW!eGx~o!%!B!tFRp z;Ld4~j3soxlxpI+pM~Jt5M}wi(K#&3{aY=UGZH1fLCQ|PPv~6Ro2a{W zkL~g!OESQg7;6xH%bqTM<3`4yyR~;h07wGO;4aj#J(?>2W@~QBG0Mj-)toBEWSI3R z@?zd8#E?$U6D#dvJS9&+!wnfcTIpB^4kMbx|5oMbX=vM|(ETO(r9szA>(&9lAuw{w zGrfN5ew+Ub(Ck0=(GOw(VOxdPKNgY~Ga$-CeON%QtoxX{!WOA8RD&?{C!lu5%CINM zz>a_RM5SOJrZs+BU@!Rea#C>Ibb*bG&FuFxxB`vu!IzA5bQvIf+ z;>&sMEV0Lft5uy}WAD=t;YZ%jGMB^<_z2_wtopvNXPs|IZG!_>1)s)lnB4HAlyUBf zA%tRPHJ!vb)KH=Pnde67ayi!YP@s847`bxzW9Fy3WM@-t0&-+moxFT)P{O>HF4JuP z!*Z0&Ib=^OW#WBl;~GjpG5c2ZYjVzLsa~XC9i-8n6J{i9K{Ntra%AUPrTYtK%ohv{ zK>gKVUyGu{?u1&!eoLAVEm!_*`lHXdUbhV8VqY-o4Db7{G#B{J(7a{%f0B!9v~ZUA zE|AYL_`T#hEaaSMsT4&N@I~kY`~pL-7Ik<95lh=MZ|8+E5gco+-%nvU;ZY|#mgVU; z-SS}`#*iy>z|C6BWN2wbAgmYBbY^^$8M< z0O?y^;e&|Z&H~V+Tr|1z&;*9)x2JZza9C))k{t(~)o?DYE08r*IPuP(3?r#Knq%-d zg5q`BmU+G9qbF=KZI_}TMtXw+M1^;^HIVX;vDJIkSc?dh7d_uXReqH^_!cxykiwy1 zjcbX%(H;pAF@+}P7nxc#D+m1yD2ZlIbR4RR@#)Q*ZK@`o*6OwoE*>QO^M7qABsfh& za;LsLvhgJ>ZO`?s+AMEHz$i-C5x=QE6x>$kzf@+AYwjV&x>!Z>y*rzDdaZ+Xx~dy3 zxTCKZ8E`u*&#l4a(%--~#&D#gQi)hiNu`lEqN=mDeajLLMiXFNe|DK}89F6)7(V26#6U8}A@mWpdpEp!KE)PX$OqpBO+DNJE&#%;=~NVB@jA>_0;Wvbyy zDxFR-#{uOa!=p~CZ$tYvaI*!Ocr^ZX5+l1fSa1oJA^1BKllO*+8phnVsn?meT75aO zTp4lbf!~))>M=ZTLoEo9bFRBHT`Fdr@qnuW94ebkxWyM z_9Po)jHwO76^_A}rMswdDpe)iaVk`$d;{jx`_raj;&niAExe=#G^|w2^v_%$MNz3G z$xah-O);X%o>w%PUX-c9ZS%=KIp-3Wp*wsmaG!QI>7{$~kCB2QWN%bp3@L|bZ^>10 zSx~*~tJ7!ZpTRPMcS<-!FEVPDr(&tB(bNc2$$3A&8KB23(>$AR3GO{YAC2Zwc`yHm zPJT+GPGV&(fa1&OVmVgUcXh52tD%$UK@X+aL8v!l&Hmy&N->NjQ zt$bPdQvAg(Cs2)M=HPcBiyQcnrXnm|IKD~1O3ND0uKj17!>Rd34eR;6&m zd+O!oS4;8aQ1{6T;EpAJvyu#2T^x}_V zri&D#E*g3w)WfwARQ+1d z-t1DtX`5=3Y|xM;+kB(*H;pF3X{{-gHlMN2ciSyRXD z7<4}>*7ihkt5zQiEgHr4-xm z4PcJhW`W-QG2M;oM{m;P-0HPoH~r$me3E0KzN|66P!jqVB-t|Gn^3KnSpITPVg`Md zyZm!g^pB1)S_v`{q~$~$-M2knFkp^f z>jKiR?rxP$Iz`L*T$BqM7$b!l9_cs4qvqm&>6e8M& zwe#ire4f9bM!0=ZojnE#9ZN7m?jO8s4cVQ30-Z71wcmt_R9}|AmCJ|MJV34uY7=fQ zfC$oU-j-_?2rk8FMnpco=yM(xpk`|MtLiya+@FMVk`OBlO&jH>PNZ~{yOiO53>l&y zAQOpN7^25O02m|ad)*5V<1-ZsUhGapBa2&2dNtx2t|L3G2T+UVUbl!RL;mBe$cWDLC6| zkHZBw`~8ozvG&5nhpdc$ruX?rVO#1$;Vr2AqwKvBOP*Kfn?sDnHZIjN$Y_hn+dugb zEiw_p@{0mHWCMuMDT9fUlB)Mu*{FTFap*6Sc~_?7fT9p`s#GvFny3a7V}UgRK-lKtzP}~s>UB$2!_gJ zM)d8gpWy?bY=W*M2s!Ri5^2-B=6kV74RWI9xb?vf-IVhu$BnHA0PX(u2FU=pr^M&d ztFxKyw)123sV6x+l0V7=@_6i)(n?;!Ta{EjswZI`$XxM&XwvOSlYCI`O-KI*@`eKG zU7#L=Feos~YlpVs?tK0+EP&)P`F%zcfvf;Bt-TtIHDc9ZlAe=HHc?R7qZsw%4PjKa zJ$tS^*&LuYJ_ccykyBu)ewNSR<%k{tx8t{9jsi>Kq;<}@3!kzX2J5L|&3zQ2$^*%z zsX_{?ME#6$*yRCQUx<q$WXmqa%1fDX6Z1^b(6b9!OAeAeO4% zD`=htqyxQirJ)`Gx^Q?Vg`OLODMpvv22n;Hd*mtl5oNG8zq@{p91D!m*JC)f3DH~R zk=$fRN?&C@IZ)*_u9ks3s!<4W*#^kL69+w5?gCR$GNJ}FXU}bh`A_8odJjMil2aF! zW?;5QZ4Odsng9(J8|`pqgWNV$S^xVgH^wNnK7Y?NQ2-M9NOqQU?G_O6Hd_kFV71pEF<7g|+FljUo%ntV7Ir=CzQp6H3jeyYt4*pL-dWB2BwUMi7sI$O z_lbd`1(TvcT^34_FNJ-Zc0TIGj?T#)hludZoHq7!|n>Cj(ccprBV>Sh0u2?}o>#lyAWLABC zt+2jp&pFlix+h`v7soqp)ct4%ZIVtpc3imczghUCdnxAlR8M9!k&GFGKCkHA5)uaN zph-`!YHB_Tswu;ttabGw1-?MWLu6`SYPm?Vt+xcFP|c-lPSv+Zt4!5HQxUM6$Oq|p zPBJTuybcXYB!d?PGO$8Y=G zSnEHk9uQ{uFeWLzV`kHcBNB2iPtR6SGy`+YGr7PGLTWrNb zqq5DIn{b{gs8c%`tG~VV5xAKZwl#vI+JH~lHoZ1%-P2{Ncx!Lc-&9{gw{9XYp>RmU zK!1sMAL;h#^4P10=8M)}@xCQ7@+rJgitlLRvium|Hq}T>xDLG9@qtitvE z5RCZLhl&)ix+~QdXag3UZze=Un6|?YG6Liam=Ic&L}2OP3to<( zI$a1X?+*Mmr_6{UX~5F7p$GVLtl2XqcE}$#n1AY%>#qWOm{Q-UrtmJElQA7oYp4W>UV=j-KO z{IU2R`+)(`^w~Z(;<<-BZ>7__ef)vOmU`D&FW{3ub}s`d5Z23$fAALx1PPZJ%6&MT z%cV%qCek+$j|7(@juV3V=@d>}E(kmBd6uiAYM&qkEf8Z&22U^K_n$zp3THmNc)d@} zG;K*T7|T^~n-BAw^lB>#`s;qRdeOG_ZTUe(VM}PvjGR&@LaaYM_>)Uw?THShe$IZ< zwqQEPgHmvqYhq(0nwYqDyuZ4Zxsclz`ninC&gq9ypZ%QM6KnE}r_0heI1Ft&=M`;< zMGWbI>lZ?5o6xS@6q)ZW#*Y#9!d(`jBcn<~@HV1J6eu7%gg}&On)! zZ8!HWpjM2EG>M+`S-S7B+jmZS#!sJ~Oe0CHUAl>DUF>RUO^j5-%zB8n~3qlrer!f}Zr z%wjF-^_Y~buUwxApKU0_NZe*0)vl-BH=F$0jFl^$~$$MBmo#qbmZWt4Mxm~GG7 z{+j3><6X(y-Wt~sirw7$FJI+h+h{u(vp9dbEC#$h# zs_Zy@j!d{UCL3vz<9=R@0(nT{Sk$H5%v1!fTB7P9NN6BMriW&PcN!MEDH`?c`PK`y z4p&z4*GH_)@)n`X$dLxF5u}92$uYAVTY98wyf#MIkQ9v2nmaCQ%QTC8zbI!(TQ(yQ z?-|BH*GEAd+2x-vNZ~0oFYsDv_bZ7XlGK;vM>s66IX;{>1-5~5OMufY48BNYPXeCj z14IN$DjF?C9+#MJ`9{IS(o)qqg(BG!{)fT6B>lFg&*Hgm{|LD+W${bQ@U5l{0$219 z6632x9X}meJnOJKdYhUVPRn$yx%wD+^02>ApN`x$gl4gOYA9W%{8=TTek6_+6iJtU zP4=vI%`cv(tVg{QRh>4z2+sr3!YNylqQ??L)5j|5L5CC7Na5%39-~JmD-t&Fq5M;% zqG;meiP56uagu}#b-u?qyq?GVmyIo3eKoGYMPEtl7_dHoI#h&Q9Av0t1lO!-TmvMx zMVUtvQ&&lj)_$gP8T%w0H#SM{n{CHf2<6p3b*gS2O3ooqEJkCjxzR%U5o~qssX3KXkYPNV(b!%$X zId(Ez&u?(T*dPV<6LK`H{-Q3`pF?r+a_AJBfFpvc``GZ0P`Jb4hlD$vliSzL-jTu? zP-YN>h0mW+yLBsjib%b)gWrQaBNd$qLc>E3Wbf{SUe$xO9nrQ`Cg04sA4j58rpO<4 z=HWMYP9DXNQOhbx@hI;6l3^rGSan*=Sa+HqSr(_t)EbR5{LNO5)_ONF_GMx&=n+`% z2P)E(ZV_j&`Q)3LKK(HH+Ghk5eyBIT$$a50dF^6QOidp*4Mlon1W?kT1AWIt6oSS? z*^6FeTVp2|%ievuE$Y;^o|m^qD#>k$STzBLXl>E^umx*xW{Vn=8ijWY>h`kA+4$pW zXjqFwUsy%YK$YDMecfH|@!8IKPt^WygUEaD4>vE2`(D1t#o*G;V>DZ~yr8+DdO-ar zzl#Jor<9eoJi>r*^xr;ZQ9A$mUjPz;TKLqiK)xR%(%;>eYa6HIDc2|V$8|RH@Y^{< z)0NiKp}bzy4NmHtt&f81Du zx+{Z%UU~EBrZ?!v5RNqp>f30A75_x<*c(jIP}&#Vvv)et)aw~;@H^4^RXJsdSLctNj$L=EG6u<)O?#tBMdV=fiK5&ow5|8oySf^m*q7YF zART`=KCNa=?7mPbD0cf}d+l|A{+oCu^RqSm)ejoifOjT_mWZ8WlEiz0jIQCgw{oZf z^7_XC;h?6iN$57bL>Nl&Z2e7YpM~ZvC6?j0{&UtE+p&I#iH;vXQ|m9DIeP0`txUH$ z%M~;r$yeJMJOZKBT{sy0N-+2e2g5zK5DijPPqT6VgO}LkiDo9-olZcu#{4wv(w~~- zQ1>v7JlY=eUEQZuKL&0mO3-0=^Z! zh;}(x;Bib$u?5vs8kwNLc<|NqTUvSSCGSIlZ4e~aNe$*dpi%9;Nh$oGPs!*8G>+6) zpr{H4riFf6d*!jj_-OaDl7b!+x(H&Do&_`Fwph^|K{q1Mg#-#qgx&J;hH(9pCll6( zR)T`@fxp}19ZsMyUdGodefN(~tgUgTa*`=mQ5`$e(>}lWF;3hscXfI3zZQol(R-hR zy30(&*Dc5Z@mM&(S0Zz`^E~oyBLweLaf z5U%Go3lpI`0{~uw`(=nkg+a>)4f_}g?3l;$b()X!5s&5iZ%Rrr=4SkE_BZ=KrZOBgNCkVQ zYT3yuNRg>pn_GdY@{C!0&3+iwy$~r((dq1^VN8e@A&|UqyqA|-k5AyEIo+>xw zv4qiSD?9iFmkdC4@j&w&{3{DY_3adSpe7A*xB1d+Fqsq-Ui04H^%kQn`D2&j(sc~hXF1L@xs9M?9F^ss1&J8_Sv`S%AurIvr{`4 z;}j;%p_>(u9n~hU1fggDqe2#`s;Cd7f@yMWUtzvL7^TWQe%@F6$DUj~3M_eC2 znF-LRsP~N4^$C286;HLj0YlM7Vou94zY8@rdVDXGnjMd=BZ~m zSEa$!Tn}0+E&=N{2xKB z(%s$N-5sI`f^@fZmvnbX3rL4&kI!??@BQ?C;>+-#nLWGKTK9b|_Tnnz`Bo5Ri+yPV zGT9d#gZ6?FQ}O8GB#Eig?M?3tMSMPqcwc^5k^P?JDzqpzIfle+s___Sd1og9JXkSV zCg3*}u3j+B8U&Xu;^rW&K6Jq&a%jp*2-poMFlrD6_DQ{tFcnxfAEoYBTHNgR0U%(a zu_mq=^<3u6! zst1nzeArh|00+Q3yfnDUengGn{Nw%Sr}S_~09??l6UZ2%U&RdyE%NSzS&Uy|CX=nT zirQ8Ia*$4SNv^FR+sbb)L_s^~WLWR0@z}8Cr7eP(u7x16KR>4jlu#QT>4wq7{2a?} z^IPJiqfA>MNrTOGyzO9T5EkW{dC$NiIV3I-i~9uf1Sl=wUcsLMuorAo{>FwJ3D$UJ z{M~=dhknl6fvJ(dOyahTDI9$x^LZ?iv0T%)QDJfYhlYmVFwsRyq9n-F%Idj_SV({R zjVTHTFU#4Pz|Cs14KkR?aOiyGts`gS5&C$yKynIyO!h=bKQ8|ln!4%1cH?l^Fo{9Q zGabzgp|GJn(SP9qa;RYuf{%BXD4D9df2c1ud!C6$6(Yy{Iv-{wlQ|-?YfaFm^AJL0 z@9}TXe=eU3kOFaeQ5AEqVNFv5Y-oGXYNB~;7L&6~CV~pI5a#f7Rt`oupjZ+9qss2U z`fBAo;SJ-7p{(!B!WR0JF3VMcA@#D#J;$W+AIQWXg z;)?($meu&wq+|zo>Q|4I$5KYIXK*GBHKp*xrHOZmon-hPXoiU$RCTOPL& zqUEC;xSIL_`p5yH39_c%3gpL24Hyy|!2XSv*;ml?BAAojyY#ZXYfXZUG&2eH2T^$QfM5u51Z~atcU9`eId;7@6@SaT&#wFJnF-$? zDgIQ|n$CfS`jtq`_bpFVgJD!Ws#f6u$RQHXI6wrCS^Yeri<+|dakzGT{w~l(Hz=2D zSTsm4Lkpq5(-yA=WvUq@b^e?G*)NjSZmDO@RgE>Un_8y3b|B^?kY$v(Ex9AI8=pfV z3$R{&3D^QRb4%k;ei4V{`l3|@T0ZJlZ$^v{@J{#F99XYBKF$!-daT%IA_ZagD2#HD zyS)ob>ea)mCZbE(*2!>h*`^C|Ou_6^SOQ;{x}>*d)LgG{vxh^ipfwv@-f@Ir-GB#a z;-N=5IHSJN1{5ohkzkzI)n$CH+HqN67t+Iu&OVj=b z?Aso03}2|>V~lOY3pn;eWcZ-&kIMWN<0Qo)2^OpmVMrW}f&7nurgp$`!MXYf@~)k0 zWMe;nj<8iRo}`B0d2vWm+iIrci;1Lf2T5El27h4MM>(Qf(??8qy0H#>I$WF%^6jN* zB3auze2J<5im0k~b9LF4ziNazcQq4ygK2Eo`R9gkJmB8WCQl^dJ7V8^h1$OhQtoc) z>JrQL4`Q27Ab$dplqn9;4R$t^2)l5moe{+)xa#zYy1)Nk6ty2Vh!5kP-wwRko$SpT za>F$~XUSe!UXMVYA89sTH?~bmdnLNez&PHI$2q(eSlkabW1{gA$DF?<&k1E1@#$jc zpHrpM&Mm~InYASMA{q1yotzwjvR-cb7It4tw=m3k$2 z(u49Rn4pU%;s=`pZ7kd+g+yp@{%GoW0*|MVpK_2&6nyeucOusQ+;DRJ$A`N+@Dbc!@L!n8fspG0%VE{ zNux>A(O%_K@!MwZhhKiLPR@6sN*akQ`p8lz?Z1aN&hT(h?eB2Dppo^3uc{QyQ?KC{ zWZ+L!X}&4 zfD17Z_4zbQ+R%bQOCf)G;T6lh)^N^ffX@q=2W{WTse7^Cd8kIp~e1So7+ zhUc4wn{c#J=Hs4GA?1IGv8i8>LVQHZ==Q}A^w^5wMXnhdb%x7Y1I3rm+3J%S;C@*z z6vvhq-+1Ft*4@ZxXihXI{UE}JOEy*8FPwdI@zx}1p;Rc~*$rPeX-4%QJk_BKYUWz1 zd<{iwpU}|Qg7T26Xe-?^YmSiLqAV0CxU}5I1gWKaOTa4W@)vsH3lgtHD~5kR%a~-n z8l%L-DDm@xBmhCOgEZH|0)!j3D=~Wu&Brxdx#JRj)W@BM9)rmOXRX`KHabl=^&}NM z`){R5H1u*7R%sR5#cTl&>o?Zco1rLz&J_r&#<{}g@h5^!E7CGZV2-KD)6=B5WZ!Y` z$kRwS3*lsM7VYS($^hysp=(SOVTrTbbkqvO0P0%GVC>DlU`}CEa+@SNW%O-(u$Q1+ z+f6!Cp^Xm3MwxQaoBzS=dOg`tWz;|92eS9^(z=dMbhIEcZ6F3^K#E}cdxe%1S#xQs zV2gapvd7wFv*ZN1-%%n(lS5v5yNLcYH`Vh_8Pl_zcJyrp*u&1cLUWkk-oqa$kBMpR z$&*mj`B>23d(g8zgjBr-b(9CJ0ZhWEHQ%hShawhtNxo-)NmBj(K1!3#n?Ive+|l!L znKh^^Glb3wTP;ac9yNDK~meS!F5R)_*PFo*(DqC?D@g5RNy5rR{`Z zmwm}=l&}<(1Y#19%saia*>S#MW7U=wG#qtJc=SJ{NoOpTT ziz5+C5w+R_P&D$HNK`}MjJ0W4c}MO9029c6OSm0#%3|~*N5QM`4HpT zApjwQXZVNJY7_aTmjiTr9c-_=FKJUUJm)W>D7LTx z6E+r+p9)23?U5Lb?=2D!Mwr-4Tr|H46Xih|s(XFQ_jVg*$W_QBC+rp+cw|RK_{acV zv46f_G{yC(VDx-kUG+9=k%U_#aZWu|ucT^GG$OInPecA(M9V$j&sgk3s#YTY^!(q5 zL@B*jZ-?%ht)i@(2QNH{DZRpljXzJ<&p0diX3%h4EERHWeWASzG-vwp>-_RP<(ltx zg(dvoN#V3X4tdtk=vis0Der8{S@XG_U0viidV5gt@Lmhm9&Pk`dJ_>Zq(!~ZwyB7i z1EzyO(aC7oQ&{t>X4hjvz2#utQUxeDYx>9C!FLHvN-}omF*fd+wv2qVDlTZgoo(Dd zUK}C8U9Yge?uzMa8>qhqYi^tGZ}lW{{6P^MzMRz=FM%HwkvN4`3&~5&Ey|9-O&o$q zVVn_Hry%|&mA8~p2*cw^jEN#;KBRdY0j9FnjAh(Vn4_bGVQ_av^VZ)}L;AN*tUu)^ zUc;Xu&Lg|yYExPMB5Y9!GVS(Jb&ds zQc^Oz{Olk);AE$a;69dTIbb&gw?RrvaT+o|%JYOmve9(qhOEKlV!y#PzXAlwE)1OgV2{|F`O_$rRv8csZ%Sp6PeQ?(?WN0s2EtPr8kXLPatC3idCy7d zv`nfFPFy_eXdwg2o}y};f3&RhT$pUaOOv{3awy7W1~;HNmINWvX;SEUL_#j zRy0#WpQ^a{Snxq7;aG4a(O3Grl*5OgBJCwu7n*GE)j7*5t#wwFL(4wrGm_+_Ayp`) zzNc-afB(TFi`aerW66d&9k6aS>x=YRPxB2HL=2JI?u_o)(~OdljhY=0O&l2gi15NC zonZMI0Vcy|o|^Ow8MO>i_(ZJn@B0q7YbNpx`_h@k(uwM-ENWAjLyP^qgz~rzRq!dx zh=lqhZkpu3n|w`2ZK=FwJw4WQE4GvNYqn!` z%m#rf$~Q-XfzhJWzxrp1>#D4A$AZcE(fr4qFBX1-v_!%$Ly_$o4fH3n&oWWXft z1MSVS;C8%(_%w+(+-CiAk(5E7{H`Oi0}*hq>qIXv=UzSAx@)FVIYMNshPyS`_;H*_ z1n<6DjqlKqP`g}Pe)Z5~Ik4ma9|;3v&2%Upt$r@eWAhd5INL`lL)S0p8}r)_#QY*l z)9VzDra0TQBgW5B>|yd0$QQ5+CS=a+h*7 zwk}H+yGFJEfw0VB=~9FJcDb}-3-{i^efB)Nj%IG}u!mzV+|%oM%(Jn;LvaNZvmPu; z6Be?*I82WRq=c96G54F&;1i4W1d@-Y-zXCnDqiwY9-wGH391->=Mlhu%`-CIjV5%2 zBA{kCm|@*^IlB>DIb%FI_3P)CAQ55Rx^-{DAio~(XD1+trHDkj+)Sd>Z1^1`i4(EZ zpi#%lbs`cgiUyJ4%;}Jt%TAj6G>EKGV#%n+8HbF<(-Ha_MaIMR-`-*j^S)D&kLO18Z#rWO``rKQy3_e-o>N`d{GFF=7rHE`na=DBx$AyI z2)n6?xz-0E2?!jtYgPm>Yerds?J3I(x)M8z~e8b zOEWP^)`c80-)~q+qXodu{QVM5F9Xpvv6V~Y{qE2N>B;+&bC>}I4ZT08Iv_YT1;UNk zAEI2jd>M2jdMK|k>(5TpmeXc=QSdx~C#>+8rCI25m~4dCw1vfb;zA9b?t9s+{Re+G zL+wsU7GX`ceDeASz*Z#{=QrGwHzrwbh42!qp>pQ0!eL& z;OE$oTt=V@LJvL%5K+YIvJj)~zt7L2!|ZAy@#!95rwuq*wHE)KA}B$|ax2bsOc1Xy zRnCzvnrc1EZ^HI=LxR)B)l-|OX=KYovLOE;kbV0+eWBi@u}`*T=WAWcsCh2-FcaZ;7Qdt zPS+@^qE6*FOWDG7t!#?EeV95`i4d-Z#=E-gMpPXT33=G$vshl1l8p0f6ksM-fZ zWoZ$X^VuazyzI0VP7N0FhGkrfx*7|YxpKGX zh34LhiYe&e@RMQmc(pD33U|=d@H0^IH68ep zdc=ul>xzFSSLaR%!NZ+#my}mr8c6eumm8?Z1SQ{@OFMuEMOh5zE3OrAAd&?LqABDN?!pyfGu;YP6T`@gw_ga4!F>O-KO*sZV05|{uLQR} zZa|hUTqzhULx7tjpmEnSp*_sXW96j$Qb|~Lu!AC zB%Lyn8^1VRfgvNc?|^xEvQU78c+Dko3gxJ6iqP<3^;-bu8;9bgf@tLE2%Oq8>+kHZ z{fW`Zgmt0yFg97_8rbb{s#ru*#Jq!;Xp2rLmJpm$JDZq+0_giW6TAbi4D_K_0NA%#H6rwiH9#S~#4)4~X;d9^@T~N2n+7lvg9@ za`Erj0YldQApma1|Fv=t-RbsV$ZuO55m<8u1uLY8K!v7HvX6}Bj%axb5j!9(dKpSU z(j6-C>>?UdZqv3&c6)OH_gN4^lM4;foQ@4XS5`VLBY*?>1isV!V){S~MP7pN%$~ z0js3j1xg4sNBJD~J9-TaYmi_qU$HUSD*ZdPB{25xRIS*Jrtz8=)&cyZU>V8%oPL_( zN&0~<^6*pv7jY~kk%%f z@)Fw(ACQzL1u1Z_UsP6y#EY*^9A&1z!%Gy_nAwiEr)8+KS>j2j*T;fG3jSjx>_3RY zzDlt$O~CJ1!2MFSc1RZ+x14boO{gZ3{XBjrv99v*ZV9mmRir zN#~aw;gLxKVBip@7wGYUT4dBDhosb>kf$1rCG;(f%a4MVl8d~+UoBdv`{r$47$lLT z`6o5+)EiW@Bs~;L18_;eMd=0Li^nlDUVR5Oy2-2PY07FdjekIU}%Eep~}B#STQu*_@f3bIjAPt zsMp6LjD@cM3>S|^*3z!Ce90AN9-@^l6r>BXhXx#^JqZg}wC3Cyzx&1Y;kQB21%puT zpl7%mBZk~KcH^+Dez8RNB6>D0-zoUe>d;*FXDC2B^BjHggNSbaz`n>faG|}ej77lN zSj=Q2Ky}p8`n#2s*cdA$z13goN=V6kBv?!VxiG>7mLU9Bc-)XncSosLNfxhNJ26tI z;M2*k60y2iMWRB##T^%2S%pc*xUDE|H*U?KJ6)UKTas{ux$AjWSrYK#cxbPcd0l5p zs7Qaed(V~8Px_^M9Z$-Iyjnr>{;WTo&2~v|JK+UYwa0XVK*X^iX6>gr%cXu|k{SB? zH7-TtsvIMh@i2lbbGRy(5c@^RLXZiQRiXY=B?%`azw+UeiE$&*I99wUS07?Q48db< zz%DvjlQ|P6;wy;-AiGV)R^p=|4)V5U{3*%-N46xJ)!N$*vI5$_+dx)p5#%V?iOAf{ z2j=dcKi{XWsD26yjL5rT$7-=`%K%dP2$QmnD+J2ei+bC;I)8SS-Wtc95VAfAC3FUTc3P}gaX9(M8_C6DwTpIE?)QYWp+!lMT z#1W}}ZUPkQ1lRJ$%}D)*p(eJTRaF~-a!_N4@Pn*drBR$*PB*cZ7tW7Y3SHGgRtZ!l zZuiu;;@~hg44=?)_jx0u9ykGE_4fj9@{if{*VL@iC$hC1%Sny;o4H4z-c@!wSkbPU zS2AoqvflUECQ9%9TeP?V7&)ANsV}}ebaKM3nY5@@R@Z@t458y^Swr!>PS}-2#`&m= zZ*?7aR2#_%(06=|H8bup9XlVx+p^I}_RiB+-wD7j=b4}EHuIV-eJ`4m8|Ti;)sZr!*z@4(kKTQAbULB%dehS8uZX&-OD|Rn3hBA<`zp>-W*1JpbLDLvSDY< zv1Od*lX98ovw&=zf~0sg(i>*?Kw2>pB+AqO^G6$0=z|aI@{)0R@p>Bb)pPjYJ7~p{ zUv}p2Lja`+1b-aqA!$J1Y&)d(Z)a=4``bW|KLCjU$ z5ZR7a%FB{Enl^YZ5vA2!f433L0SS0#u7zIA0YsS4tivEpTAv`(=_UcDGA%$!U;{v<}Urzr3zDUCs|G0Ikov0BTw7`S+iKM`_;VjY#L+( zfvI!)>lLE@7D}#yZm}|y>NsTPlh);MctH#_wsecWOdxeIfEZAbOwXnsf`-MT((}-3 z5+*+i=`cYW%W_ByWEJj0!5U+I58F@qS;<5kC#U9xdVPcIt+Mb}mME5r4@5IF2#joS zf^5`BWJ%t-mT?_Wk$4+Yz2pyrLhdJ`Lvi6rqBBq^MpwiZNPxs%GNZXH-w7!LX-h;s z8RO|c6G^!;hzTTDeHu33rbA>)cNVbm7*9$~yMSp>BBjNV>bb@-m)4 z{^f$fwgLRf%^M9LUGN(cT+nurpeZnf!wywT3gdciyzGT zN`~_v(N*4K?@8H|wgfbzEy_$k%1vLbT!&kKe(T1wLg^Svx-&b&KPcB*&9U4h zQ}@uk^>#(go!Ma6AkH8Th7cxd zt&NB_Y$cArKt5ZE_$1*>9vm)WJ#__aG>~)T<9-_^l;cPbze5}-0Q#_&YVl^~F0pM} zHNirT<+2S|Ql#4Co@9o1T_wTiTJ{tsld3EJdj8Ai(_Zde+8@t@J-f2V+cRdFe2d&C zul#|tgLO~eoQ$RRoNr!{?t?>fu3)ffD$;9D@(og zMCf9_1%mq=4aV9GCzJR7uxXZ9 z&jWOT>q&_in*iMX+|43hGWUJ@Z_!t0ezMy%QU>n*pJ4V#zdQi#hWIzx%{B>Rs28B~ zAq?_Kn#{*JoJ^0v_BQjhhvQ>K^z|^N3Vga^RO&9Z%R}_?*TZ-r?C`xy3CZp#i|F0M z&*s$9WJI?xx=I)Lls){HjCX;0MlJ~{@1n0TVNAAPVP>mg%-jX9Ns#9}nqiMKYIHk8 z9i~0@whbR}^eQftDDfEgw|o6I2R|!$zv+vK(>sS_4PV^A`w`T3jma+^5ybGUBpFyp z;A1*Kp`wVzbQfhY%Kdq*jbzFC;VSs)en;_IByzL`i)KH)g_9MZo=H}x%|y)#yX<`< z&xK7ak^&f~MblJqmZhholBAxR1#g^aqIgFa!Y(crR&}>KgcGp;jNMW6zFuvtl3Mr~ zM|<$2_?qhY;99C_*~M*JfyB}C#Q*$=u+0B?dMl8ty&U+;-S^rP5Xj-h4LovZQDGEN z!`HG*QrCbo638vk4PFtqI>hIqu#+XlcW%GB zFY-La?0SoVw=MZNz@9;}%5P1(v@Q{1;82^aQ(T`ISKP?-Ll=jL_(w+FORg;lsl68< z2*Qb_m)9gU;;}E-7ZbscAU#?V>XJIn0L7WV2g~VbZLS10rm+5&DX;l0d;i8?aWaTu zbVb|MVl|-YhwaS*i5Ur3nmIkae7F>2RN+*QVcHUpiN)H`=auar5yvcq>W0BzHnu%v z+-0fvKTa){#NjAf6@K>f`Y&E*;*P0|5TE-6@^)2BA}=-(6}`4VBDX6UG?@`;Lm z{YH~a%6Gmp_!#k;T4Eoderi^itb!eNN|+(8=_yDi{344zM}h7e0}lNpc!nuPh2qI% z!lq2t)Hb}y##@mUvJx(naZssZ28C#}30DxOaI6e@_Xc7n*~qTK#nj(F{0Xw3v3}a= zqDZ12|1wvMr@ezG|D)$J*%oF#@CMUR#IAWTd$Lf2lCC_btMB9%kIQ?FF9H2{A*+q; z&uAr9O~yx`ON|A42@R*FX+#+DFb>SXjSsc!AW6TUjY`3Is zo8mUyO!FGIkd}ZC5mqFU^myB64EhuH&3L-g(?5%K<8Pu|LHn8E5IntCP&rz<{@eR^ zZ6T{|USS&_X-hw`m&%v1Y2N|2qfRLLj6o*q^A@|>VECl3ci{HXuGO4$M7B-7d19zJH?nWa&LaGT{Gk zYO(uzyAwBWJKke-Nd6qF0sw7la6W`yfFeZMlHaldO#Y!8caJ|x6^YdxJ#&3+)3kfs+qn`Bm5OPUoS z5#O(&U6QIA!K5dueJFhqZa`^u_)B42kCIi0XvR3)`!~7GVWRo@7@1!r@shIfrd5I~ z*Mjg8DWectO7l3UVMHO~F%h8wk?qKv(%-e59YU~WUqh_2*(FX-cc=u1YR@e^yk+(U zh2{-PDCtJ{_EyJI+)ID!QfOVc0?(C2ScH(%O4c%b9mG1a=`VrnxRiZB4^~6cc2n%F zj8Ghp*{{*I*45>V=xPW?j^mlZNv-?N!k@a5>eelxckUX9x5Cn&;2|BR(SsPlJyT}l zO>9+WyCVa;4sbM?^PJUw5;RHkiiYwTv@OmXokdCh0+G6GD%;#L=Kr;ILc}r_)#6 zt@fRi9Y^QZvNUwV@ji*7pFb$C&LkIgx6$LvN9v3v}aP zgGWHc*Z+=|%R$k%T_tU;cudTiRpT2MTl?#Uox9j!&8zU(O8ECIt+fvOhJiWTf0qO? z>=`33V&WpJ@--nS-}ED@`XqZvQ4ITI3FXz9=5?6f{w2(vw>r}N`AMG@-|Mt*)llq< zI9C+_*!UCPp4yeKnVfL!E!PySIE}Sm2zi;hxgH35nXWyW?%t1l@UzK!>M$fC5x$={ z6y!ZP;5npK$l~0I@#LAztW8YESf0cEJH%L6NaS=l?UaKALqQH7EP6;qZn$vgQ;KUU zNoQ0)Z9(bPE|cw^cC4a1<9s zM1(%YGUyp0nw(2|r~(dbqhlFwigxTKQh+68hqelr)<7KvxnVJ#*neLd+VRP_TazCB z7$M#AVNZI@ppE{g02Lj4NoWUX@bOTXQetvrBxUU>5!5SEmBF}&38|V6B8P{CuVzv~ zPuy2WL<~@ZbgW*}X4UO$bIU6Ym&L#O^Y?>J*9SLc8IyBcrRdCB!CE4HFXAtB*yu)VBz#*zwsA{>85*pDi*5T2nq3Xj&VbB2qw~=(Cj+A{vHro|S?ah$;`w=CGP{WA+PWPCS0$z{s-*dXh0*_(bx>^52ZshJm4uOGj&?t*Q)K)gV?|{31nnr{(2VyFKmNaEv!B zkVMyjsUkkc&&$5xZlC2+d^v|=bfAl9EZt%4X7mQ>JHLVs=MG7;mLC_h>1@e6Et8jS zKrKv{HT<-}=Leh?-M@12#qk|vsNVM3jty-!b1rS0N@NRUpgBy2c-XIeQ|hUH>m_xG z1q)V3?K4;zlIykcCW=8qxl&n!IZ`X-w2zskP0RC5{dwWM=W`MJeKlB&O6d+gLu1Sh z{Qb`SSR-_KkwtI6L0*MEGiE)m9E|ysz@1=M18EEw1Qzz~kQl zraSTxxXd4YR|-}bXW|wh{iVj7{FuY02h1~dyNJeGpL8#VzCELu5Jx0VP_S8ZC~~rE z(`cqCn(&-LU*d0=6CSgv=TBA4Zh0KHiM-Q6q~!h@idb6Nh3t#AT|=K_9E5;&&eOtf z0__~vv))7Ji)LKPFn=8yd;<8rwl}{wRx!35;&#;GUZDR`+if{YEL|J;^=d0hvNjIQ zp>TDfHi)3l)Nxc}$yA)@g-c4=?@56Six~0xE&{GX@AulmW%%;-T`h(06PCTR!qE>c z7=>0QP%(A6t#OCs5}}n5TNJ!_zV}!i;wok?BZIt5vgk zPLl%@H^By`q6Uv&J1MP*qQ3hfR+K+AdpFn1vd-kuf&o{65iLLqIh{JoV>g53#n`L{ z?r;@;G4%?xi64`P>~$r@YH2x@jos|KZ>!>;kTCsa2Af>P>6wE6vs1ET4t%zi>8WPF zSE;iUgIA#J=R#GZzJWt1Jx=#U;lZfaAKp@I-kGic9f3_(pCq4f%=ZrYC3NOE)4*z} zpG9c8nkW|^Et5kM<)~7B2`oAEuTg_Whb^m7Bfml)4FoD)HK&M)`Ha{YC8FW$jKseh z$z^#f<6ZhbALb`dHM8Twkh1?GE!gJ2Q+?u2EnDrj{MZd1CNF~%)vYz|G(aKEUn;ch zGOl*gwPA$4zvE^*%3Ua5%I|&pPII*>k4`$lWfN_x-TBSL5xcAP=jppk$+i*BsV2I- z?9R5kMGRS;UD?w2rQ1q;28usA9_b>438_-`;m>fJ6uuN&{o4Ya$iV?a{8*sP!wy83 zXA~+j3=}C42HfZ`Ur^;y(*GE0r?2<}K6^mRQ>gNahTq+?&(rODq=kpPEWfAy70N-W z9-7pf&c~x75$bnjx`};Fxfc(=uC3KbKVW>TTzXhA6CurODTp0wyKk5O(=ocf>SNd3 zBiEZOY0`&LE*+PV_$4gM=jt};j&n8^T2it0yaH<6fOu2wF7bC5UgrMkE0`pNOTB-+A7MKd%sSw z4EM0a1!*YiZMKt~YAuBRK493+XcQ@*J3ie!wd^muMN}cqdi(x;bk_SiJom9h@};!n zgPEM_7!I$+Bu+(Tv)8rcroeEtZDEF)+<7L=pkHjk$U@S2Gu=D-MQ^I;S^})Wa)od% zhu=-K+4r?NQyW^j481R5%fg-+(Zp6^na%Cqi$1DH`)?48L9{f#W<+Vei~Ka%fCKy0 zH^Ib)%TM#EgSIMIfuKsVpQa#@FP`xv|2>+a#WlHM`UPb!$uAsEj>vIt3nmf^iqe~p zPv2u2wF#sC9$2w9+H#h!57)!=c^?RDDVsW9TpjG<${akG<&tH*tmO%Klit@QYxiQ` zoK+HwdYbI4mErvWk()?xal@( zVh9&jb^q%wiWC_-2}2})u0fi1Q05Jj!aeH_8tN%$CjJZV_V$eBjg&>li2=W}1NzZX z8mznzZ053;41%%pTnVbUIBZnq2WTXd><4IWG;@>b=)mIe#jLD)mRBWNE`T`C7ZYcM zT2Y8yRZ5#VB#($A+98AQXPhKQxWAPfBF00Jq7aw04J^gCkRkj|)QG|z8{rwbJRJ?K zL-R2N{D(hHAq)D!%J}Uh6#2DIK>z%e+Yu>$8?mTc*63C{XB!2EB3=Z(>nxN#fE~+h zOZ5p-tQXZe*{5CEtAYIgYAW`)h12hDle(@8|*Ajj=pCd&nyxLYn?f$Qy^~y@fBkDLWOUa zxc7K|Z7$V`$ohgRib>J2syr-^11=+YT1mLuoJHekXN@#;M)8D(p;-rO5f4Xk?0}to zfDAU8nU6>WpNw3Z7(zvr63l5zSydZ%{E+|Bt}15g3TC_%0=lSxS>;8%jyOBxoT&K@ zTXBa5UO)_J7`MlsZZ_5}f~bPo6seChYqPhA9D4c}xUc!_amUAue5;D>H`Szxj>=YV zc<>i|E+`^4nm&a20z9bDNS5Qj4?qXY)6T|&9}tYXFfYw#9Kn{gQ02gu7p}Nu$;Nd= zzD`>gtQ$xoNn$vtY13cZl#EAds6}ckM*}}3MNX~qkWb-axR4>f+UfnFB;qpAV(* z@KLLw!b&}f=fS)*D`0QM%f&95s9Wjo$7FQ|ZAPTs7nGQgYCCC5zvmR4hHSXz(#8g3 zn|kPw3X4MD6)^xFyzLEs2>wEPFSq0?-KbccDe4SZI|kTL3U>#8Dr2lb0y(BmF5;r1 zU6rSdaFg44dJ_~%Hfzm&l;rOV-UtEl#sr}1bhL;t|A+gGX=JRsHlz#DY$dU&65Z8S z*x`tHZH!q6wdO*Tm{GnR8sLz}_{R|3#FG+TG=@GCOSEPd2%ap+z-7((s8@l;%El>f z3y+4I5l#LTjQ)69iE?`Ffp zJqP`i$!-M?deRhN`lD`;OYl+2#8V6yYpYt+2nsun`B|g(@ot7=q^?^^PIZNwS(L!! zgqv`ysFR9+ibb4%rte$)29Gj>$G+%dFw;+Z-_m~I~w7& zg8$&(vcnor%2Flm&VA7vRh9D8{5XKxHo0#^2Zq)y|-JBre-*)mQ z%F;+u51%4ea_J6_e|47whv&_&a_Bhziw-Ax5rd6i37u|Q7T+Mk<|vm+W6{g}Cl>tA zW5~&%r>;7kp%#N`WE#r9t?xOxWx;=+5P*spu4(#A{;dvK024~G4jEdV%K!em)4x-~ z)9^nJ`fo5=66hapqM@%&`=9s!@290q^+X63^@mPLF^(K$nE!kWb1nFPdz#J_yGr-} z^Aw;l)#9m-)O;7);Al<({YbX?rr+uR_lf_XxA-?Q9a$bpZ?NZL;WP^{UPC{Jb1e8w zd-(KHvH$KA(v{zenIru%i$JsNRfiF_ZdB&Q$;qte2%%vcYMz$8IAH90RLA`xi2q(L zQ>aEU;f#oH!GZNW|6sN(%dX#INE#Cx?nIy;9ZI?fn;dAO_%y zKkG71ltY|}<{bMa6+^fKn$^f(121V4xN9~;+n+-(&QPtT6O{au*?w8OAoB+_!#Og| zFu~;oBK8(gK4}C_(j}t~AHmG_PD_8tH%?f}R2Kx@-hZ(cAq-4XpBPh&`WV-g0(vz! z=&n(hBy;k(O($~Ja}Ml(=D{WJ)n8Ce)ka;3Gg+{{u%SZ@1n2K>ikNK304FSKd<}<=ZNRK3IduwcTaZwn42|R=&_TO^ZZmCGG={P*-8@811#Q6+LT?y!wmjZ5 zGDwsEo03Ck@6iAKZ=pm7wHt(q+F7c)+3oaX3AQTGTq##*L7tH!Rjp9LqS>{O(Yj!9 z5iOx0r)Esxdzv<7Cs#2IK4g5d;9{~4Ot&dYazlP*T4YS<)UD-$l}hfiut$8^lis5~KApl3={+lEL<5K^o6X+o=r$fgBVdC-P@#S1sy(!*p>I{B894zbIRu zwwVGx^I1T=W@rIumhkr3GbUtNNilV-ed3FTfKNy)~&+DW-;W^uJ{qCJ}qXxZ3p$hnA-#jibHmV9ss1& z2G9MDhsc(}g*K+AAxLOyFD3QMA8~S4L~{lq*4JgO|CXdG=vxuU=NRieCR1=rz0s<7 z^6~Fonip{6`Z!GQgSqM|cvLA->pm+v1Sa5wCZhRw9iz)I%c)+(fp_kBZvK&{SfGno zVNsAx8HE+gq07VVxZjl(3Ct#NwY%-qUy#=^yJS{AnS&kE&DZyP)5&THAT0PZ;AM!`C&;nFv z=nIzW#sm&;ihH2_>K2zj$nh^J*JCPolJMr5R|sQEn!bkF>**T{hb%P+kSdb|G- zocIeGS~r3&m}@b_{F6Swi){OFbM`@sVYrU3N6Ay2*TiKfHLWgNs0T0g!_+Wk>W82f zIUALKo8ScPH*N7xG!W602>^-bY}f(2gAP}+to3$`Ezj^0eJjOLkE!Cv zVd(MrcCN?`52NB#j|4973`h+q4I zp?CL6hyJ9m&;sPbO*FfJZ}}9osnj8Z?w^f7ku+Ea>Hz3I8I6D)(1^w}-(hgvPi+Rp zF3=T$T{33w>ps=fjT7*z-q+6QI>jsj+un*4kh4br_vs|1%SO^~i37X6o}QETn)~#NVI0&N$XF2FOQ5Z^0aF{lwoK%l}Ib zY*Y|1j5MiU@rh!UAf>(@LF_>Wy04u8G{L3XR(1`7-EyPD31Dovl%5KK&c+Xf%16mm zXF{NX)G|0_@ZSw%4Dfg|YR>f3oP3?PkHj>keD=B^MskuEd2ED|LMkq33aYHgdoN>)=k ztD3|Y4NTLvr0>6Tl%YPl>ZZPAiFTXzG$k*A})js%{S;4mq9kIYv&uB_KH)0uG)W$_^~7BraD%{+AnO zBKH}B=L1x)BZ5ko!88JyoH=KZfM}-6mJDkYcLqr<+c2&Kmket{PhhcJR`$D7^FtGZ zwk1++Qf{lo7z)AF9x#Z+;&1^4*?UBQCeRC`VT;BDH-R`E0MsD4R=J90{3@i*vN7p$ z8IVAxqr(U0ZWGXy%=j%=Cm^cgcax6<(p$K0W`SG9{W3srp0XOLGKMpve-`q-v1i=~ z+1{8qMG|`+lc35FQl<&$Qxta$E2{S@JU+)(rF-2S(*?>OWwr7QsVnA%=tDn>A{;^1*r-Z z(@-w6*m(BN21u53Yby(}DB)m0vki|&0m*RG9t^DBKewSow!Z+g@r6hZ(eC@B?8_{0 zaF?)#?cdaH`5`Q51K-o4U49McJ5q;YI#ng=xT@x-%L+f6@uFnxzHeLz1xBtR^yle8 z{!|f?o5(!zQxjFe{>9dtmAlFA@uCBqQ)ku%47+!!LJ7{a@2lmSSE)g=ZZWtH;0m5-N}+!K_bet z&y{2gHwAkbcPJCMlw=D$ze*lM=-%lWa<)kjvlzPM(5lkGu%jksM1L+$yv&?hRxWHU zq8>&q>`V%;EmBV_MY19DGS9md6Ywdvi8II;lb>6F3~(SQrzDt><3uvRi;{aV`Q^EH z^QBqoY$fSW;;2-1k}tbN6kGz5?(f7Z_?Yc&k{=Rav${cW% zbnvJ!iXlW;=OLo85K%OlM$sQS4Yqn8WMaux)QE@!XE2x(BD%`|ISIiMo#n6kDW-(Tg$z5RZM5*}*y74_K*+G>TL1BY)lf^M zR6`Y;TxAe;hsm?OnK1G>u4c#g7x|>tyZ_VNb-%;awp&DELNH1QMi*s_9$lg{T8LhQ z(V~SQ5~4GD?=3=tMDG$pL~o;HbRt3Yl0+{-&NJ`(opXJE!ttBSn7!wjz4vq9Ypr`N z$={!^d~8-%9(3cxKCEYLHW4Nml0(?6LTs#Wi#HEb)QwN*-!jncHWXzvxz0J1EH?P@ zL07GbufZWTfq@F1aQ|ZsP3_v0x+Dvcq$8D{nyk&q8(mloMfiuFj*dTZUJrjYqolB2 z9`t0~wd4~5CeI}UYFakuCp|X5_c_-3ebtcnWj`+5Gz7z`YE~|rGv{7Eh|zC<7;Mpt zxUBcs#8DI0<>^_t^h8OLh|AKbTA#}0o1>!W3xE^xY%7>rC36;Bk5}$L2KJo`d{`@w zBnKvosC6x+U(4-8%-aTMR%FnXZ~Z@Qo}Da4ym*=Nj@%b$_F>U2I2nLj+G;=7tAl|L zr{D7(C>xa8PPzsBdaXFxJCf(u#!{VId;i9Hy_4_o9RfM37k`t4AfgJ!ALUIXS8t73 z``s{Qtd>ftWcPVHmQTrT@zB^P^@}I)t#x~WUXLYT2+mR-KM^zxBTJJbzk85qBp)`w zNmd>9wEznlOR^4Fwk~Ghpe_3gTxu56CdP)n+Dv>hH8-(mooCU({O8pGe_f^d~mypG6P8?sN6 zbO$~{f{AnOgWbbhDI?}y+rN(?WG$w9 z5IwyAScMq7iudJ5G@2<-a?jW%P87CE&kBc9VCo)%-G>pwlr5>io`!cTyn8@2k<~pB zj*)roO!YEBeBLX?@-+cPILsgDEWQZ#)nz@5oe)abE(d0zj(Q2_v@uzYE(T*sld^k* zs&bTrdo_CMtfTn&#l$BeV!b2-@rj5hWKGLVYZ3r|vK(J@0Q=NY(QnGKx1rOIewXG?$j2vEn6#xUdR!DY4O%SLdg!XBUpJ8ux{`D;%-B?uB{AX82lHihkkQP-AvZ9k~&qe zYc@Tx8>1e5oS?*nY`KegSaU*lvxMzT(|Pn^J}gn8kX`d4#&l2M=!}vT)q-i>sRW+W z0i>YfY}YA!kg~?AL1pgBV{^)CO$mU)DoE0%M3jQiYv;otw}y zc3;OaN7Ramg@Rr}7zHhli$gIV>yOoK#iT9cfTHa9`hYoLpDb;Bs+JRP8*8=c4`!$K zCu0Q}`-ws&8DzI-&quv*V;)4NQ+= zbC|w@`je#C68I{$hK-8VSsU&GbC*N!?qiURIwk&K@TOE{(yiLOv0Sv!p?SG^;Tuqk zmblS!KzMkZSxiWpK_4a&iCE^LvA0|7-Mpni#XGV&kUd;q(yNYP$69WjzjJ<*6JHpQ zYpJ|g39i$f_> zS&do|786uic;{9tZ#Muw;M$-%!dgpBEMKX!Ja?3Q>(*ezhmAc533%8Y8b|qb83znm zNl}_FOR*)c%ommXCghW3)gdLs>aF*G$5s{DZj$oAwz`(3R6o1dil!rFfy(!}zy|5t zIjIpfp==8L;yW|JFQEuae+~znKlf^Jb)K+S8bYz1(214yK9O0jV7YPA_BC-;{})Hw z8(L+xt64k`t3MyWR}DJ_HK@+48EsCX;ucKS))pqR0~AtbRP^C#zD$D&mxxvDg^D@H z3=Tf&<$5WmanaEzY$0GIMPOpTza%BAE}$Xg87rrdJ6$4Np2iaNK@P(Ebn)OB8^v#d ztK(0rnM&zTjZE8Aw8$@Ud9B}V+clyxpaNGWV}4FWN14 z9c!ChnsQ&NpI5C)*=H!7YlQm4bX~XP@b9IsKRv&d$FK3uByE?uF*IXcYZ~8nGVDm1 zGhif%aE1)mK?e%>e3f=n{ctBp#{)$UoF zBD`_aL|I6NLJyEg@a?NbrcuMNmagNp0q=LizFuTe5>in8*ODvRd$ov^d&}O2J`9U2 zI`wIn+i@YoL8*s6<1qhHQ!#u3M@KFWyxzg@@#;Cj2FXPk4`+Yzi)&B=(Pp+{%|2)n z*WzXNkd}n%ueBUS1`=@2L~SB3wJ4@#>`kv6-VS>OuHA0|!c(BsZqV?$=Btd|M)g`e zoiM;v{EtD%xi`7&c{O<*vnF;2bQ=cq8%)WZf_GpQ7@0p-)+d5Pd_{l8;4|+Xiof{h z*%a=>#O^I@`Hbw#wusL1qh{zY-WlG(BREoa5V%uH{fVPv-=i$%Vo@o$e$J_^;_FQ# z^Tj$zVB6HPA?0#a7U(X10>tE4+OcLiI5f7l?g*%C4!koStaXxXgL(O0KX=W_8> zNY`=jYOL`pU4}jA+__fS+k4`iVyT8=qc*>t0|Zl> z6$Y9X!x@@E#_LC5h1gg1#t+mIGBdWWT+3cy@4H93;sT!i@eK*w0^#MTy?FNeskwq6 zN`VgA@|TK;GPHHn#G?<7!mlnLyBv)o6`w(C(_eCDoiEP3R)^F*n@d z^ffQnUt{`0L!4LQM-ah1&za-0!~}c{j~^Fnn7AmlSRiw4Z ztW5UA%#+>{{OsDiQB39^2(&gXuPRs1z?|Q4bTN+&z;O2T7C`oO%B!C}IG3e)V4}Ae zwE^Ipg|r85;2~7$_Wcvt1}Dc8;6KHyJiinNDKb4#jwSB% zq!3(rwDjjA3#w7Whwu4(z^*kj7#*-Lva@kKBo;{HOG<>K{l zqW)%-|6{Vjh{*EAE_(F}gMy$%@GbvZefjL6IMA*AIm}6%10Am~52FU&-X=>5l4qwW zThKbha%3PvnJ=%TWy!qCd%yfUxY#cxRmJ%R)lR-!yY}frC(8KYst~X6pw{nFF}?n3 z2n~6)Cmx}4Mw$*rSH6OiYoROfcH64}H*|i_9s}ccD#4UeVe2g~HjM(tpoSTO zM9n~&_I%~M{JY(ooAwG%5(87$Z5|$gonX&epyhXnxM;M;B?CG1XP@zjo{O5`tX$u7Ts>VX>WRfWee)8JS7+Vc?gzFjipoFzk=*BUo+r;&pBNW( zK=FLd{vN!QJrxv_wH}~`HO@VjZc~ess^-jUv|genF-&EM(coNo5BkJEEn1=(BQie{ z4w(ba`ZCjAlLTk8CEBR~W)z3#?}y3VW(d6e_4jhbdga>jv`s=2mA1^6S8pv>340#q zzxyFCyF$!czA%QCJxX3)iM`$fd|)Q$=-0aQIEfr`pM(cm?_;}qB;sFgClWxoKVn$Z z)`u}DGa19R>UgQDy9AjFu)DtXAHiVQJfQx5lEh|R+4n6Yt-HT9faa6d{KOp)AG^Iy$!kfFOnc>*?{ruI;x7lT^H zPla(P@0mBr9RUUjLQaeo;GJn%OJUCUJju+yb|*uK)sYC;r~qqljX{ORBa% z^S@ez@Hh%A{dUp4K^D_8-(74&p^EwKJ&|>38pcxmcDsSypYaTjyIz1}mJUqhZ{bK1ZS|y8IT@V&q-JBc&C~l+b<+(8#x#m=>t8UqR0nhE zZdHD)G*VVB&6LzR%Wz0o8^r`GEF?Zxm}arraA*a!z&Cwvr5pV$BlKrF z^o)^4B-yZi(px?6Wv-@*sx@W1MF8?4><9WK; z6z)C@OeTJUJ;j{_Q5`fj7X8Uk1TL2yN+$I#G~_Spi4aE+nQ>u8QDcG595p|o>2-WH zgjtFBM0&ac%aO3a0YT9s4lB(teh5g2OXawF?ww46<*S$jmNSH6uSV)dH>hcPc^57L zF*LUmkdl$*S%0T!mPWwPVX#cZva41kOsdUoRql%_QB!9(CfMT@Ie8&QhGaU-LF5)z z1HyKZmp<%Cu6%|_lWV_6rlGa2B1%;9W5#A^8DQf(Me#`J7cHk%xf*H9KEC=@QPW-rEg?jU~9$waB921IT8n zdllY$13vjq9XMbGT95?W%d=d(5-@b57(}hFKl}+rYM`14@TgOMpak|61ay|>VkjJM z2smL7#V>cLRk@0iNMaPfwLIEc!B7{>Tr{@u@duPNcE?OeVzk!|mGKy((U@R13fMir z4;j$cxp{W<$gV6ymAIQ>RK5ElHlCw^e3-Y6U|cmu1&8+4s!#|=C|;!*p|SM@H^MW; z@Ef}tLZdm+JUV23%={NFXP?<*k=Cr`%V0*25xp&M)f7RUmcYaDI`du;Y^ehH5C$~2 z>P*&rz#y#PX=h`P?MX7#wci)$TnjiGOSmhC1ay-*FL`Tt46MwSJ#dvJHj@eFnu{Pz!dz z9x&oXNUyK>!_`HSR8beCIV!o|>$1Bt4rUIj2QrV(G0CFdE;?4THHji~OPVvsT9c*L zX0TeT#_ps@`NpybYb|9A@NuG|@HtH~6TC`vY7s+-)LX*JdA=hf!=eW?m?9)TFwU8kw0j0kMWHc+4=2_~jkpR6$Zqf$*=r|SVI zdQ-w+#0v;0Q3}T=p*a;vW>(`@Xn7e(q`_cu`kdeX(1i+@rY!0v zB+Y2ss&_X@x9_|pAs?qrgahGAE!#=zhY9FA+n2TlCGPA>>ys&4F#OKJ^I<)16(d%h z4W^GpQs#p$8Fs?N`7Nlp%Qt4=mGcv%o$szP?y^1)dy@^JO8PKr`v@cUtl%7Fr>IK= zSVeK>8pDHeJ?+CV%8vfORJS*~F|WLD#>0&u^^^p@V-31)rF=Kpbs}Nu$xnR!IlCgS zC`4^SDF$_QyqI`@3xmqUA6>Na+W1fo@9Gj@=?jb`E&;FP)6YSCNkj%)$FWOKv-yGl z_$N4U&8_PEBzE)(>;zL}6Cx=ELUUiydogK@_Ucm9g*LMk2KUy{=c9TzF?5CI^D&fi_GPdRvdq)_PBxpH2{ycqiBh+4EqMf9g4;3*!r{J0`b88)mZ` z7&%gkW50GF;<6N=d|yc4-u=UB{rd{} zR|ufDQw-^AT0j%nf^shR&KJymiZULRFOWJ4FDqGZRFWS`; zvfhWfpqR4-?6E6fcFlk1j&~N?82XLb-}Q;FIV3IkVH4F52jUu{Uo1s&wb-V7ViZD! zy>EzfJ zES$>-@*69-XRE6Qp%gI9)w-f6<4P~%)&KD;)k-l9p)!7*U7XDLr^)wZ z^Xs&GD_d9mTIQ$Cv9}2?KnphyOwi5)@hLxS6z80|7x&`AB|}7kjkw#NyL+)Ks$pI; z>D+f7IN6T4X)NYSOd`JvB3!=HeC+WNrE5VXs)oHz^f>3lZqox$qzzK&yUua7gs*GT0WH6|) zSHZ&)s@*7Z4U{f_96XnR@oF`S|w1#yt7zT6945SF)gkn&F237F1JGxaC zmdl366LLo(okdk84-)E??WU`~zmG@zd?D66n;B(Sh+SI6&(NLamQe?tEcn-TIOsk< z68oghCF1sz+{bbVzU4lS4DY;joz*CvVj+=a>a&F~c1*|E#-5++hs;+Fi0sy#4F`*n z7j?&vwkBnd0(i$wE20I$Vmr`sDul#ZQ+u@wS3820hQ-RloUgmOT7UC1({RjOU@%IV0u>V|l9r{pMU$yy~m!NfvjF^fsWK6Y!S?yb0;lM4Q{ zyeD}lD`cqy&(bhCO1;B|G_)e5ILZ`F~P7F~RpU1>19! z&p{>D;;2};!*sN+rS0@H$f^3;oGRX#NspeB+3Dqf~{^kvj) z=)qHuXgp@8qT$#_`}glNQ;JFh^tm2!*1Xijtq&O|!#9g?B3@x-Gv-wgfQQN|g}*4s z8=sv}k)&1pTv$A5WN$D4DMX7Lx;}|nc?&f3bU4>df{sOZs<@s$XGxeo znloKRhoF?T|yuEYPCNE&8DKr!n{t-v#2ZJ;q z{HCx-mZUHVk6VuNn^#Mgt6z|QEi~7UyIjgoSQn4v2umgp)lu({z$bdL5+?VCRnTR+ zI=QT{pmlVn>kDaruhJ^#?+)uNT}YDOQ8; zt~4{@pADZ4_;F<9Dk63_jJAGMR@FGM5Dwt(QlL!G{YNHfM99JzIVS6 z+c_#Io2Ux&e{|u%LG>M3boiuG-~1*8!S#O!xHgk02=EC^Xt;^`z2JnKA#zA53DwQC z^!u}-Hz}LSHQsxfB9U;uS|GtxhLhertq2Md`&F+X;QfFhIV0T#{fy!WZU_mIL$d9d z_T3R9*hskI#1Jm(y<;Ga+n&JH9iM~(>%c-tPl=!6C2G5(&jWt(hzg>%U#Xp&SXY2l z$14FfDDEZT(ZNj)>Wn>iy;2%wJX98EhIx)H_iFD_ZwJbe4?!bKW4~(?fCxY!(kDAm zyEfJ|vj6Y@C>0`?Gq$DSH2|u%_yaqP&e?C5Lepw8+ zOgsM#`CmurV literal 0 HcmV?d00001 diff --git a/trends/mvnw b/trends/mvnw new file mode 100755 index 000000000..961a82500 --- /dev/null +++ b/trends/mvnw @@ -0,0 +1,286 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven2 Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + export JAVA_HOME="`/usr/libexec/java_home`" + else + export JAVA_HOME="/Library/Java/Home" + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" + # TODO classpath? +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="`which java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +BASE_DIR=`find_maven_basedir "$(pwd)"` +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found .mvn/wrapper/maven-wrapper.jar" + fi +else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." + fi + jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.0/maven-wrapper-0.4.0.jar" + while IFS="=" read key value; do + case "$key" in (wrapperUrl) jarUrl="$value"; break ;; + esac + done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Downloading from: $jarUrl" + fi + wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + + if command -v wget > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + fi + wget "$jarUrl" -O "$wrapperJarPath" + elif command -v curl > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + fi + curl -o "$wrapperJarPath" "$jarUrl" + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + if [ -e "$javaClass" ]; then + if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Compiling MavenWrapperDownloader.java ..." + fi + # Compiling the Java class + ("$JAVA_HOME/bin/javac" "$javaClass") + fi + if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + # Running the downloader + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Running MavenWrapperDownloader.java ..." + fi + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +if [ "$MVNW_VERBOSE" = true ]; then + echo $MAVEN_PROJECTBASEDIR +fi +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +fi + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/trends/mvnw.cmd b/trends/mvnw.cmd new file mode 100755 index 000000000..830073a17 --- /dev/null +++ b/trends/mvnw.cmd @@ -0,0 +1,161 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven2 Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" +if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.0/maven-wrapper-0.4.0.jar" +FOR /F "tokens=1,2 delims==" %%A IN (%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties) DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + echo Found %WRAPPER_JAR% +) else ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + powershell -Command "(New-Object Net.WebClient).DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')" + echo Finished downloading %WRAPPER_JAR% +) +@REM End of extension + +%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" +if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%" == "on" pause + +if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% + +exit /B %ERROR_CODE% diff --git a/trends/pom.xml b/trends/pom.xml new file mode 100644 index 000000000..d47bc8751 --- /dev/null +++ b/trends/pom.xml @@ -0,0 +1,556 @@ + + + + + + 4.0.0 + + com.expedia.www + haystack-trends + 1.0.0-SNAPSHOT + pom + + + scm:git:git://github.com/ExpediaDotCom/haystack-trends.git + scm:git:ssh://github.com/ExpediaDotCom/haystack-trends.git + http://github.com/ExpediaDotCom/haystack-trends + + + ${project.groupId}:${project.artifactId} + Code to trend the tuple of serviceName and operationName present in a span + https://github.com/ExpediaDotCom/haystack-trends/tree/master + + + + + Apache License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + + haystack + Haystack Team + haystack@expedia.com + https://github.com/ExpediaDotCom/haystack + + + + + span-timeseries-transformer + timeseries-aggregator + + + + + + 1.6.0 + 3.0.3 + 1.7.25 + 1.0.0 + 4.12 + 1.3.1 + 1.2.3 + 3.0.2 + 3.3.1 + 0.8.13 + 1.2.1 + 1.4 + 3.4 + 0.1.12 + 1.0.61 + 2.23.0 + + + 1.8 + 2 + 12 + 2 + ${scala.major.version}.${scala.minor.version} + ${scala.major.minor.version}.${scala.maintenance.version} + + + 3.3.0.1 + 3.2.1 + 1.0 + false + 3.6.1 + 2.6 + 3.0.0 + 1.0 + ${project.basedir}/../scalastyle/scalastyle_config.xml + 0.9.0 + 1.3.0 + com.expedia.www.haystack.trends.App + + 1.6 + 3.0.1 + 1.6.8 + + true + + + + + + + + org.scalatest + scalatest_${scala.major.minor.version} + ${scalatest.version} + test + + + + org.mockito + mockito-core + ${mockito-core.version} + test + + + + + + + org.pegdown + pegdown + ${pegdown.version} + test + + + + org.easymock + easymock + ${easymock.version} + test + + + commons-codec + commons-codec + ${commons-codec.version} + + + + org.apache.kafka + kafka-streams + ${kafka.version} + test + test + + + + org.apache.kafka + kafka-clients + ${kafka.version} + test + test + + + + org.apache.kafka + kafka_${scala.major.minor.version} + ${kafka.version} + + + org.slf4j + slf4j-log4j12 + + + test + + + + org.apache.kafka + kafka_${scala.major.minor.version} + ${kafka.version} + + + org.slf4j + slf4j-log4j12 + + + test + test + + + + junit + junit + ${junit.version} + test + + + + + + + + + + org.scala-lang + scala-library + ${scala-library.version} + + + + org.slf4j + slf4j-api + ${slf4j.version} + compile + + + + com.codahale.metrics + metrics-core + ${metrics-core.version} + + + com.expedia.www + haystack-commons + ${haystack.commons.version} + + + com.expedia.www + haystack-logback-metrics-appender + ${haystack.logback.metrics.appender.version} + + + + org.msgpack + msgpack-core + ${msgpack.version} + + + + + org.apache.kafka + kafka-streams + ${kafka.version} + + + + ch.qos.logback + logback-classic + ${logback.version} + + + + com.typesafe + config + ${typesafe-config.version} + compile + + + + org.hdrhistogram + HdrHistogram + ${hdrhistogram.version} + + + + com.google.protobuf + protobuf-java + ${protobuf.version} + + + + + + + + ${basedir}/src/main/scala + + + ${basedir}/src/main/resources + true + + + + + + org.apache.maven.plugins + maven-shade-plugin + ${maven-shade-plugin.version} + + false + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + + + + package + + shade + + + + + ${mainClass} + + + + + + + + + org.scalatest + scalatest-maven-plugin + ${maven-scalatest-plugin.version} + + + test + + test + + + + testTopic + + ${featureTestClasses} + + + + integration-test + integration-test + + test + + + ${integrationTestClasses} + false + + + + + + + com.github.os72 + protoc-jar-maven-plugin + ${maven-protobuf-plugin.version} + + + generate-sources + + run + + + + ${project.basedir}/../haystack-idl/proto + + + ${project.basedir}/../haystack-idl/proto + + ${project.basedir}/target/generated-sources + + + + + + org.scoverage + scoverage-maven-plugin + ${scoverage.plugin.version} + + + 80 + true + true + ${scala-library.version} + true + + + + + org.scalastyle + scalastyle-maven-plugin + ${maven-scalastyle-plugin.version} + + false + true + true + false + ${project.basedir}/src/main/scala + ${project.basedir}/src/test/scala + ${scalastyle.config.location} + ${project.build.directory}/scalastyle-output.xml + UTF-8 + + + + compile-scalastyle + + check + + compile + + + + + + org.apache.maven.plugins + maven-source-plugin + ${maven-source-plugin.version} + + + attach-sources + + jar-no-fork + + + + + + org.apache.maven.plugins + maven-gpg-plugin + ${maven-gpg-plugin.version} + + + org.sonatype.plugins + nexus-staging-maven-plugin + ${nexus-staging-maven-plugin.version} + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + ${project.jdk.version} + ${project.jdk.version} + UTF-8 + + ${maven-compiler-plugin.version} + + + + net.alchim31.maven + scala-maven-plugin + ${maven-scala-plugin.version} + + + scala-compile-first + process-resources + + add-source + compile + + + + scala-test-compile + process-test-resources + + testCompile + + + + attach-javadocs + + doc-jar + + + + + + + org.apache.maven.plugins + maven-source-plugin + + + attach-sources + + jar-no-fork + + + + + + org.apache.maven.plugins + maven-gpg-plugin + + ${skipGpg} + + + + sign-artifacts + verify + + sign + + + + + + org.sonatype.plugins + nexus-staging-maven-plugin + true + + ossrh + https://oss.sonatype.org/ + true + + + + + + + + ossrh + https://oss.sonatype.org/content/repositories/snapshots + + + ossrh + http://oss.sonatype.org/service/local/staging/deploy/maven2/ + + + diff --git a/trends/scalastyle/scalastyle_config.xml b/trends/scalastyle/scalastyle_config.xml new file mode 100644 index 000000000..e0cd28086 --- /dev/null +++ b/trends/scalastyle/scalastyle_config.xml @@ -0,0 +1,136 @@ + + Scalastyle standard configuration + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/trends/span-timeseries-transformer/Makefile b/trends/span-timeseries-transformer/Makefile new file mode 100644 index 000000000..102470ff3 --- /dev/null +++ b/trends/span-timeseries-transformer/Makefile @@ -0,0 +1,19 @@ +.PHONY: integration_test release all + +MAVEN := ../mvnw + +export DOCKER_ORG := expediadotcom +export DOCKER_IMAGE_NAME := haystack-span-timeseries-transformer + +docker_build: + docker build -t $(DOCKER_IMAGE_NAME) -f build/docker/Dockerfile . + +integration_test: + ${MAVEN} scoverage:integration-check + +# build jar, docker image and run integration tests +all: docker_build integration_test + +# build all and release +release: docker_build + ../deployment/scripts/publish-to-docker-hub.sh diff --git a/trends/span-timeseries-transformer/README.md b/trends/span-timeseries-transformer/README.md new file mode 100644 index 000000000..992cdf563 --- /dev/null +++ b/trends/span-timeseries-transformer/README.md @@ -0,0 +1,35 @@ +# Haystack Span Timeseries Transformer + +Haystack-span-timeseries-transformer is the module which reads the spans from kafka and converts them to timeseries metricPoints based on transformers and writes out the time-series metricPoints back to kafka. + +Haystack's has another app [timeseries-aggregator](https://github.com/ExpediaDotCom/haystack-trends/tree/master/timeseries-aggregator) which consumes these metric points +and aggregates them based on predefined rules which can be visualized on the [haystack ui](https://github.com/ExpediaDotCom/haystack-ui) + +This is a simple public static void main application which is written in scala and uses kafka-streams. This is designed to be deployed as a docker containers. + + +## Building + +#### Prerequisite: + +* Make sure you have Java 1.8 +* Make sure you have maven 3.3.9 or higher +* Make sure you have docker 1.13 or higher + + + + +#### Build + +For a full build, including unit tests, jar + docker image build and integration test, you can run - +``` +make all +``` + +#### Integration Test + +If you are developing and just want to run integration tests +``` +make integration_test + +``` \ No newline at end of file diff --git a/trends/span-timeseries-transformer/build/docker/Dockerfile b/trends/span-timeseries-transformer/build/docker/Dockerfile new file mode 100644 index 000000000..073e990c1 --- /dev/null +++ b/trends/span-timeseries-transformer/build/docker/Dockerfile @@ -0,0 +1,23 @@ +FROM openjdk:8-jre +MAINTAINER Haystack + +ENV APP_NAME haystack-span-timeseries-transformer +ENV APP_HOME /app/bin +ENV JMXTRANS_AGENT jmxtrans-agent-1.2.6 +ENV DOCKERIZE_VERSION v0.6.1 + +ADD https://github.com/jwilder/dockerize/releases/download/${DOCKERIZE_VERSION}/dockerize-alpine-linux-amd64-${DOCKERIZE_VERSION}.tar.gz dockerize.tar.gz +RUN tar xzf dockerize.tar.gz +RUN chmod +x dockerize + +RUN mkdir -p ${APP_HOME} + +COPY target/${APP_NAME}.jar ${APP_HOME}/ +COPY build/docker/start-app.sh ${APP_HOME}/ +COPY build/docker/jmxtrans-agent.xml ${APP_HOME}/ + +ADD https://github.com/jmxtrans/jmxtrans-agent/releases/download/${JMXTRANS_AGENT}/${JMXTRANS_AGENT}.jar ${APP_HOME}/ + +WORKDIR ${APP_HOME} + +ENTRYPOINT ["./start-app.sh"] diff --git a/trends/span-timeseries-transformer/build/docker/jmxtrans-agent.xml b/trends/span-timeseries-transformer/build/docker/jmxtrans-agent.xml new file mode 100644 index 000000000..3504e8e07 --- /dev/null +++ b/trends/span-timeseries-transformer/build/docker/jmxtrans-agent.xml @@ -0,0 +1,115 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ${HAYSTACK_GRAPHITE_HOST:monitoring-influxdb-graphite.kube-system.svc} + ${HAYSTACK_GRAPHITE_PORT:2003} + ${HAYSTACK_GRAPHITE_ENABLED:false} + haystack.trends.span-transformer.#hostname#. + + 60 + diff --git a/trends/span-timeseries-transformer/build/docker/start-app.sh b/trends/span-timeseries-transformer/build/docker/start-app.sh new file mode 100755 index 000000000..58cf4cd31 --- /dev/null +++ b/trends/span-timeseries-transformer/build/docker/start-app.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +[ -z "$JAVA_XMS" ] && JAVA_XMS=1024m +[ -z "$JAVA_XMX" ] && JAVA_XMX=1024m + +set -e +JAVA_OPTS="${JAVA_OPTS} \ +-javaagent:${APP_HOME}/${JMXTRANS_AGENT}.jar=${APP_HOME}/jmxtrans-agent.xml \ +-XX:+UseG1GC \ +-Xloggc:/var/log/gc.log \ +-XX:+PrintGCDetails \ +-XX:+PrintGCDateStamps \ +-XX:+UseGCLogFileRotation \ +-XX:NumberOfGCLogFiles=5 \ +-XX:GCLogFileSize=2M \ +-Xmx${JAVA_XMX} \ +-Xms${JAVA_XMS} \ +-Dcom.sun.management.jmxremote.authenticate=false \ +-Dcom.sun.management.jmxremote.ssl=false \ +-Dcom.sun.management.jmxremote.port=1098 \ +-Dapplication.name=${APP_NAME} \ +-Dapplication.home=${APP_HOME}" + +exec java ${JAVA_OPTS} -jar "${APP_HOME}/${APP_NAME}.jar" diff --git a/trends/span-timeseries-transformer/pom.xml b/trends/span-timeseries-transformer/pom.xml new file mode 100644 index 000000000..e88a6ba27 --- /dev/null +++ b/trends/span-timeseries-transformer/pom.xml @@ -0,0 +1,113 @@ + + + + + + 4.0.0 + + haystack-span-timeseries-transformer + jar + haystack-span-timeseries-transformer + scala module which creates timeseries metricpoints for spans + + + com.expedia.www + haystack-trends + 1.0.0-SNAPSHOT + + + + + The Apache License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + + + + + com.expedia.www.haystack.trends.App + com.expedia.www.haystack.trends.feature.tests + com.expedia.www.haystack.trends.integration.tests + ${project.artifactId}-${project.version} + + + + + com.google.protobuf + protobuf-java + ${protobuf.version} + + + com.expedia.www + haystack-commons + + + com.expedia.www + haystack-logback-metrics-appender + + + org.apache.kafka + kafka-streams + + + org.msgpack + msgpack-core + + + com.typesafe + config + + + com.codahale.metrics + metrics-core + + + + + ${finalName} + + + src/main/resources + true + + + + + + org.apache.maven.plugins + maven-shade-plugin + + + com.github.os72 + protoc-jar-maven-plugin + + + org.scalatest + scalatest-maven-plugin + + + org.scalastyle + scalastyle-maven-plugin + + + + + diff --git a/trends/span-timeseries-transformer/src/main/resources/config/base.conf b/trends/span-timeseries-transformer/src/main/resources/config/base.conf new file mode 100644 index 000000000..48b0f6386 --- /dev/null +++ b/trends/span-timeseries-transformer/src/main/resources/config/base.conf @@ -0,0 +1,36 @@ +health.status.path = "/app/isHealthy" + +kafka { + close.timeout.ms = 30000 + + streams { + application.id = "haystack-span-timeseries-transformer" + bootstrap.servers = "kafkasvc:9092" + num.stream.threads = 4 + request.timeout.ms = 60000 + commit.interval.ms = 3000 + auto.offset.reset = latest + timestamp.extractor = "com.expedia.www.haystack.commons.kstreams.SpanTimestampExtractor" + } + + producer { + topic = "metric-data-points" + } + + consumer { + topic = "proto-spans" + } +} + +haystack.graphite.host = "monitoring-influxdb-graphite.kube-system.svc" + +// there are three types of encoders that are used on service and operation names: +// 1) periodreplacement: replaces all periods with 3 underscores +// 2) base64: base64 encodes the full name with a padding of _ +// 3) noop: does not perform any encoding +metricpoint.encoder.type = "periodreplacement" +enable.metricpoint.service.level.generation = true + +// List of Regex expressions used to filter out services from generating trends +blacklist.services = [ +] diff --git a/trends/span-timeseries-transformer/src/main/resources/logback.xml b/trends/span-timeseries-transformer/src/main/resources/logback.xml new file mode 100644 index 000000000..c45f62d7b --- /dev/null +++ b/trends/span-timeseries-transformer/src/main/resources/logback.xml @@ -0,0 +1,27 @@ + + + + + + + true + + + + + + %d{yyyy-MM-dd HH:mm:ss:SSS} %thread, %level, %logger{70}, "%msg" %replace(%ex){'[\n]+', '\\n'}%nopex%n + + + + + + ${HAYSTACK_LOG_QUEUE_SIZE:-500} + ${HAYSTACK_LOG_DISCARD_THRESHOLD:-0} + + + + + + + diff --git a/trends/span-timeseries-transformer/src/main/scala/com/expedia/www/haystack/trends/App.scala b/trends/span-timeseries-transformer/src/main/scala/com/expedia/www/haystack/trends/App.scala new file mode 100644 index 000000000..c5ad0e83f --- /dev/null +++ b/trends/span-timeseries-transformer/src/main/scala/com/expedia/www/haystack/trends/App.scala @@ -0,0 +1,72 @@ +/* + * + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trends + +import java.util.function.Supplier + +import com.expedia.www.haystack.commons.health.{HealthStatusController, UpdateHealthStatusFile} +import com.expedia.www.haystack.commons.kstreams.app.{Main, StateChangeListener, StreamsFactory, StreamsRunner} +import com.expedia.www.haystack.trends.config.AppConfiguration +import com.netflix.servo.util.VisibleForTesting +import org.apache.kafka.streams.Topology + +object App extends Main { + + /** + * Creates a valid instance of StreamsRunner. + * + * StreamsRunner is created with a valid StreamsFactory instance and a listener that observes + * state changes of the kstreams application. + * + * StreamsFactory in turn is created with a Topology Supplier and kafka.StreamsConfig. Any failure in + * StreamsFactory is gracefully handled by StreamsRunner to shut the application off + * + * Core logic of this application is in the `Streams` instance - which is a topology supplier. The + * topology of this application is built in this class. + * + * @return A valid instance of `StreamsRunner` + */ + + override def createStreamsRunner(): StreamsRunner = { + val appConfiguration = new AppConfiguration() + + val healthStatusController = new HealthStatusController + healthStatusController.addListener(new UpdateHealthStatusFile(appConfiguration.healthStatusFilePath)) + + val stateChangeListener = new StateChangeListener(healthStatusController) + + createStreamsRunner(appConfiguration, stateChangeListener) + } + + @VisibleForTesting + private[trends] def createStreamsRunner(appConfiguration: AppConfiguration, + stateChangeListener: StateChangeListener): StreamsRunner = { + //create the topology provider + val kafkaConfig = appConfiguration.kafkaConfig + val streams: Supplier[Topology] = new Streams(appConfiguration.kafkaConfig, appConfiguration.transformerConfiguration) + + val streamsFactory = new StreamsFactory(streams, kafkaConfig.streamsConfig, kafkaConfig.consumeTopic) + + new StreamsRunner(streamsFactory, stateChangeListener) + } +} + + + + diff --git a/trends/span-timeseries-transformer/src/main/scala/com/expedia/www/haystack/trends/MetricDataGenerator.scala b/trends/span-timeseries-transformer/src/main/scala/com/expedia/www/haystack/trends/MetricDataGenerator.scala new file mode 100644 index 000000000..12d096ef0 --- /dev/null +++ b/trends/span-timeseries-transformer/src/main/scala/com/expedia/www/haystack/trends/MetricDataGenerator.scala @@ -0,0 +1,74 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.trends + +import com.expedia.metrics.MetricData +import com.expedia.open.tracing.Span +import com.expedia.www.haystack.commons.entities.encoders.Encoder +import com.expedia.www.haystack.commons.metrics.MetricsSupport +import com.expedia.www.haystack.trends.transformer.MetricDataTransformer + +import scala.util.matching.Regex + +trait MetricDataGenerator extends MetricsSupport { + + private val SpanValidationErrors = metricRegistry.meter("span.validation.failure") + private val BlackListedSpans = metricRegistry.meter("span.validation.black.listed") + private val metricPointGenerationTimer = metricRegistry.timer("metricpoint.generation.time") + + /** + * This function is responsible for generating all the metric points which can be created given a span + * + * @param span incoming span + * @param transformers list of transformers to be applied + * @param encoder encoder object + * @param serviceOnlyFlag tells if metric data should be generated for serviceOnly, default is true + * @return + */ + def generateMetricDataList(span: Span, + transformers: Seq[MetricDataTransformer], + encoder: Encoder, + serviceOnlyFlag: Boolean = true): Seq[MetricData] = { + val timer = metricPointGenerationTimer.time() + val metricPoints = transformers.flatMap(transformer => transformer.mapSpan(span, serviceOnlyFlag, encoder)) + timer.close() + metricPoints + } + + /** + * This function validates a span and makes sure that the span has the necessary data to generate meaningful metrics + * This layer is supposed to do generic validations which would impact all the transformers. + * Validation specific to the transformer can be done in the transformer itself + * + * @param span incoming span + * @return Try object which should return either the span as is or a validation exception + */ + def isValidSpan(span: Span, blackListedServices: List[Regex]): Boolean = { + if (span.getServiceName.isEmpty || span.getOperationName.isEmpty) { + SpanValidationErrors.mark() + return false + } + + val isBlacklisted = blackListedServices.exists { + regexp => + regexp.pattern.matcher(span.getServiceName).find() + } + + if (isBlacklisted) BlackListedSpans.mark() + !isBlacklisted + } +} diff --git a/trends/span-timeseries-transformer/src/main/scala/com/expedia/www/haystack/trends/Streams.scala b/trends/span-timeseries-transformer/src/main/scala/com/expedia/www/haystack/trends/Streams.scala new file mode 100644 index 000000000..b9a717163 --- /dev/null +++ b/trends/span-timeseries-transformer/src/main/scala/com/expedia/www/haystack/trends/Streams.scala @@ -0,0 +1,69 @@ +/* + * + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trends + +import java.util.function.Supplier + +import com.expedia.metrics.MetricData +import com.expedia.open.tracing.Span +import com.expedia.www.haystack.commons.kstreams.serde.SpanSerde +import com.expedia.www.haystack.commons.kstreams.serde.metricdata.MetricTankSerde +import com.expedia.www.haystack.commons.util.MetricDefinitionKeyGenerator._ +import com.expedia.www.haystack.trends.config.entities.{KafkaConfiguration, TransformerConfiguration} +import com.expedia.www.haystack.trends.transformer.MetricDataTransformer.allTransformers +import org.apache.kafka.common.serialization.Serdes.StringSerde +import org.apache.kafka.streams._ +import org.apache.kafka.streams.kstream.Produced + +import scala.collection.JavaConverters._ + +class Streams(kafkaConfig: KafkaConfiguration, transformConfig: TransformerConfiguration) extends Supplier[Topology] + with MetricDataGenerator { + + private[trends] def initialize(builder: StreamsBuilder): Topology = { + val consumed = Consumed.`with`(kafkaConfig.autoOffsetReset) + .withKeySerde(new StringSerde) + .withValueSerde(new SpanSerde) + .withTimestampExtractor(kafkaConfig.timestampExtractor) + + builder + .stream(kafkaConfig.consumeTopic, consumed) + .filter((_: String, span: Span) => isValidSpan(span, transformConfig.blacklistedServices)) + .flatMap[String, MetricData]((_: String, span: Span) => mapToMetricDataKeyValue(span)) + .to(kafkaConfig.produceTopic, Produced.`with`(new StringSerde(), new MetricTankSerde())) + + builder.build() + } + + private def mapToMetricDataKeyValue(span: Span): java.lang.Iterable[KeyValue[String, MetricData]] = { + val metricData: Seq[MetricData] = generateMetricDataList(span, + allTransformers, + transformConfig.encoder, + transformConfig.enableMetricPointServiceLevelGeneration) + + metricData.map { + md => new KeyValue[String, MetricData](generateKey(md.getMetricDefinition), md) + }.asJavaCollection + } + + override def get(): Topology = { + val builder = new StreamsBuilder() + initialize(builder) + } +} diff --git a/trends/span-timeseries-transformer/src/main/scala/com/expedia/www/haystack/trends/config/AppConfiguration.scala b/trends/span-timeseries-transformer/src/main/scala/com/expedia/www/haystack/trends/config/AppConfiguration.scala new file mode 100644 index 000000000..0172267c9 --- /dev/null +++ b/trends/span-timeseries-transformer/src/main/scala/com/expedia/www/haystack/trends/config/AppConfiguration.scala @@ -0,0 +1,92 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.trends.config + +import java.util.Properties + +import com.expedia.www.haystack.commons.config.ConfigurationLoader +import com.expedia.www.haystack.commons.entities.encoders.EncoderFactory +import com.expedia.www.haystack.trends.config.entities.{KafkaConfiguration, TransformerConfiguration} +import com.typesafe.config.Config +import org.apache.kafka.streams.StreamsConfig +import org.apache.kafka.streams.Topology.AutoOffsetReset +import org.apache.kafka.streams.processor.TimestampExtractor + +import scala.collection.JavaConverters._ +import scala.util.matching.Regex + +class AppConfiguration { + private val config = ConfigurationLoader.loadConfigFileWithEnvOverrides() + + val healthStatusFilePath: String = config.getString("health.status.path") + + /** + * + * @return transformer related configs + */ + def transformerConfiguration: TransformerConfiguration = { + val encoderType = config.getString("metricpoint.encoder.type") + TransformerConfiguration(EncoderFactory.newInstance(encoderType), + config.getBoolean("enable.metricpoint.service.level.generation"), + config.getStringList("blacklist.services").asScala.toList.map(x => new Regex(x)) + ) + } + + /** + * + * @return streams configuration object + */ + def kafkaConfig: KafkaConfiguration = { + + // verify if the applicationId and bootstrap server config are non empty + def verifyRequiredProps(props: Properties): Unit = { + require(props.getProperty(StreamsConfig.APPLICATION_ID_CONFIG).nonEmpty) + require(props.getProperty(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG).nonEmpty) + } + + def addProps(config: Config, props: Properties, prefix: (String) => String = identity): Unit = { + config.entrySet().asScala.foreach(kv => { + val propKeyName = prefix(kv.getKey) + props.setProperty(propKeyName, kv.getValue.unwrapped().toString) + }) + } + + val kafka = config.getConfig("kafka") + val producerConfig = kafka.getConfig("producer") + val consumerConfig = kafka.getConfig("consumer") + val streamsConfig = kafka.getConfig("streams") + + val props = new Properties + + // add stream specific properties + addProps(streamsConfig, props) + + // validate props + verifyRequiredProps(props) + + val timestampExtractor = Class.forName(props.getProperty("timestamp.extractor", + "org.apache.kafka.streams.processor.WallclockTimestampExtractor")) + + KafkaConfiguration(new StreamsConfig(props), + produceTopic = producerConfig.getString("topic"), + consumeTopic = consumerConfig.getString("topic"), + if (streamsConfig.hasPath("auto.offset.reset")) AutoOffsetReset.valueOf(streamsConfig.getString("auto.offset.reset").toUpperCase) + else AutoOffsetReset.LATEST + , timestampExtractor.newInstance().asInstanceOf[TimestampExtractor], + kafka.getLong("close.timeout.ms")) + } +} diff --git a/trends/span-timeseries-transformer/src/main/scala/com/expedia/www/haystack/trends/config/entities/KafkaConfiguration.scala b/trends/span-timeseries-transformer/src/main/scala/com/expedia/www/haystack/trends/config/entities/KafkaConfiguration.scala new file mode 100644 index 000000000..ff2efc96a --- /dev/null +++ b/trends/span-timeseries-transformer/src/main/scala/com/expedia/www/haystack/trends/config/entities/KafkaConfiguration.scala @@ -0,0 +1,36 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.trends.config.entities + +import org.apache.kafka.streams.StreamsConfig +import org.apache.kafka.streams.Topology.AutoOffsetReset +import org.apache.kafka.streams.processor.TimestampExtractor + +/** + * @param streamsConfig config object to be used for initializing KafkaStreams + * @param produceTopic producer topic + * @param consumeTopic consumer topic + * @param autoOffsetReset auto offset reset policy + * @param timestampExtractor timestamp extractor + * @param closeTimeoutInMs timeout for closing kafka streams in ms + */ +case class KafkaConfiguration(streamsConfig: StreamsConfig, + produceTopic: String, + consumeTopic: String, + autoOffsetReset: AutoOffsetReset, + timestampExtractor: TimestampExtractor, + closeTimeoutInMs: Long) diff --git a/trends/span-timeseries-transformer/src/main/scala/com/expedia/www/haystack/trends/config/entities/TransformerConfiguration.scala b/trends/span-timeseries-transformer/src/main/scala/com/expedia/www/haystack/trends/config/entities/TransformerConfiguration.scala new file mode 100644 index 000000000..5943178c6 --- /dev/null +++ b/trends/span-timeseries-transformer/src/main/scala/com/expedia/www/haystack/trends/config/entities/TransformerConfiguration.scala @@ -0,0 +1,29 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.trends.config.entities + +import com.expedia.www.haystack.commons.entities.encoders.Encoder + +import scala.util.matching.Regex + +/** + * @param encoder config for encoder type in metric point key + * @param enableMetricPointServiceLevelGeneration config for also generating service level trends + */ +case class TransformerConfiguration(encoder: Encoder, + enableMetricPointServiceLevelGeneration: Boolean, + blacklistedServices: List[Regex]) diff --git a/trends/span-timeseries-transformer/src/main/scala/com/expedia/www/haystack/trends/transformer/MetricDataTransformer.scala b/trends/span-timeseries-transformer/src/main/scala/com/expedia/www/haystack/trends/transformer/MetricDataTransformer.scala new file mode 100644 index 000000000..7caf35839 --- /dev/null +++ b/trends/span-timeseries-transformer/src/main/scala/com/expedia/www/haystack/trends/transformer/MetricDataTransformer.scala @@ -0,0 +1,84 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.trends.transformer + +import java.util + +import com.expedia.metrics.{MetricData, MetricDefinition, TagCollection} +import com.expedia.open.tracing.Span +import com.expedia.www.haystack.commons.entities.TagKeys._ +import com.expedia.www.haystack.commons.entities.encoders.{Encoder, PeriodReplacementEncoder} +import com.expedia.www.haystack.commons.metrics.MetricsSupport + + +trait MetricDataTransformer extends MetricsSupport { + + protected val PRODUCT = "haystack" + protected var encoder: Encoder = new PeriodReplacementEncoder + + def mapSpan(span: Span, serviceOnlyFlag: Boolean, encoder: Encoder): List[MetricData] = { + this.encoder = encoder + mapSpan(span, serviceOnlyFlag) + } + + protected def mapSpan(span: Span, serviceOnlyFlag: Boolean): List[MetricData] + + protected def getDataPointTimestamp(span: Span): Long = span.getStartTime / 1000000 + + protected def getMetricData(metricName: String, + metricTags: util.LinkedHashMap[String, String], + metricType: String, + metricUnit: String, + value: Double, + timestamp: Long): MetricData = { + val tags = new util.LinkedHashMap[String, String] { + putAll(metricTags) + put(MetricDefinition.MTYPE, metricType) + put(MetricDefinition.UNIT, metricUnit) + put(PRODUCT_KEY, PRODUCT) + } + val metricDefinition = new MetricDefinition(metricName, new TagCollection(tags), TagCollection.EMPTY) + val metricData = new MetricData(metricDefinition, value, timestamp) + metricData + } + + /** + * This function creates the common metric tags from a span object. + * Every metric point must have the operationName and ServiceName in its tags, the individual transformer + * can add more tags to the metricPoint. + * + * @param span incoming span + * @return metric tags in the form of HashMap of string,string + */ + protected def createCommonMetricTags(span: Span): util.LinkedHashMap[String, String] = { + new util.LinkedHashMap[String, String] { + put(SERVICE_NAME_KEY, encoder.encode(span.getServiceName)) + put(OPERATION_NAME_KEY, encoder.encode(span.getOperationName)) + } + } + + protected def createServiceOnlyMetricTags(span: Span): util.LinkedHashMap[String, String] = { + new util.LinkedHashMap[String, String] { + put(SERVICE_NAME_KEY, encoder.encode(span.getServiceName)) + } + } +} + +object MetricDataTransformer { + val allTransformers = List(SpanDurationMetricDataTransformer, SpanStatusMetricDataTransformer) +} + diff --git a/trends/span-timeseries-transformer/src/main/scala/com/expedia/www/haystack/trends/transformer/SpanDurationMetricDataTransformer.scala b/trends/span-timeseries-transformer/src/main/scala/com/expedia/www/haystack/trends/transformer/SpanDurationMetricDataTransformer.scala new file mode 100644 index 000000000..5005b8121 --- /dev/null +++ b/trends/span-timeseries-transformer/src/main/scala/com/expedia/www/haystack/trends/transformer/SpanDurationMetricDataTransformer.scala @@ -0,0 +1,47 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.trends.transformer + +import com.expedia.metrics.MetricData +import com.expedia.open.tracing.Span + +/** + * This Transformer reads a span and creates a duration metric point with the value as the + */ +trait SpanDurationMetricDataTransformer extends MetricDataTransformer { + + private val spanDurationMetricPoints = metricRegistry.meter("metricpoint.span.duration") + + val DURATION_METRIC_NAME = "duration" + val MTYPE = "gauge" + val UNIT = "microseconds" + + override def mapSpan(span: Span, serviceOnlyFlag: Boolean): List[MetricData] = { + spanDurationMetricPoints.mark() + + var metricDataList = List(getMetricData(DURATION_METRIC_NAME, createCommonMetricTags(span), MTYPE, UNIT, span.getDuration, getDataPointTimestamp(span))) + if (serviceOnlyFlag) { + metricDataList = metricDataList :+ + getMetricData(DURATION_METRIC_NAME, createServiceOnlyMetricTags(span), MTYPE, UNIT, span.getDuration, getDataPointTimestamp(span)) + } + metricDataList + } + +} + +object SpanDurationMetricDataTransformer extends SpanDurationMetricDataTransformer + diff --git a/trends/span-timeseries-transformer/src/main/scala/com/expedia/www/haystack/trends/transformer/SpanStatusMetricDataTransformer.scala b/trends/span-timeseries-transformer/src/main/scala/com/expedia/www/haystack/trends/transformer/SpanStatusMetricDataTransformer.scala new file mode 100644 index 000000000..e0440ae93 --- /dev/null +++ b/trends/span-timeseries-transformer/src/main/scala/com/expedia/www/haystack/trends/transformer/SpanStatusMetricDataTransformer.scala @@ -0,0 +1,71 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.trends.transformer + +import com.expedia.metrics.MetricData +import com.expedia.open.tracing.Span +import com.expedia.open.tracing.Tag.TagType +import com.expedia.www.haystack.commons.entities.TagKeys + +import scala.collection.JavaConverters._ + + +/** + * This transformer generates a success or a failure metric y + */ +trait SpanStatusMetricDataTransformer extends MetricDataTransformer { + private val spanSuccessMetricPoints = metricRegistry.meter("metricpoint.span.success") + private val spanFailuresMetricPoints = metricRegistry.meter("metricpoint.span.failure") + + val SUCCESS_METRIC_NAME = "success-span" + val FAILURE_METRIC_NAME = "failure-span" + val MTYPE = "gauge" + val UNIT = "short" + + override def mapSpan(span: Span, serviceOnlyFlag: Boolean): List[MetricData] = { + var metricName: String = null + + if (isError(span)) { + spanFailuresMetricPoints.mark() + metricName = FAILURE_METRIC_NAME + } else { + spanSuccessMetricPoints.mark() + metricName = SUCCESS_METRIC_NAME + } + + var metricDataList = List(getMetricData(metricName, createCommonMetricTags(span), MTYPE, UNIT, 1, getDataPointTimestamp(span))) + + if (serviceOnlyFlag) { + metricDataList = metricDataList :+ getMetricData(metricName, createServiceOnlyMetricTags(span), MTYPE, UNIT, 1, getDataPointTimestamp(span)) + } + metricDataList + } + + protected def isError(span: Span): Boolean = { + val value = span.getTagsList.asScala.find(tag => tag.getKey.equalsIgnoreCase(TagKeys.ERROR_KEY)).map(x => { + if (TagType.BOOL == x.getType) { + return x.getVBool + } else if (TagType.STRING == x.getType) { + return !"false".equalsIgnoreCase(x.getVStr) + } + return true + }) + value.getOrElse(false) + } +} + +object SpanStatusMetricDataTransformer extends SpanStatusMetricDataTransformer diff --git a/trends/span-timeseries-transformer/src/test/resources/config/base.conf b/trends/span-timeseries-transformer/src/test/resources/config/base.conf new file mode 100644 index 000000000..5ed779efd --- /dev/null +++ b/trends/span-timeseries-transformer/src/test/resources/config/base.conf @@ -0,0 +1,25 @@ +health.status.path = "/app/isHealthy" + +kafka { + close.timeout.ms = 30000 + + streams { + application.id = "haystack-span-timeseries-transformer-v2" + bootstrap.servers = "kafkasvc:9092" + num.stream.threads = 4 + commit.interval.ms = 3000 + auto.offset.reset = latest + timestamp.extractor = "com.expedia.www.haystack.commons.kstreams.SpanTimestampExtractor" + } + + producer { + topic = "metric-data-points" + } + + consumer { + topic = "proto-spans" + } +} + +metricpoint.encoder.type = "periodreplacement" +enable.metricpoint.service.level.generation=true diff --git a/trends/span-timeseries-transformer/src/test/resources/logback-test.xml b/trends/span-timeseries-transformer/src/test/resources/logback-test.xml new file mode 100644 index 000000000..adfa02c68 --- /dev/null +++ b/trends/span-timeseries-transformer/src/test/resources/logback-test.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/trends/span-timeseries-transformer/src/test/scala/com/expedia/www/haystack/trends/feature/FeatureSpec.scala b/trends/span-timeseries-transformer/src/test/scala/com/expedia/www/haystack/trends/feature/FeatureSpec.scala new file mode 100644 index 000000000..b91e9087e --- /dev/null +++ b/trends/span-timeseries-transformer/src/test/scala/com/expedia/www/haystack/trends/feature/FeatureSpec.scala @@ -0,0 +1,69 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.trends.feature + +import java._ +import java.util.Properties + +import com.expedia.metrics.MetricData +import com.expedia.open.tracing.Span +import com.expedia.www.haystack.commons.entities.encoders.Base64Encoder +import com.expedia.www.haystack.trends.config.AppConfiguration +import com.expedia.www.haystack.trends.config.entities.{KafkaConfiguration, TransformerConfiguration} +import org.apache.kafka.streams.StreamsConfig +import org.easymock.EasyMock +import org.scalatest.easymock.EasyMockSugar +import org.scalatest.{FeatureSpecLike, GivenWhenThen, Matchers} + + +trait FeatureSpec extends FeatureSpecLike with GivenWhenThen with Matchers with EasyMockSugar { + + protected val METRIC_TYPE = "gauge" + + def generateTestSpan(duration: Long): Span = { + val operationName = "testSpan" + val serviceName = "testService" + Span.newBuilder() + .setDuration(duration) + .setOperationName(operationName) + .setServiceName(serviceName) + .build() + } + + protected def mockAppConfig: AppConfiguration = { + val kafkaConsumeTopic = "test-consume" + val kafkaProduceTopic = "test-produce" + val streamsConfig = new Properties() + streamsConfig.put(StreamsConfig.APPLICATION_ID_CONFIG, "test-app") + streamsConfig.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, "test-kafka-broker") + val kafkaConfig = KafkaConfiguration(new StreamsConfig(streamsConfig), kafkaProduceTopic, kafkaConsumeTopic, null, null, 0l) + val transformerConfig = TransformerConfiguration(new Base64Encoder, enableMetricPointServiceLevelGeneration = true, List()) + val appConfiguration = mock[AppConfiguration] + + expecting { + appConfiguration.kafkaConfig.andReturn(kafkaConfig).anyTimes() + appConfiguration.transformerConfiguration.andReturn(transformerConfig).anyTimes() + } + EasyMock.replay(appConfiguration) + appConfiguration + } + + protected def getMetricDataTags(metricData : MetricData): util.Map[String, String] = { + metricData.getMetricDefinition.getTags.getKv + } + +} diff --git a/trends/span-timeseries-transformer/src/test/scala/com/expedia/www/haystack/trends/feature/tests/config/ConfigurationLoaderSpec.scala b/trends/span-timeseries-transformer/src/test/scala/com/expedia/www/haystack/trends/feature/tests/config/ConfigurationLoaderSpec.scala new file mode 100644 index 000000000..8225382fb --- /dev/null +++ b/trends/span-timeseries-transformer/src/test/scala/com/expedia/www/haystack/trends/feature/tests/config/ConfigurationLoaderSpec.scala @@ -0,0 +1,83 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trends.feature.tests.config + +import com.expedia.www.haystack.commons.entities.encoders.PeriodReplacementEncoder +import com.expedia.www.haystack.trends.config.AppConfiguration +import com.expedia.www.haystack.trends.feature.FeatureSpec + +class ConfigurationLoaderSpec extends FeatureSpec { + + feature("Configuration loader") { + + + scenario("should load the health status config from base.conf") { + + Given("A config file at base config file containing config for health status file path") + val healthStatusFilePath = "/app/isHealthy" + + When("When the configuration is loaded in app configuration") + val projectConfig = new AppConfiguration() + + Then("the healthStatusFilePath should be correct") + projectConfig.healthStatusFilePath shouldEqual healthStatusFilePath + } + + scenario("should load the metric point enable period replacement config from base.conf") { + + Given("A config file at base config file containing config for enable period replacement") + val enableMetricPointServiceLevelGeneration = true + + When("When the configuration is loaded in app configuration") + val projectConfig = new AppConfiguration() + + Then("the encoder should be correct") + projectConfig.transformerConfiguration.encoder shouldBe an[PeriodReplacementEncoder] + projectConfig.transformerConfiguration.enableMetricPointServiceLevelGeneration shouldEqual enableMetricPointServiceLevelGeneration + } + + scenario("should load the kafka config from base.conf") { + + Given("A config file at base config file containing kafka ") + + When("When the configuration is loaded in app configuration") + val projectConfig = new AppConfiguration() + + Then("It should create the write configuration object based on the file contents") + val kafkaConfig = projectConfig.kafkaConfig + kafkaConfig.consumeTopic shouldBe "proto-spans" + } + + + scenario("should override configuration based on environment variable") { + + + Given("A config file at base config file containing config for kafka") + + When("When the configuration is loaded in app configuration") + val projectConfig = new AppConfiguration() + + Then("It should override the configuration object based on the environment variable if it exists") + + val kafkaProduceTopic = sys.env.getOrElse("HAYSTACK_PROP_KAFKA_PRODUCER_TOPIC", "metric-data-points") + val kafkaConfig = projectConfig.kafkaConfig + kafkaConfig.produceTopic shouldBe kafkaProduceTopic + } + + } +} diff --git a/trends/span-timeseries-transformer/src/test/scala/com/expedia/www/haystack/trends/feature/tests/kstreams/StreamsSpec.scala b/trends/span-timeseries-transformer/src/test/scala/com/expedia/www/haystack/trends/feature/tests/kstreams/StreamsSpec.scala new file mode 100644 index 000000000..751ad7b3c --- /dev/null +++ b/trends/span-timeseries-transformer/src/test/scala/com/expedia/www/haystack/trends/feature/tests/kstreams/StreamsSpec.scala @@ -0,0 +1,31 @@ +package com.expedia.www.haystack.trends.feature.tests.kstreams + +import com.expedia.www.haystack.trends.Streams +import com.expedia.www.haystack.trends.feature.FeatureSpec +import org.apache.kafka.streams.StreamsBuilder + + +class StreamsSpec extends FeatureSpec { + + feature("Streams should build a topology") { + + scenario("a valid kafka configuration") { + + Given("an valid kafka configuration") + + val appConfig = mockAppConfig + val streams = new Streams(appConfig.kafkaConfig, appConfig.transformerConfiguration) + val streamBuilder = mock[StreamsBuilder] + + + When("the stream topology is built") + val topology = streams.get() + + Then("it should be able to build a successful topology") + topology should not be null + + Then("then it should return an empty state store") + topology.describe().globalStores().isEmpty shouldBe true + } + } +} diff --git a/trends/span-timeseries-transformer/src/test/scala/com/expedia/www/haystack/trends/feature/tests/transformer/MetricDataGeneratorSpec.scala b/trends/span-timeseries-transformer/src/test/scala/com/expedia/www/haystack/trends/feature/tests/transformer/MetricDataGeneratorSpec.scala new file mode 100644 index 000000000..43e92a40f --- /dev/null +++ b/trends/span-timeseries-transformer/src/test/scala/com/expedia/www/haystack/trends/feature/tests/transformer/MetricDataGeneratorSpec.scala @@ -0,0 +1,167 @@ +/* + * + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.trends.feature.tests.transformer + +import com.expedia.metrics.MetricDefinition +import com.expedia.open.tracing.{Span, Tag} +import com.expedia.www.haystack.commons.entities.TagKeys +import com.expedia.www.haystack.commons.entities.encoders.PeriodReplacementEncoder +import com.expedia.www.haystack.trends.MetricDataGenerator +import com.expedia.www.haystack.trends.feature.FeatureSpec +import com.expedia.www.haystack.trends.transformer.{SpanDurationMetricDataTransformer, SpanStatusMetricDataTransformer} + +import scala.collection.JavaConverters._ +import scala.util.matching.Regex + + +class MetricDataGeneratorSpec extends FeatureSpec with MetricDataGenerator { + + private def getMetricDataTransformers = { + List(SpanDurationMetricDataTransformer, SpanStatusMetricDataTransformer) + } + + feature("The metricData generator must generate list of metricData given a span object") { + + scenario("any valid span object") { + val operationName = "testSpan" + val serviceName = "testService" + Given("a valid span") + val span = Span.newBuilder() + .setDuration(System.currentTimeMillis()) + .setOperationName(operationName) + .setServiceName(serviceName) + .setStartTime(System.currentTimeMillis() * 1000) // in micro seconds + .addTags(Tag.newBuilder().setKey(TagKeys.ERROR_KEY).setVBool(false)) + .build() + When("its asked to map to metricPoints") + val isValid = isValidSpan(span, Nil) + val metricDataList = generateMetricDataList(span, getMetricDataTransformers, new PeriodReplacementEncoder) + + Then("the number of metricPoints returned should be equal to the number of metricPoint transformers") + metricDataList should not be empty + val metricPointTransformers = getMetricDataTransformers + metricDataList.size shouldEqual metricPointTransformers.size * 2 + + Then("each metricPoint should have the timestamps in seconds and which should equal to the span timestamp") + isValid shouldBe true + metricDataList.foreach(metricData => { + metricData.getTimestamp shouldEqual span.getStartTime / 1000000 + }) + + Then("each metricPoint should have the metric type as Metric") + metricDataList.foreach(metricData => { + getMetricDataTags(metricData).get(MetricDefinition.MTYPE) shouldEqual METRIC_TYPE + }) + + } + + scenario("an invalid span object") { + val operationName = "" + val serviceName = "" + Given("an invalid span") + val span = Span.newBuilder() + .setDuration(System.currentTimeMillis()) + .setOperationName(operationName) + .setServiceName(serviceName) + .addTags(Tag.newBuilder().setKey(TagKeys.ERROR_KEY).setVBool(false)) + .build() + + When("its asked to map to metricPoints") + val isValid = isValidSpan(span, Nil) + Then("It should return a metricPoint validation exception") + isValid shouldBe false + metricRegistry.meter("span.validation.failure").getCount shouldBe 1 + } + + scenario("a span object with a valid service Name") { + val operationName = "testSpan" + val serviceName = "testService" + + Given("a valid span") + val span = Span.newBuilder() + .setDuration(System.currentTimeMillis()) + .setOperationName(operationName) + .setServiceName(serviceName) + .addTags(Tag.newBuilder().setKey(TagKeys.ERROR_KEY).setVBool(false)) + .build() + val encoder = new PeriodReplacementEncoder + + When("its asked to map to metricPoints") + val isValid = isValidSpan(span, Nil) + val metricDataList = generateMetricDataList(span, getMetricDataTransformers, encoder) + + Then("it should create metricPoints with service name as one its keys") + isValid shouldBe true + metricDataList.map(metricData => { + val tags = getMetricDataTags(metricData).asScala + tags.get(TagKeys.SERVICE_NAME_KEY) should not be None + tags.get(TagKeys.SERVICE_NAME_KEY) shouldEqual Some(encoder.encode(serviceName)) + }) + } + + scenario("a span object with a blacklisted service Name") { + val operationName = "testSpan" + val blacklistedServiceName = "testService" + + Given("a valid span with a blacklisted service name") + val span = Span.newBuilder() + .setDuration(System.currentTimeMillis()) + .setOperationName(operationName) + .setServiceName(blacklistedServiceName) + .addTags(Tag.newBuilder().setKey(TagKeys.ERROR_KEY).setVBool(false)) + .build() + + When("its asked to map to metricPoints") + val isValid = isValidSpan(span, List(new Regex(blacklistedServiceName))) + Then("It should return a metricPoint validation exception") + + isValid shouldBe false + metricRegistry.meter("span.validation.black.listed").getCount shouldBe 1 + } + + scenario("a span object with a blacklisted regex service Name") { + val serviceName = "testservice" + + Given("a valid span with a blacklisted service name") + val span = Span.newBuilder().setDuration(System.currentTimeMillis()).setOperationName("testSpan").setServiceName(serviceName) + .addTags(Tag.newBuilder().setKey(TagKeys.ERROR_KEY).setVBool(false)).build() + + When("its asked to map to metricPoints") + val isValid = isValidSpan(span, List(new Regex("^[a-z]*$"))) + + Then("It should return a metricPoint") + isValid shouldBe false + } + + scenario("a span object with a non-blacklisted regex service Name") { + val serviceName = "testService" + + Given("a valid span with a blacklisted service name") + val span = Span.newBuilder().setDuration(System.currentTimeMillis()).setOperationName("testSpan").setServiceName(serviceName) + .addTags(Tag.newBuilder().setKey(TagKeys.ERROR_KEY).setVBool(false)).build() + + When("its asked to map to metricPoints") + val isValid = isValidSpan(span, List(new Regex("^[a-z]*"))) + val metricDataList = generateMetricDataList(span, getMetricDataTransformers, new PeriodReplacementEncoder, serviceOnlyFlag = false) + + Then("It should return a metricPoint validation exception") + isValid shouldBe false + metricDataList should not be empty + } + } +} diff --git a/trends/span-timeseries-transformer/src/test/scala/com/expedia/www/haystack/trends/feature/tests/transformer/SpanDurationMetricDataTransformerSpec.scala b/trends/span-timeseries-transformer/src/test/scala/com/expedia/www/haystack/trends/feature/tests/transformer/SpanDurationMetricDataTransformerSpec.scala new file mode 100644 index 000000000..30e2d447c --- /dev/null +++ b/trends/span-timeseries-transformer/src/test/scala/com/expedia/www/haystack/trends/feature/tests/transformer/SpanDurationMetricDataTransformerSpec.scala @@ -0,0 +1,81 @@ +/* + * + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.trends.feature.tests.transformer + +import com.expedia.www.haystack.commons.entities.TagKeys +import com.expedia.www.haystack.commons.entities.encoders.PeriodReplacementEncoder +import com.expedia.www.haystack.trends.feature.FeatureSpec +import com.expedia.www.haystack.trends.transformer.SpanDurationMetricDataTransformer + +class SpanDurationMetricDataTransformerSpec extends FeatureSpec with SpanDurationMetricDataTransformer { + + feature("metricData transformer for creating duration metricData") { + scenario("should have duration value in metricData for given duration in span " + + "and when service level generation is enabled") { + + Given("a valid span object") + val duration = System.currentTimeMillis + val span = generateTestSpan(duration) + + When("metricPoint is created using transformer") + val metricDataList = mapSpan(span, true) + + Then("should only have 2 metricPoint") + metricDataList.length shouldEqual 2 + + Then("same duration should be in metricPoint value") + metricDataList.head.getValue shouldEqual duration + + + Then("the metric name should be duration") + metricDataList.head.getMetricDefinition.getKey shouldEqual DURATION_METRIC_NAME + + Then("returned keys should be as expected") + getMetricDataTags(metricDataList.head).get(TagKeys.SERVICE_NAME_KEY) shouldEqual encoder.encode(span.getServiceName) + getMetricDataTags(metricDataList.head).get(TagKeys.OPERATION_NAME_KEY) shouldEqual encoder.encode(span.getOperationName) + getMetricDataTags(metricDataList.reverse.head).get(TagKeys.SERVICE_NAME_KEY) shouldEqual encoder.encode(span.getServiceName) + getMetricDataTags(metricDataList.reverse.head).get(TagKeys.OPERATION_NAME_KEY) shouldEqual null + + } + + scenario("should have duration value in metricPoint for given duration in span " + + "and when service level generation is disabled") { + + Given("a valid span object") + val duration = System.currentTimeMillis + val span = generateTestSpan(duration) + + When("metricData is created using transformer") + val metricDataList = mapSpan(span, false) + + Then("should only have 1 metricPoint") + metricDataList.length shouldEqual 1 + + Then("same duration should be in metricPoint value") + metricDataList.head.getValue shouldEqual duration + + + Then("the metric name should be duration") + metricDataList.head.getMetricDefinition.getKey shouldEqual DURATION_METRIC_NAME + + Then("returned keys should be as expected") + getMetricDataTags(metricDataList.head).get(TagKeys.SERVICE_NAME_KEY) shouldEqual encoder.encode(span.getServiceName) + getMetricDataTags(metricDataList.head).get(TagKeys.OPERATION_NAME_KEY) shouldEqual encoder.encode(span.getOperationName) + } + } +} diff --git a/trends/span-timeseries-transformer/src/test/scala/com/expedia/www/haystack/trends/feature/tests/transformer/SpanStatusMetricDataTransformerSpec.scala b/trends/span-timeseries-transformer/src/test/scala/com/expedia/www/haystack/trends/feature/tests/transformer/SpanStatusMetricDataTransformerSpec.scala new file mode 100644 index 000000000..7e8e76786 --- /dev/null +++ b/trends/span-timeseries-transformer/src/test/scala/com/expedia/www/haystack/trends/feature/tests/transformer/SpanStatusMetricDataTransformerSpec.scala @@ -0,0 +1,248 @@ +/* + * + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.trends.feature.tests.transformer + +import com.expedia.open.tracing.Tag.TagType +import com.expedia.open.tracing.{Span, Tag} +import com.expedia.www.haystack.commons.entities.TagKeys +import com.expedia.www.haystack.trends.feature.FeatureSpec +import com.expedia.www.haystack.trends.transformer.SpanStatusMetricDataTransformer + +class SpanStatusMetricDataTransformerSpec extends FeatureSpec with SpanStatusMetricDataTransformer { + + feature("metricData transformer for creating status count metricData") { + + scenario("should have a success-spans metricData given span which is successful " + + "and when service level generation is enabled") { + + Given("a successful span object") + val operationName = "testSpan" + val serviceName = "testService" + val duration = System.currentTimeMillis + val span = Span.newBuilder() + .setDuration(duration) + .setOperationName(operationName) + .setServiceName(serviceName) + .addTags(Tag.newBuilder().setKey(TagKeys.ERROR_KEY).setType(TagType.BOOL).setVBool(false)) + .build() + + When("metricData is created using the transformer") + val metricDataList = mapSpan(span, true) + + Then("should only have 2 metricData") + metricDataList.length shouldEqual 2 + + Then("the metricData value should be 1") + metricDataList(0).getValue shouldEqual 1 + metricDataList(1).getValue shouldEqual 1 + + Then("metric name should be success-spans") + metricDataList(0).getMetricDefinition.getKey shouldEqual SUCCESS_METRIC_NAME + + Then("returned keys should be as expected") + getMetricDataTags(metricDataList.head).get(TagKeys.SERVICE_NAME_KEY) shouldEqual encoder.encode(span.getServiceName) + getMetricDataTags(metricDataList.head).get(TagKeys.OPERATION_NAME_KEY) shouldEqual encoder.encode(span.getOperationName) + getMetricDataTags(metricDataList.reverse.head).get(TagKeys.SERVICE_NAME_KEY) shouldEqual encoder.encode(span.getServiceName) + getMetricDataTags(metricDataList.reverse.head).get(TagKeys.OPERATION_NAME_KEY) shouldEqual null + } + + scenario("should have a failure-spans metricData given span which is erroneous " + + "and when service level generation is enabled") { + + Given("a erroneous span object") + val operationName = "testSpan" + val serviceName = "testService" + val duration = System.currentTimeMillis + val span = Span.newBuilder() + .setDuration(duration) + .setOperationName(operationName) + .setServiceName(serviceName) + .addTags(Tag.newBuilder().setKey(TagKeys.ERROR_KEY).setType(TagType.BOOL).setVBool(true)) + .build() + + When("metricData is created using transformer") + val metricDataList = mapSpan(span, true) + + Then("should only have 2 metricData") + metricDataList.length shouldEqual 2 + + Then("the metricData value should be 1") + metricDataList(0).getValue shouldEqual 1 + metricDataList(1).getValue shouldEqual 1 + + Then("metric name should be failure-spans") + metricDataList(0).getMetricDefinition.getKey shouldEqual FAILURE_METRIC_NAME + } + + scenario("should have a failure-span metricData if the error tag is a true string") { + Given("a failure span object") + val operationName = "testSpan" + val serviceName = "testService" + val duration = System.currentTimeMillis + val span = Span.newBuilder() + .setDuration(duration) + .setOperationName(operationName) + .setServiceName(serviceName) + .addTags(Tag.newBuilder().setKey(TagKeys.ERROR_KEY).setVStr("true")) + .build() + + When("metricData is created using transformer") + val metricDataList = mapSpan(span, true) + + Then("metric name should be failure-spans") + metricDataList(0).getMetricDefinition.getKey shouldEqual FAILURE_METRIC_NAME + } + + scenario("should have a failure-span metricData if the error tag not a false string") { + Given("a failure span object") + val operationName = "testSpan" + val serviceName = "testService" + val duration = System.currentTimeMillis + val span = Span.newBuilder() + .setDuration(duration) + .setOperationName(operationName) + .setServiceName(serviceName) + .addTags(Tag.newBuilder().setKey(TagKeys.ERROR_KEY).setVStr("500")) + .build() + + When("metricData is created using transformer") + val metricDataList = mapSpan(span, true) + + Then("metric name should be failure-spans") + metricDataList(0).getMetricDefinition.getKey shouldEqual FAILURE_METRIC_NAME + } + + scenario("should have a failure-span metricData if the error tag exists but is not a boolean or string") { + Given("a failure span object") + val operationName = "testSpan" + val serviceName = "testService" + val duration = System.currentTimeMillis + val span = Span.newBuilder() + .setDuration(duration) + .setOperationName(operationName) + .setServiceName(serviceName) + .addTags(Tag.newBuilder().setKey(TagKeys.ERROR_KEY).setType(TagType.LONG).setVLong(100L)) + .build() + + When("metricData is created using transformer") + val metricDataList = mapSpan(span, true) + + Then("metric name should be failure-spans") + metricDataList(0).getMetricDefinition.getKey shouldEqual FAILURE_METRIC_NAME + } + + scenario("should return a success span when error key is missing in span tags and when service level generation is enabled") { + + Given("a span object which missing error tag") + val operationName = "testSpan" + val serviceName = "testService" + val duration = System.currentTimeMillis + val span = Span.newBuilder() + .setDuration(duration) + .setOperationName(operationName) + .setServiceName(serviceName) + .build() + + When("metricData is created using transformer") + val metricDataList = mapSpan(span, true) + + Then("should return metricData List") + metricDataList.length shouldEqual 2 + metricDataList(0).getMetricDefinition.getKey shouldEqual SUCCESS_METRIC_NAME + metricDataList(1).getMetricDefinition.getKey shouldEqual SUCCESS_METRIC_NAME + } + + scenario("should have a success-spans metricData given span which is successful " + + "and when service level generation is disabled") { + + Given("a successful span object") + val operationName = "testSpan" + val serviceName = "testService" + val duration = System.currentTimeMillis + val span = Span.newBuilder() + .setDuration(duration) + .setOperationName(operationName) + .setServiceName(serviceName) + .addTags(Tag.newBuilder().setKey(TagKeys.ERROR_KEY).setType(TagType.BOOL).setVBool(false)) + .build() + + When("metricData is created using the transformer") + val metricDataList = mapSpan(span, false) + + Then("should only have 1 metricData") + metricDataList.length shouldEqual 1 + + Then("the metricData value should be 1") + metricDataList(0).getValue shouldEqual 1 + + Then("metric name should be success-spans") + metricDataList(0).getMetricDefinition.getKey shouldEqual SUCCESS_METRIC_NAME + + Then("returned keys should be as expected") + getMetricDataTags(metricDataList.head).get(TagKeys.SERVICE_NAME_KEY) shouldEqual encoder.encode(span.getServiceName) + getMetricDataTags(metricDataList.head).get(TagKeys.OPERATION_NAME_KEY) shouldEqual encoder.encode(span.getOperationName) + } + + scenario("should have a failure-spans metricData given span which is erroneous " + + "and when service level generation is disabled") { + + Given("a erroneous span object") + val operationName = "testSpan" + val serviceName = "testService" + val duration = System.currentTimeMillis + val span = Span.newBuilder() + .setDuration(duration) + .setOperationName(operationName) + .setServiceName(serviceName) + .addTags(Tag.newBuilder().setKey(TagKeys.ERROR_KEY).setType(TagType.BOOL).setVBool(true)) + .build() + + When("metricData is created using transformer") + val metricDataList = mapSpan(span, false) + + Then("should only have 1 metricData") + metricDataList.length shouldEqual 1 + + Then("the metricData value should be 1") + metricDataList(0).getValue shouldEqual 1 + + Then("metric name should be failure-spans") + metricDataList(0).getMetricDefinition.getKey shouldEqual FAILURE_METRIC_NAME + } + + scenario("should return a success span when error key is missing in span tags and when service level generation is disabled") { + + Given("a span object which missing error tag") + val operationName = "testSpan" + val serviceName = "testService" + val duration = System.currentTimeMillis + val span = Span.newBuilder() + .setDuration(duration) + .setOperationName(operationName) + .setServiceName(serviceName) + .build() + + When("metricData is created using transformer") + val metricPoints = mapSpan(span, false) + + Then("should return metricData") + metricPoints.length shouldEqual 1 + metricPoints(0).getMetricDefinition.getKey shouldEqual SUCCESS_METRIC_NAME + } + } +} diff --git a/trends/span-timeseries-transformer/src/test/scala/com/expedia/www/haystack/trends/integration/IntegrationTestSpec.scala b/trends/span-timeseries-transformer/src/test/scala/com/expedia/www/haystack/trends/integration/IntegrationTestSpec.scala new file mode 100644 index 000000000..74c26e5e2 --- /dev/null +++ b/trends/span-timeseries-transformer/src/test/scala/com/expedia/www/haystack/trends/integration/IntegrationTestSpec.scala @@ -0,0 +1,120 @@ +/* + * + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.trends.integration + + +import java.util.Properties +import java.util.concurrent.{Executors, ScheduledExecutorService, ScheduledFuture, TimeUnit} + +import com.expedia.open.tracing.Span +import com.expedia.www.haystack.commons.entities.encoders.PeriodReplacementEncoder +import com.expedia.www.haystack.commons.kstreams.serde.SpanSerde +import com.expedia.www.haystack.commons.kstreams.serde.metricdata.MetricTankSerde +import org.apache.kafka.clients.consumer.ConsumerConfig +import org.apache.kafka.clients.producer.ProducerConfig +import org.apache.kafka.common.serialization.{StringDeserializer, StringSerializer} +import org.apache.kafka.streams.integration.utils.{EmbeddedKafkaCluster, IntegrationTestUtils} +import org.apache.kafka.streams.{KeyValue, StreamsConfig} +import org.scalatest._ + +import scala.collection.JavaConverters._ +import scala.concurrent.duration.FiniteDuration + +object EmbeddedKafka { + val CLUSTER = new EmbeddedKafkaCluster(1) + CLUSTER.start() +} + +class IntegrationTestSpec extends WordSpec with GivenWhenThen with Matchers with BeforeAndAfterAll with BeforeAndAfterEach { + + protected val PUNCTUATE_INTERVAL_MS = 2000 + protected val PRODUCER_CONFIG = new Properties() + protected val RESULT_CONSUMER_CONFIG = new Properties() + protected val STREAMS_CONFIG = new Properties() + protected val scheduledJobFuture: ScheduledFuture[_] = null + protected val INPUT_TOPIC = "spans" + protected val OUTPUT_TOPIC = "metricpoints" + protected var scheduler: ScheduledExecutorService = _ + protected var APP_ID = "haystack-trends" + protected val METRIC_TYPE = "gauge" + protected var CHANGELOG_TOPIC = "" + protected var KAFKA_ENDPOINT = "192.168.99.100:9092" + + + override def beforeAll() { + scheduler = Executors.newSingleThreadScheduledExecutor() + } + + override def afterAll(): Unit = { + scheduler.shutdownNow() + } + + override def beforeEach() { + val metricTankSerde = new MetricTankSerde() + + EmbeddedKafka.CLUSTER.createTopic(INPUT_TOPIC) + EmbeddedKafka.CLUSTER.createTopic(OUTPUT_TOPIC) + + PRODUCER_CONFIG.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, EmbeddedKafka.CLUSTER.bootstrapServers) + PRODUCER_CONFIG.put(ProducerConfig.ACKS_CONFIG, "all") + PRODUCER_CONFIG.put(ProducerConfig.RETRIES_CONFIG, "0") + PRODUCER_CONFIG.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, classOf[StringSerializer]) + PRODUCER_CONFIG.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, new SpanSerde().serializer().getClass) + + RESULT_CONSUMER_CONFIG.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, EmbeddedKafka.CLUSTER.bootstrapServers) + RESULT_CONSUMER_CONFIG.put(ConsumerConfig.GROUP_ID_CONFIG, APP_ID + "-result-consumer") + RESULT_CONSUMER_CONFIG.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest") + RESULT_CONSUMER_CONFIG.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, classOf[StringDeserializer]) + RESULT_CONSUMER_CONFIG.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, metricTankSerde.deserializer().getClass) + + STREAMS_CONFIG.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, EmbeddedKafka.CLUSTER.bootstrapServers) + STREAMS_CONFIG.put(StreamsConfig.APPLICATION_ID_CONFIG, APP_ID) + STREAMS_CONFIG.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest") + STREAMS_CONFIG.put(StreamsConfig.CACHE_MAX_BYTES_BUFFERING_CONFIG, "0") + STREAMS_CONFIG.put(StreamsConfig.COMMIT_INTERVAL_MS_CONFIG, "300") + STREAMS_CONFIG.put(StreamsConfig.STATE_DIR_CONFIG, "/tmp/kafka-streams") + + IntegrationTestUtils.purgeLocalStreamsState(STREAMS_CONFIG) + + CHANGELOG_TOPIC = s"$APP_ID-AggregatedMetricPointStore-changelog" + } + + override def afterEach(): Unit = { + EmbeddedKafka.CLUSTER.deleteTopic(INPUT_TOPIC) + EmbeddedKafka.CLUSTER.deleteTopic(OUTPUT_TOPIC) + } + + protected def produceSpansAsync(produceInterval: FiniteDuration, + spans: List[Span]): Unit = { + var currentTime = System.currentTimeMillis() + var idx = 0 + scheduler.scheduleWithFixedDelay(() => { + if (idx < spans.size) { + currentTime = currentTime + ((idx * PUNCTUATE_INTERVAL_MS) / (spans.size - 1)) + val span = spans.apply(idx) + val records = List(new KeyValue[String, Span](span.getTraceId, span)).asJava + IntegrationTestUtils.produceKeyValuesSynchronouslyWithTimestamp( + INPUT_TOPIC, + records, + PRODUCER_CONFIG, + currentTime) + } + idx = idx + 1 + }, 0, produceInterval.toMillis, TimeUnit.MILLISECONDS) + } +} diff --git a/trends/span-timeseries-transformer/src/test/scala/com/expedia/www/haystack/trends/integration/tests/TimeSeriesTransformerTopologySpec.scala b/trends/span-timeseries-transformer/src/test/scala/com/expedia/www/haystack/trends/integration/tests/TimeSeriesTransformerTopologySpec.scala new file mode 100644 index 000000000..491337d0b --- /dev/null +++ b/trends/span-timeseries-transformer/src/test/scala/com/expedia/www/haystack/trends/integration/tests/TimeSeriesTransformerTopologySpec.scala @@ -0,0 +1,125 @@ +/* + * + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.trends.integration.tests + +import java.util.UUID + +import com.expedia.metrics.{MetricData, MetricDefinition} +import com.expedia.open.tracing.Span +import com.expedia.www.haystack.commons.entities.TagKeys +import com.expedia.www.haystack.commons.entities.encoders.PeriodReplacementEncoder +import com.expedia.www.haystack.commons.health.HealthStatusController +import com.expedia.www.haystack.commons.kstreams.app.{StateChangeListener, StreamsFactory, StreamsRunner} +import com.expedia.www.haystack.commons.util.MetricDefinitionKeyGenerator +import com.expedia.www.haystack.trends.config.entities.{KafkaConfiguration, TransformerConfiguration} +import com.expedia.www.haystack.trends.integration.IntegrationTestSpec +import com.expedia.www.haystack.trends.transformer.MetricDataTransformer +import com.expedia.www.haystack.trends.{MetricDataGenerator, Streams} +import org.apache.kafka.clients.admin.AdminClient +import org.apache.kafka.streams.Topology.AutoOffsetReset +import org.apache.kafka.streams.integration.utils.IntegrationTestUtils +import org.apache.kafka.streams.processor.WallclockTimestampExtractor +import org.apache.kafka.streams.{KeyValue, StreamsConfig} + +import scala.collection.JavaConverters._ +import scala.concurrent.duration._ + +class TimeSeriesTransformerTopologySpec extends IntegrationTestSpec with MetricDataGenerator { + + "TimeSeries Transformer Topology" should { + + "consume spans from input topic and transform them to metric data list based on available transformers" in { + + Given("a set of spans and kafka specific configurations") + val traceId = "trace-id-dummy" + val spanId = "span-id-dummy" + val duration = 3 + val errorFlag = false + val spans = generateSpans(traceId, spanId, duration, errorFlag, 10000, 8) + val kafkaConfig = KafkaConfiguration(new StreamsConfig(STREAMS_CONFIG), OUTPUT_TOPIC, INPUT_TOPIC, AutoOffsetReset.EARLIEST, new WallclockTimestampExtractor, 30000) + val transformerConfig = TransformerConfiguration(encoder = new PeriodReplacementEncoder, enableMetricPointServiceLevelGeneration = true, List()) + val streams = new Streams(kafkaConfig, transformerConfig) + val factory = new StreamsFactory(streams, kafkaConfig.streamsConfig, kafkaConfig.consumeTopic) + val streamsRunner = new StreamsRunner(factory, new StateChangeListener(new HealthStatusController)) + + + When("spans with duration and error=false are produced in 'input' topic, and kafka-streams topology is started") + produceSpansAsync(10.millis, spans) + streamsRunner.start() + + Then("we should write transformed metricPoints to the 'output' topic") + val metricDataList: List[MetricData] = spans.flatMap(span => generateMetricDataList(span, MetricDataTransformer.allTransformers, new PeriodReplacementEncoder)) // directly call transformers to get metricPoints + metricDataList.size shouldBe (spans.size * MetricDataTransformer.allTransformers.size * 2) // two times because of service only metric points + + val records: List[KeyValue[String, MetricData]] = + IntegrationTestUtils.waitUntilMinKeyValueRecordsReceived[String, MetricData](RESULT_CONSUMER_CONFIG, OUTPUT_TOPIC, metricDataList.size, 15000).asScala.toList // get metricPoints from Kafka's output topic + records.map(record => { + record.value.getMetricDefinition.getTags.getKv.get(MetricDefinition.MTYPE) shouldEqual METRIC_TYPE + }) + + Then("same metricPoints should be created as that from transformers") + + val metricDataSetTransformer: Set[MetricData] = metricDataList.toSet + val metricDataSetKafka: Set[MetricData] = records.map(metricDataKv => metricDataKv.value).toSet + + val diffSetMetricPoint: Set[MetricData] = metricDataSetTransformer.diff(metricDataSetKafka) + + metricDataList.size shouldEqual records.size + diffSetMetricPoint.isEmpty shouldEqual true + + Then("same keys / partition should be created as that from transformers") + val keySetTransformer: Set[String] = metricDataList.map(metricData => MetricDefinitionKeyGenerator.generateKey(metricData.getMetricDefinition)).toSet + val keySetKafka: Set[String] = records.map(metricDataKv => metricDataKv.key).toSet + + val diffSetKey: Set[String] = keySetTransformer.diff(keySetKafka) + + keySetTransformer.size shouldEqual keySetKafka.size + diffSetKey.isEmpty shouldEqual true + + Then("no other intermediate partitions are created after as a result of topology") + val adminClient: AdminClient = AdminClient.create(STREAMS_CONFIG) + val topicNames: Iterable[String] = adminClient.listTopics.listings().get().asScala + .map(topicListing => topicListing.name) + + topicNames.size shouldEqual 2 + topicNames.toSet.contains(INPUT_TOPIC) shouldEqual true + topicNames.toSet.contains(OUTPUT_TOPIC) shouldEqual true + } + } + + private def generateSpans(traceId: String, spanId: String, duration: Int, errorFlag: Boolean, spanIntervalInMs: Long, spanCount: Int): List[Span] = { + + var currentTime = System.currentTimeMillis() + for (i <- 1 to spanCount) yield { + currentTime = currentTime + i * spanIntervalInMs + + val span = Span.newBuilder() + .setTraceId(traceId) + .setParentSpanId(UUID.randomUUID().toString) + .setSpanId(spanId) + .setOperationName("some-op") + .setStartTime(currentTime) + .setDuration(duration) + .setServiceName("some-service") + .addTags(com.expedia.open.tracing.Tag.newBuilder().setKey(TagKeys.ERROR_KEY).setVStr("some-error")) + .build() + span + } + }.toList +} + diff --git a/trends/timeseries-aggregator/Makefile b/trends/timeseries-aggregator/Makefile new file mode 100644 index 000000000..00051d8de --- /dev/null +++ b/trends/timeseries-aggregator/Makefile @@ -0,0 +1,19 @@ +.PHONY: integration_test release all + +MAVEN := ../mvnw + +export DOCKER_ORG := expediadotcom +export DOCKER_IMAGE_NAME := haystack-timeseries-aggregator + +docker_build: + docker build -t $(DOCKER_IMAGE_NAME) -f build/docker/Dockerfile . + +integration_test: + ${MAVEN} scoverage:integration-check + +# build jar, docker image and run integration tests +all: docker_build integration_test + +# build all and release +release: docker_build + ../deployment/scripts/publish-to-docker-hub.sh diff --git a/trends/timeseries-aggregator/README.md b/trends/timeseries-aggregator/README.md new file mode 100644 index 000000000..9f99b1ffd --- /dev/null +++ b/trends/timeseries-aggregator/README.md @@ -0,0 +1,35 @@ +# Haystack Timeseries Aggregator + +haystack-timeseries-aggregator is the module which reads metric points from kafka, aggregates them based on rules and pushes the aggregated metric points to kafka + +These aggregated metric points and stored in a time-series database and can be visualized on the [haystack ui](https://github.com/ExpediaDotCom/haystack-ui) + + +Haystack's has another app [span-timeseries-transformer](https://github.com/ExpediaDotCom/haystack-trends/tree/master/span-timeseries-transformer) +which is responsible for reading the spans and creating raw metric points for aggregation + +This is a simple public static void main application which is written in scala and uses kafka-streams. This is designed to be deployed as a docker containers. + + +## Building + +#### Prerequisite: + +* Make sure you have Java 1.8 +* Make sure you have maven 3.3.9 or higher +* Make sure you have docker 1.13 or higher + +#### Build + +For a full build, including unit tests, jar + docker image build and integration test, you can run - +``` +make all +``` + +#### Integration Test + +If you are developing and just want to run integration tests +``` +make integration_test + +``` \ No newline at end of file diff --git a/trends/timeseries-aggregator/build/docker/Dockerfile b/trends/timeseries-aggregator/build/docker/Dockerfile new file mode 100644 index 000000000..af451d9a3 --- /dev/null +++ b/trends/timeseries-aggregator/build/docker/Dockerfile @@ -0,0 +1,23 @@ +FROM openjdk:8-jre +MAINTAINER Haystack + +ENV APP_NAME haystack-timeseries-aggregator +ENV APP_HOME /app/bin +ENV JMXTRANS_AGENT jmxtrans-agent-1.2.6 +ENV DOCKERIZE_VERSION v0.6.1 + +ADD https://github.com/jwilder/dockerize/releases/download/${DOCKERIZE_VERSION}/dockerize-alpine-linux-amd64-${DOCKERIZE_VERSION}.tar.gz dockerize.tar.gz +RUN tar xzf dockerize.tar.gz +RUN chmod +x dockerize + +RUN mkdir -p ${APP_HOME} + +COPY target/${APP_NAME}.jar ${APP_HOME}/ +COPY build/docker/start-app.sh ${APP_HOME}/ +COPY build/docker/jmxtrans-agent.xml ${APP_HOME}/ + +ADD https://github.com/jmxtrans/jmxtrans-agent/releases/download/${JMXTRANS_AGENT}/${JMXTRANS_AGENT}.jar ${APP_HOME}/ + +WORKDIR ${APP_HOME} + +ENTRYPOINT ["./start-app.sh"] diff --git a/trends/timeseries-aggregator/build/docker/jmxtrans-agent.xml b/trends/timeseries-aggregator/build/docker/jmxtrans-agent.xml new file mode 100644 index 000000000..7b06786f5 --- /dev/null +++ b/trends/timeseries-aggregator/build/docker/jmxtrans-agent.xml @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ${HAYSTACK_GRAPHITE_HOST:monitoring-influxdb-graphite.kube-system.svc} + ${HAYSTACK_GRAPHITE_PORT:2003} + ${HAYSTACK_GRAPHITE_ENABLED:false} + haystack.trends.timeseries-aggregator.#hostname#. + + 60 + diff --git a/trends/timeseries-aggregator/build/docker/start-app.sh b/trends/timeseries-aggregator/build/docker/start-app.sh new file mode 100755 index 000000000..1988f0624 --- /dev/null +++ b/trends/timeseries-aggregator/build/docker/start-app.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +[ -z "$JAVA_XMS" ] && JAVA_XMS=1024m +[ -z "$JAVA_XMX" ] && JAVA_XMX=1024m + +set -e +JAVA_OPTS="${JAVA_OPTS} \ +-javaagent:${APP_HOME}/${JMXTRANS_AGENT}.jar=${APP_HOME}/jmxtrans-agent.xml \ +-XX:+UseG1GC \ +-Xloggc:/var/log/gc.log \ +-XX:+PrintGCDetails \ +-XX:+PrintGCDateStamps \ +-XX:+UseGCLogFileRotation \ +-XX:NumberOfGCLogFiles=5 \ +-XX:GCLogFileSize=2M \ +-Xmx${JAVA_XMX} \ +-Xms${JAVA_XMS} \ +-Dapplication.name=${APP_NAME} \ +-Dcom.sun.management.jmxremote.authenticate=false \ +-Dcom.sun.management.jmxremote.ssl=false \ +-Dcom.sun.management.jmxremote.port=1098 \ +-Dapplication.home=${APP_HOME}" + +exec java ${JAVA_OPTS} -jar "${APP_HOME}/${APP_NAME}.jar" diff --git a/trends/timeseries-aggregator/pom.xml b/trends/timeseries-aggregator/pom.xml new file mode 100644 index 000000000..bc8fc0d97 --- /dev/null +++ b/trends/timeseries-aggregator/pom.xml @@ -0,0 +1,107 @@ + + + + + + 4.0.0 + + haystack-timeseries-aggregator + jar + haystack-timeseries-aggregator + scala module which aggregates timeseries metricpoints based on predefined rules + + + com.expedia.www + haystack-trends + 1.0.0-SNAPSHOT + + + + + The Apache License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + + + + + com.expedia.www.haystack.trends.App + com.expedia.www.haystack.trends.feature.tests,com.expedia.www.haystack.trends.unit.tests + com.expedia.www.haystack.trends.integration.tests + ${project.artifactId}-${project.version} + + + + + org.hdrhistogram + HdrHistogram + + + com.expedia.www + haystack-commons + + + com.expedia.www + haystack-logback-metrics-appender + + + org.apache.kafka + kafka-streams + + + org.msgpack + msgpack-core + + + com.typesafe + config + + + com.codahale.metrics + metrics-core + + + + + ${finalName} + + + src/main/resources + true + + + + + org.apache.maven.plugins + maven-shade-plugin + + + org.scalatest + scalatest-maven-plugin + + + org.scalastyle + scalastyle-maven-plugin + + + + + diff --git a/trends/timeseries-aggregator/src/main/resources/config/base.conf b/trends/timeseries-aggregator/src/main/resources/config/base.conf new file mode 100644 index 000000000..052e652a2 --- /dev/null +++ b/trends/timeseries-aggregator/src/main/resources/config/base.conf @@ -0,0 +1,74 @@ +health.status.path = "/app/isHealthy" + +kafka { + close.timeout.ms = 30000 + + streams { + application.id = "haystack-timeseries-aggregator-dev-v2" + bootstrap.servers = "kafkasvc:9092" + num.stream.threads = 1 + commit.interval.ms = 3000 + auto.offset.reset = latest + producer.retries = 50, + producer.batch.size = 65536, + producer.linger.ms = 250 + metrics.recording.level = DEBUG + timestamp.extractor = "com.expedia.www.haystack.commons.kstreams.MetricDataTimestampExtractor" + } + + // For producing data to external & internal (both) kafka: set enable.external.kafka.produce to true and uncomment the props. + // For producing to same (internal) kafka: set enable.external.kafka.produce to false and comment the props. + producer { + topics : [ + { + topic: "metrics" + serdeClassName : "com.expedia.www.haystack.commons.kstreams.serde.metricdata.MetricDataSerde" + enabled: true + }, + { + topic: "mdm" + serdeClassName : "com.expedia.www.haystack.commons.kstreams.serde.metricdata.MetricTankSerde" + enabled: true + } + ] + enable.external.kafka.produce = false + external.kafka.topic = "mdm" + // props { + // bootstrap.servers = "kafkasvc:9092" + // } + } + + consumer { + topic = "metric-data-points" + } +} + +state.store { + enable.logging = true + logging.delay.seconds = 60 + // It is capacity for the trends to be kept in memory before flushing it to state store + cache.size = 3000 + changelog.topic { + cleanup.policy = "compact,delete" + retention.ms = 14400000 // 4Hrs + } +} + +// there are three types of encoders that are used on service and operation names: +// 1) periodreplacement: replaces all periods with 3 underscores +// 2) base64: base64 encodes the full name with a padding of _ +// 3) noop: does not perform any encoding +metricpoint.encoder.type = "periodreplacement" + +histogram { + max.value = 1800000 // 30 mins + precision = 2 + value.unit = "millis" // can be micros / millis / seconds +} + +// additional tags to be passed as part of metric data +// It can be of format hocon config such as +// additionalTags = {key: "value", key2:"value2"} +// or json such as +// additionalTags = """{"key": "value", "key2":"value2"}""" +additionalTags = {} diff --git a/trends/timeseries-aggregator/src/main/resources/logback.xml b/trends/timeseries-aggregator/src/main/resources/logback.xml new file mode 100644 index 000000000..31cc8523a --- /dev/null +++ b/trends/timeseries-aggregator/src/main/resources/logback.xml @@ -0,0 +1,28 @@ + + + + + + + true + + + + + + %d{yyyy-MM-dd HH:mm:ss:SSS} %thread, %level, %logger{70}, "%msg" %replace(%ex){'[\n]+', '\\n'}%nopex%n + + + + + + ${HAYSTACK_LOG_QUEUE_SIZE:-500} + ${HAYSTACK_LOG_DISCARD_THRESHOLD:-0} + + + + + + + + diff --git a/trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/App.scala b/trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/App.scala new file mode 100644 index 000000000..7375d7001 --- /dev/null +++ b/trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/App.scala @@ -0,0 +1,77 @@ +/* + * + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trends + +import java.util.function.Supplier + +import com.expedia.www.haystack.commons.health.{HealthStatusController, UpdateHealthStatusFile} +import com.expedia.www.haystack.commons.kstreams.app.{Main, StateChangeListener, StreamsFactory, StreamsRunner} +import com.expedia.www.haystack.trends.config.AppConfiguration +import com.expedia.www.haystack.trends.kstream.Streams +import com.netflix.servo.util.VisibleForTesting +import org.apache.kafka.streams.Topology + + +object App extends Main { + + /** + * Creates a valid instance of StreamsRunner. + * + * StreamsRunner is created with a valid StreamsFactory instance and a listener that observes + * state changes of the kstreams application. + * + * StreamsFactory in turn is created with a Topology Supplier and kafka.StreamsConfig. Any failure in + * StreamsFactory is gracefully handled by StreamsRunner to shut the application off + * + * Core logic of this application is in the `Streams` instance - which is a topology supplier. The + * topology of this application is built in this class. + * + * @return A valid instance of `StreamsRunner` + */ + + override def createStreamsRunner(): StreamsRunner = { + val ProjectConfiguration = new AppConfiguration() + + val healthStatusController = new HealthStatusController + healthStatusController.addListener(new UpdateHealthStatusFile(ProjectConfiguration.healthStatusFilePath)) + + val stateChangeListener = new StateChangeListener(healthStatusController) + + createStreamsRunner(ProjectConfiguration, stateChangeListener) + } + + @VisibleForTesting + private[trends] def createStreamsRunner(ProjectConfiguration: AppConfiguration, + stateChangeListener: StateChangeListener): StreamsRunner = { + //create the topology provider + val kafkaConfig = ProjectConfiguration.kafkaConfig + val streams: Supplier[Topology] = new Streams(ProjectConfiguration) + + val streamsFactory = new StreamsFactory(streams, kafkaConfig.streamsConfig, kafkaConfig.consumeTopic) + + new StreamsRunner(streamsFactory, stateChangeListener) + } +} + + + + + + + diff --git a/trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/aggregation/TrendHdrHistogram.scala b/trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/aggregation/TrendHdrHistogram.scala new file mode 100644 index 000000000..28a5116b9 --- /dev/null +++ b/trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/aggregation/TrendHdrHistogram.scala @@ -0,0 +1,81 @@ +/* + * + * Copyright 2019 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trends.aggregation + +import java.nio.ByteBuffer +import java.util.concurrent.TimeUnit + +import com.expedia.www.haystack.trends.config.entities.HistogramUnit.HistogramUnit +import com.expedia.www.haystack.trends.config.entities.{HistogramMetricConfiguration, HistogramUnit} +import org.HdrHistogram.Histogram + +/** + * Wrapper over hdr Histogram. Takes care of unit mismatch of histogram and the other systems. + * + * @param hdrHistogram : instance of hdr Histogram + * @param unit : unit of the recorded values, can be millis, micros or seconds + */ +case class TrendHdrHistogram(private val hdrHistogram: Histogram, unit: HistogramUnit) { + + def this(histogramConfig: HistogramMetricConfiguration) = this( + new Histogram(histogramConfig.maxValue, histogramConfig.precision), histogramConfig.unit) + + def recordValue(valInMicros: Long): Unit = { + val metricDataValue = fromMicros(valInMicros) + hdrHistogram.recordValue(metricDataValue) + } + + def getMinValue: Long = toMicros(hdrHistogram.getMinValue) + + def getMaxValue: Long = toMicros(hdrHistogram.getMaxValue) + + def getMean: Long = toMicros(hdrHistogram.getMean.toLong) + + def getStdDeviation: Long = toMicros(hdrHistogram.getStdDeviation.toLong) + + def getTotalCount: Long = hdrHistogram.getTotalCount + + def getHighestTrackableValue: Long = hdrHistogram.getHighestTrackableValue + + def getHighesTrackableValueInMicros: Long = toMicros(hdrHistogram.getHighestTrackableValue) + + def getValueAtPercentile(percentile: Double): Long = toMicros(hdrHistogram.getValueAtPercentile(percentile)) + + def getEstimatedFootprintInBytes: Int = hdrHistogram.getEstimatedFootprintInBytes + + def encodeIntoByteBuffer(buffer: ByteBuffer): Int = { + hdrHistogram.encodeIntoByteBuffer(buffer) + } + + private def fromMicros(value: Long): Long = { + unit match { + case HistogramUnit.MILLIS => TimeUnit.MICROSECONDS.toMillis(value) + case HistogramUnit.SECONDS => TimeUnit.MICROSECONDS.toSeconds(value) + case _ => value + } + } + + private def toMicros(value: Long): Long = { + unit match { + case HistogramUnit.MILLIS => TimeUnit.MILLISECONDS.toMicros(value.toLong) + case HistogramUnit.SECONDS => TimeUnit.SECONDS.toMicros(value.toLong) + case _ => value + } + } +} \ No newline at end of file diff --git a/trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/aggregation/TrendMetric.scala b/trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/aggregation/TrendMetric.scala new file mode 100644 index 000000000..ac6296b0b --- /dev/null +++ b/trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/aggregation/TrendMetric.scala @@ -0,0 +1,140 @@ +/* + * + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trends.aggregation + +import com.codahale.metrics.{Meter, Timer} +import com.expedia.metrics.MetricData +import com.expedia.www.haystack.commons.entities.Interval +import com.expedia.www.haystack.commons.entities.Interval.Interval +import com.expedia.www.haystack.commons.metrics.MetricsSupport +import com.expedia.www.haystack.trends.aggregation.TrendMetric._ +import com.expedia.www.haystack.trends.aggregation.metrics.MetricFactory +import com.expedia.www.haystack.trends.config.AppConfiguration +import org.slf4j.LoggerFactory + +import scala.util.Try + +/** + * This class contains a windowedMetric for each interval being computed. The number of time windows at any moment is = no. of intervals + * depends upon interval, numberOfWatermarkWindows and in which timeWindow incoming metric lies + * + * @param trendMetricsMap map containing intervals and windowedMetrics + * @param metricFactory factory which is used to create new metrics when required + */ +class TrendMetric private(var trendMetricsMap: Map[Interval, WindowedMetric], metricFactory: MetricFactory) extends MetricsSupport { + + private val trendMetricComputeTimer: Timer = metricRegistry.timer("trendmetric.compute.time") + private val metricPointComputeFailureMeter: Meter = metricRegistry.meter("metricpoints.compute.failure") + private var currentEpochTimeInSec: Long = 0 + private var shouldLog = true + + def getMetricFactory: MetricFactory = { + metricFactory + } + + /** + * function to compute the incoming metric data + * it updates all the metrics for the windows within which the incoming metric point lies + * + * @param incomingMetricData - incoming metric data + */ + def compute(incomingMetricData: MetricData): Unit = { + val timerContext = trendMetricComputeTimer.time() + Try { + //discarding values which are less than 0 assuming they are invalid metric points + trendMetricsMap.foreach(trendMetrics => { + val windowedMetric = trendMetrics._2 + windowedMetric.compute(incomingMetricData) + }) + }.recover { + case failure: Throwable => + metricPointComputeFailureMeter.mark() + LOGGER.error(s"Failed to compute metricpoint : $incomingMetricData with exception ", failure) + failure + } + + // check whether time to log to state store + if ((incomingMetricData.getTimestamp - currentEpochTimeInSec) > AppConfiguration.stateStoreConfig.changeLogDelayInSecs) { + currentEpochTimeInSec = incomingMetricData.getTimestamp + shouldLog = true + } + + timerContext.close() + } + + /** + * returns list of metricPoints which are evicted and their window is closes + * + * @return list of evicted metricPoints + */ + def getComputedMetricPoints(incomingMetricData: MetricData): List[MetricData] = { + List(trendMetricsMap.flatMap { + case (_, windowedMetric) => + windowedMetric.getComputedMetricDataList(incomingMetricData) + }).flatten + } + + /** + * flag to tell whether we need to log to state store + * + * @return flag to indicate should we log + */ + def shouldLogToStateStore: Boolean = { + if (shouldLog) { + shouldLog = false + return true + } + false + } +} + +object TrendMetric { + + private val LOGGER = LoggerFactory.getLogger(this.getClass) + + + // config for watermark windows & tick per interval + val trendMetricConfig = Map( + Interval.ONE_MINUTE -> (1, 1), + Interval.FIVE_MINUTE -> (1, 1), + Interval.FIFTEEN_MINUTE -> (0, 1), + Interval.ONE_HOUR -> (0, 1)) + + def createTrendMetric(intervals: List[Interval], + firstMetricData: MetricData, + metricFactory: MetricFactory): TrendMetric = { + // this enable to log data to state store for the very first time + val trendMetricMap = createMetricsForEachInterval(intervals, firstMetricData, metricFactory) + new TrendMetric(trendMetricMap, metricFactory) + } + + def restoreTrendMetric(trendMetricMap: Map[Interval, WindowedMetric], + metricFactory: MetricFactory): TrendMetric = { + new TrendMetric(trendMetricMap, metricFactory) + } + + private def createMetricsForEachInterval(intervals: List[Interval], + metricData: MetricData, + metricFactory: MetricFactory): Map[Interval, WindowedMetric] = { + intervals.map(interval => { + val windowedMetric = WindowedMetric.createWindowedMetric(metricData, metricFactory, trendMetricConfig(interval)._1, interval) + interval -> windowedMetric + }).toMap + } +} diff --git a/trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/aggregation/WindowedMetric.scala b/trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/aggregation/WindowedMetric.scala new file mode 100644 index 000000000..6fb905199 --- /dev/null +++ b/trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/aggregation/WindowedMetric.scala @@ -0,0 +1,126 @@ +/* + * + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trends.aggregation + +import java.util.concurrent.TimeUnit + +import com.codahale.metrics.{Histogram, Meter} +import com.expedia.metrics.MetricData +import com.expedia.www.haystack.commons.entities.Interval.Interval +import com.expedia.www.haystack.commons.metrics.MetricsSupport +import com.expedia.www.haystack.trends.aggregation.entities.TimeWindow +import com.expedia.www.haystack.trends.aggregation.metrics.{Metric, MetricFactory} +import org.slf4j.LoggerFactory + +import scala.collection.mutable +import scala.util.Try + +/** + * This class contains a metric for each time window being computed for a single interval + * + * @param windowedMetricsMap map containing sorted timewindows and metrics for an Interval + * @param metricFactory factory which is used to create new metrics when required + */ +class WindowedMetric private(var windowedMetricsMap: mutable.TreeMap[TimeWindow, Metric], metricFactory: MetricFactory, numberOfWatermarkedWindows: Int, interval: Interval) extends MetricsSupport { + + private val disorderedMetricPointMeter: Meter = metricRegistry.meter("metricpoints.disordered") + private val timeInTopicMetricPointHistogram: Histogram = metricRegistry.histogram("metricpoints.timeInTopic") + private var computedMetrics = List[(Long, Metric)]() + private val LOGGER = LoggerFactory.getLogger(this.getClass) + + def getMetricFactory: MetricFactory = { + metricFactory + } + + /** + * function to compute the incoming metric data + * it updates all the metrics for the windows within which the incoming metric data lies for an interval + * + * @param incomingMetricData - incoming metric data + */ + def compute(incomingMetricData: MetricData): Unit = { + timeInTopicMetricPointHistogram.update(TimeUnit.SECONDS.toMillis(incomingMetricData.getTimestamp()) - System.currentTimeMillis()) + + val incomingMetricPointTimeWindow = TimeWindow.apply(incomingMetricData.getTimestamp, interval) + + val matchedWindowedMetric = windowedMetricsMap.get(incomingMetricPointTimeWindow) + + if (matchedWindowedMetric.isDefined) { + // an existing metric + matchedWindowedMetric.get.compute(incomingMetricData) + } else { + // incoming metric is a new metric + if (incomingMetricPointTimeWindow.compare(windowedMetricsMap.firstKey) > 0) { + // incoming metric's time is more that minimum (first) time window + createNewMetric(incomingMetricPointTimeWindow, incomingMetricData) + evictMetric() + } else { + // disordered metric + disorderedMetricPointMeter.mark() + } + } + } + + private def createNewMetric(incomingMetricPointTimeWindow: TimeWindow, incomingMetricData: MetricData) = { + val newMetric = metricFactory.createMetric(interval) + newMetric.compute(incomingMetricData) + windowedMetricsMap.put(incomingMetricPointTimeWindow, newMetric) + } + + private def evictMetric() = { + if (windowedMetricsMap.size > (numberOfWatermarkedWindows + 1)) { + val evictInterval = windowedMetricsMap.firstKey + windowedMetricsMap.remove(evictInterval).foreach { evictedMetric => + computedMetrics = (evictInterval.endTime, evictedMetric) :: computedMetrics + } + } + } + + /** + * returns list of metricData which are evicted and their window is closes + * + * @return list of evicted metricData + */ + def getComputedMetricDataList(incomingMetricData: MetricData): List[MetricData] = { + val metricDataList = computedMetrics.flatMap { + case (publishTime, metric) => + metric.mapToMetricDataList(incomingMetricData.getMetricDefinition.getKey, incomingMetricData.getMetricDefinition.getTags.getKv, publishTime) + } + computedMetrics = List[(Long, Metric)]() + metricDataList + } +} + +/** + * Windowed metric factory which can create a new windowed metric or restore an existing windowed metric for an interval + */ +object WindowedMetric { + + private val LOGGER = LoggerFactory.getLogger(this.getClass) + + def createWindowedMetric(firstMetricData: MetricData, metricFactory: MetricFactory, watermarkedWindows: Int, interval: Interval): WindowedMetric = { + val windowedMetricMap = mutable.TreeMap[TimeWindow, Metric]() + windowedMetricMap.put(TimeWindow.apply(firstMetricData.getTimestamp, interval), metricFactory.createMetric(interval)) + new WindowedMetric(windowedMetricMap, metricFactory, watermarkedWindows, interval) + } + + def restoreWindowedMetric(windowedMetricsMap: mutable.TreeMap[TimeWindow, Metric], metricFactory: MetricFactory, watermarkedWindows: Int, interval: Interval): WindowedMetric = { + new WindowedMetric(windowedMetricsMap, metricFactory, watermarkedWindows, interval) + } +} diff --git a/trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/aggregation/entities/StatValue.scala b/trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/aggregation/entities/StatValue.scala new file mode 100644 index 000000000..70675f934 --- /dev/null +++ b/trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/aggregation/entities/StatValue.scala @@ -0,0 +1,18 @@ +package com.expedia.www.haystack.trends.aggregation.entities + +/** + * This enumeration contains all the supported statistics we want to emit for a given histogram metric + */ +object StatValue extends Enumeration { + type StatValue = Value + + val MEAN = Value("mean") + val MAX = Value("max") + val MIN = Value("min") + val COUNT = Value("count") + val STDDEV = Value("std") + val PERCENTILE_95 = Value("*_95") + val PERCENTILE_99 = Value("*_99") + val MEDIAN = Value("*_50") + +} diff --git a/trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/aggregation/entities/TimeWindow.scala b/trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/aggregation/entities/TimeWindow.scala new file mode 100644 index 000000000..163cf7758 --- /dev/null +++ b/trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/aggregation/entities/TimeWindow.scala @@ -0,0 +1,40 @@ +package com.expedia.www.haystack.trends.aggregation.entities + +import com.expedia.www.haystack.commons.entities.Interval.Interval + + +/** + * This class encapsulates a time window which contains a start time and an end-time + */ +case class TimeWindow(startTime: Long, endTime: Long) extends Ordered[TimeWindow] { + + override def compare(that: TimeWindow): Int = { + this.startTime.compare(that.startTime) + } + + override def hashCode(): Int = { + this.startTime.hashCode() + } + + override def equals(that: scala.Any): Boolean = { + this.startTime == that.asInstanceOf[TimeWindow].startTime && this.endTime == that.asInstanceOf[TimeWindow].endTime + } +} + +object TimeWindow { + + /** + * This function creates the time window based on the given time in seconds and the interval of the window + * Eg : given a timestamp 145 seconds and an interval of 1 minute, the window would be 120 seconds - 180 seconds + * @param timestamp given time in seconds + * @param interval interval for which we would need to create the window + * @return time window + */ + + def apply(timestamp: Long, interval: Interval): TimeWindow = { + val intervalTimeInSeconds = interval.timeInSeconds + val windowStart = (timestamp / intervalTimeInSeconds) * intervalTimeInSeconds + val windowEnd = windowStart + intervalTimeInSeconds + TimeWindow(windowStart, windowEnd) + } +} diff --git a/trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/aggregation/metrics/CountMetric.scala b/trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/aggregation/metrics/CountMetric.scala new file mode 100644 index 000000000..16948b9b5 --- /dev/null +++ b/trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/aggregation/metrics/CountMetric.scala @@ -0,0 +1,70 @@ +/* + * + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trends.aggregation.metrics + +import java._ + +import com.codahale.metrics.Timer +import com.expedia.metrics.{MetricData, MetricDefinition, TagCollection} +import com.expedia.www.haystack.commons.entities.Interval.Interval +import com.expedia.www.haystack.trends.aggregation.entities.StatValue +import com.expedia.www.haystack.trends.aggregation.metrics.AggregationType.AggregationType +import com.expedia.www.haystack.trends.kstream.serde.metric.{CountMetricSerde, MetricSerde} + +/** + * This is a base metric which can compute the count of the given events + * + * @param interval : interval for the metric + * @param currentCount : current count, the current count should be 0 for a new metric but can be passed when we want to restore a given metric after the application crashed + */ + +class CountMetric(interval: Interval, var currentCount: Long) extends Metric(interval) { + + def this(interval: Interval) = this(interval, 0) + + private val CountMetricComputeTimer: Timer = metricRegistry.timer("count.metric.compute.time") + + + override def mapToMetricDataList(metricKey: String, tags: util.Map[String, String], publishingTimestamp: Long): List[MetricData] = { + val tagCollection = new TagCollection(appendTags(tags, interval, StatValue.COUNT)) + val metricDefinition = new MetricDefinition(metricKey, tagCollection, TagCollection.EMPTY) + val metricData = new MetricData(metricDefinition, currentCount, publishingTimestamp) + List(metricData) + } + + def getCurrentCount: Long = { + currentCount + } + + + override def compute(metricData: MetricData): CountMetric = { + val timerContext = CountMetricComputeTimer.time() + currentCount += metricData.getValue.toLong + timerContext.close() + this + } +} + +object CountMetricFactory extends MetricFactory { + override def createMetric(interval: Interval): CountMetric = new CountMetric(interval) + + override def getAggregationType: AggregationType = AggregationType.Count + + override def getMetricSerde: MetricSerde = CountMetricSerde +} diff --git a/trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/aggregation/metrics/HistogramMetric.scala b/trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/aggregation/metrics/HistogramMetric.scala new file mode 100644 index 000000000..f7f70759f --- /dev/null +++ b/trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/aggregation/metrics/HistogramMetric.scala @@ -0,0 +1,90 @@ +/* + * + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trends.aggregation.metrics + +import com.codahale.metrics.Timer +import com.expedia.metrics.{MetricData, MetricDefinition, TagCollection} +import com.expedia.www.haystack.commons.entities.Interval.Interval +import com.expedia.www.haystack.trends.aggregation.TrendHdrHistogram +import com.expedia.www.haystack.trends.aggregation.metrics.AggregationType.AggregationType +import com.expedia.www.haystack.trends.config.AppConfiguration +import com.expedia.www.haystack.trends.kstream.serde.metric.{HistogramMetricSerde, MetricSerde} + + +/** + * This is a base metric which can compute the histogram of the given events. It uses hdr histogram(https://github.com/HdrHistogram/HdrHistogram) internally to compute the histogram + * + * @param interval : interval for the metric + * @param histogram : current histogram, the current histogram should be a new histogram object for a new metric but can be passed when we want to restore a given metric after the application crashed + */ +class HistogramMetric(interval: Interval, histogram: TrendHdrHistogram) extends Metric(interval) { + + private val HistogramMetricComputeTimer: Timer = metricRegistry.timer("histogram.metric.compute.time") + + def this(interval: Interval) = this(interval, new TrendHdrHistogram(AppConfiguration.histogramMetricConfiguration)) + + + override def mapToMetricDataList(metricKey: String, tags: java.util.Map[String, String], publishingTimestamp: Long): List[MetricData] = { + import com.expedia.www.haystack.trends.aggregation.entities.StatValue._ + histogram.getTotalCount match { + case 0 => List() + case _ => val result = Map( + MEAN -> histogram.getMean, + MIN -> histogram.getMinValue, + PERCENTILE_95 -> histogram.getValueAtPercentile(95), + PERCENTILE_99 -> histogram.getValueAtPercentile(99), + STDDEV -> histogram.getStdDeviation, + MEDIAN -> histogram.getValueAtPercentile(50), + MAX -> histogram.getMaxValue + ).map { + case (stat, value) => + val tagCollection = new TagCollection(appendTags(tags, interval, stat)) + val metricDefinition = new MetricDefinition(metricKey, tagCollection, TagCollection.EMPTY) + new MetricData(metricDefinition, value, publishingTimestamp) + } + result.toList + } + } + + def getRunningHistogram: TrendHdrHistogram = histogram + + override def compute(metricData: MetricData): HistogramMetric = { + //metricdata value is in micro seconds + if (metricData.getValue.toLong <= histogram.getHighesTrackableValueInMicros) { + val timerContext = HistogramMetricComputeTimer.time() + histogram.recordValue(metricData.getValue.toLong) + timerContext.close() + } + else { + val timerContext = HistogramMetricComputeTimer.time() + histogram.recordValue(histogram.getHighesTrackableValueInMicros) + timerContext.close() + } + this + } +} + +object HistogramMetricFactory extends MetricFactory { + + override def createMetric(interval: Interval): HistogramMetric = new HistogramMetric(interval) + + override def getAggregationType: AggregationType = AggregationType.Histogram + + override def getMetricSerde: MetricSerde = HistogramMetricSerde +} diff --git a/trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/aggregation/metrics/Metric.scala b/trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/aggregation/metrics/Metric.scala new file mode 100644 index 000000000..097ed1565 --- /dev/null +++ b/trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/aggregation/metrics/Metric.scala @@ -0,0 +1,84 @@ +/* + * + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trends.aggregation.metrics + +import java._ + +import com.expedia.metrics.MetricData +import com.expedia.www.haystack.commons.entities.Interval.Interval +import com.expedia.www.haystack.commons.entities.TagKeys +import com.expedia.www.haystack.commons.metrics.MetricsSupport +import com.expedia.www.haystack.trends.aggregation.entities.StatValue.StatValue +import com.expedia.www.haystack.trends.aggregation.metrics.AggregationType.AggregationType +import com.expedia.www.haystack.trends.kstream.serde.metric.MetricSerde + +abstract class Metric(interval: Interval) extends MetricsSupport { + + /** + * function to compute the incoming metric-data + * + * @param value - incoming metric data + * @return : returns the metric (in most cases it should return the same object(this) but returning a metric gives the metric implementation class to create an immutable metric) + */ + def compute(value: MetricData): Metric + + def getMetricInterval: Interval = { + interval + } + + + /** + * This function returns the metric points which contains the current snapshot of the metric + * + * @param publishingTimestamp : timestamp in seconds which the consumer wants to be used as the timestamps of these published metricpoints + * @param metricKey : the name of the metricData to be generated + * @param tags : tags to be associated with the metricData + * @return list of published metricdata + */ + def mapToMetricDataList(metricKey: String, tags: util.Map[String, String], publishingTimestamp: Long): List[MetricData] + + protected def appendTags(tags: util.Map[String, String], interval: Interval, statValue: StatValue): util.Map[String, String] = { + new util.LinkedHashMap[String, String] { + putAll(tags) + put(TagKeys.INTERVAL_KEY, interval.name) + put(TagKeys.STATS_KEY, statValue.toString) + } + } + +} + +/** + * The enum contains the support aggregation type, which is currently count and histogram + */ +object AggregationType extends Enumeration { + type AggregationType = Value + val Count, Histogram = Value +} + + +/** + * This trait is supposed to be created by every metric class which lets them create the metric when ever required + */ +trait MetricFactory { + def createMetric(interval: Interval): Metric + + def getAggregationType: AggregationType + + def getMetricSerde: MetricSerde +} diff --git a/trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/aggregation/rules/DurationMetricRule.scala b/trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/aggregation/rules/DurationMetricRule.scala new file mode 100644 index 000000000..924ece4eb --- /dev/null +++ b/trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/aggregation/rules/DurationMetricRule.scala @@ -0,0 +1,36 @@ +/* + * + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trends.aggregation.rules + +import com.expedia.metrics.{MetricData, MetricDefinition} +import com.expedia.www.haystack.trends.aggregation.metrics.AggregationType +import com.expedia.www.haystack.trends.aggregation.metrics.AggregationType.AggregationType + +/** + * This Rule applies a Histogram aggregation type when the incoming metric point's name is duration and is of type gauge + */ +trait DurationMetricRule extends MetricRule { + override def isMatched(metricData: MetricData): Option[AggregationType] = { + if (metricData.getMetricDefinition.getKey.toLowerCase.contains("duration") && containsTag(metricData,MetricDefinition.MTYPE, MTYPE_GAUGE)) { + Some(AggregationType.Histogram) + } else { + super.isMatched(metricData) + } + } +} diff --git a/trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/aggregation/rules/FailureMetricRule.scala b/trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/aggregation/rules/FailureMetricRule.scala new file mode 100644 index 000000000..10d950726 --- /dev/null +++ b/trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/aggregation/rules/FailureMetricRule.scala @@ -0,0 +1,36 @@ +/* + * + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trends.aggregation.rules + +import com.expedia.metrics.{MetricData, MetricDefinition} +import com.expedia.www.haystack.trends.aggregation.metrics.AggregationType +import com.expedia.www.haystack.trends.aggregation.metrics.AggregationType.AggregationType + +/** + * This Rule applies a Count aggregation type when the incoming metric point's name is failure-span and is of type gauge + */ +trait FailureMetricRule extends MetricRule { + override def isMatched(metricData: MetricData): Option[AggregationType] = { + if (metricData.getMetricDefinition.getKey.toLowerCase.contains("failure-span") && containsTag(metricData, MetricDefinition.MTYPE, MTYPE_GAUGE)) { + Some(AggregationType.Count) + } else { + super.isMatched(metricData) + } + } +} diff --git a/trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/aggregation/rules/LatencyMetricRule.scala b/trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/aggregation/rules/LatencyMetricRule.scala new file mode 100644 index 000000000..a388577de --- /dev/null +++ b/trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/aggregation/rules/LatencyMetricRule.scala @@ -0,0 +1,37 @@ +/* + * + * Copyright 2018 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trends.aggregation.rules + +import com.expedia.metrics.{MetricData, MetricDefinition} +import com.expedia.www.haystack.trends.aggregation.metrics.AggregationType +import com.expedia.www.haystack.trends.aggregation.metrics.AggregationType.AggregationType + +/** + * This Rule applies a Histogram aggregation type when the incoming metric point's name is latency and is of type gauge + */ +trait LatencyMetricRule extends MetricRule { + + override def isMatched(metricData: MetricData): Option[AggregationType] = { + if (metricData.getMetricDefinition.getKey.toLowerCase.contains("latency") && containsTag(metricData,MetricDefinition.MTYPE, MTYPE_GAUGE)) { + Some(AggregationType.Histogram) + } else { + super.isMatched(metricData) + } + } +} diff --git a/trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/aggregation/rules/MetricRule.scala b/trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/aggregation/rules/MetricRule.scala new file mode 100644 index 000000000..f3b9144e1 --- /dev/null +++ b/trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/aggregation/rules/MetricRule.scala @@ -0,0 +1,39 @@ +/* + * + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trends.aggregation.rules + +import java._ + +import com.expedia.metrics.MetricData +import com.expedia.www.haystack.trends.aggregation.metrics.AggregationType.AggregationType + +trait MetricRule { + val MTYPE_GAUGE = "gauge" + def isMatched(metricData: MetricData): Option[AggregationType] = None + + def containsTag(metricData: MetricData, tagKey: String, tagValue: String): Boolean = { + val tags = getTags(metricData) + tags.containsKey(tagKey) && tags.get(tagKey).equalsIgnoreCase(tagValue) + } + + def getTags(metricData: MetricData): util.Map[String, String] = { + metricData.getMetricDefinition.getTags.getKv + } +} + diff --git a/trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/aggregation/rules/MetricRuleEngine.scala b/trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/aggregation/rules/MetricRuleEngine.scala new file mode 100644 index 000000000..90c73108b --- /dev/null +++ b/trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/aggregation/rules/MetricRuleEngine.scala @@ -0,0 +1,36 @@ +/* + * + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trends.aggregation.rules + +import com.expedia.metrics.MetricData +import com.expedia.www.haystack.trends.aggregation.metrics.AggregationType.AggregationType + + +/** + * This Metric Rule engine applies all the metric rules it extends from right to left(http://jim-mcbeath.blogspot.in/2009/08/scala-class-linearization.html). + * it returns None if none of the rules are applicable. + * to add another rule, create a rule trait and add it to the with clause in the engine. + * If multiple rules match the rightmost rule is applied + */ +trait MetricRuleEngine extends LatencyMetricRule with DurationMetricRule with FailureMetricRule with SuccessMetricRule { + + def findMatchingMetric(metricData: MetricData): Option[AggregationType] = { + isMatched(metricData) + } +} diff --git a/trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/aggregation/rules/SuccessMetricRule.scala b/trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/aggregation/rules/SuccessMetricRule.scala new file mode 100644 index 000000000..5afff8e69 --- /dev/null +++ b/trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/aggregation/rules/SuccessMetricRule.scala @@ -0,0 +1,37 @@ +/* + * + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trends.aggregation.rules + +import com.expedia.metrics.{MetricData, MetricDefinition} +import com.expedia.www.haystack.trends.aggregation.metrics.AggregationType +import com.expedia.www.haystack.trends.aggregation.metrics.AggregationType.AggregationType + +/** + * This Rule applies a Count aggregation type when the incoming metric point's name is success-span and is of type gauge + */ +trait SuccessMetricRule extends MetricRule { + + override def isMatched(metricData: MetricData): Option[AggregationType] = { + if (metricData.getMetricDefinition.getKey.toLowerCase.contains("success-span") && containsTag(metricData, MetricDefinition.MTYPE, MTYPE_GAUGE)) { + Some(AggregationType.Count) + } else { + super.isMatched(metricData) + } + } +} diff --git a/trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/config/AppConfiguration.scala b/trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/config/AppConfiguration.scala new file mode 100644 index 000000000..2f1f73e85 --- /dev/null +++ b/trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/config/AppConfiguration.scala @@ -0,0 +1,189 @@ +/* + * + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.trends.config + +import java.util.Properties + +import com.expedia.www.haystack.commons.config.ConfigurationLoader +import com.expedia.www.haystack.commons.entities.encoders.{Encoder, EncoderFactory} +import com.expedia.www.haystack.commons.kstreams.MetricDataTimestampExtractor +import com.expedia.www.haystack.commons.kstreams.serde.metricdata.MetricDataSerializer +import com.expedia.www.haystack.trends.config.entities._ +import com.typesafe.config.{Config, ConfigFactory, ConfigValueType} +import org.apache.kafka.clients.producer.ProducerConfig +import org.apache.kafka.clients.producer.ProducerConfig.{KEY_SERIALIZER_CLASS_CONFIG, VALUE_SERIALIZER_CLASS_CONFIG} +import org.apache.kafka.streams.StreamsConfig +import org.apache.kafka.streams.Topology.AutoOffsetReset +import org.apache.kafka.streams.processor.TimestampExtractor + +import scala.collection.JavaConverters._ + +class AppConfiguration { + private val config = ConfigurationLoader.loadConfigFileWithEnvOverrides() + + val healthStatusFilePath: String = config.getString("health.status.path") + private val kafka = config.getConfig("kafka") + private val producerConfig = kafka.getConfig("producer") + private val consumerConfig = kafka.getConfig("consumer") + private val streamsConfig = kafka.getConfig("streams") + + + + /** + * + * @return type of encoder to use on metricpoint key names + */ + def encoder: Encoder = { + val encoderType = config.getString("metricpoint.encoder.type") + EncoderFactory.newInstance(encoderType) + } + + + /** + * + * @return configurations specific to creating HDR histogram objects + */ + def histogramMetricConfiguration: HistogramMetricConfiguration = { + val histCfg = config.getConfig("histogram") + + HistogramMetricConfiguration( + histCfg.getInt("precision"), + histCfg.getInt("max.value"), + HistogramUnit.from(histCfg.getString("value.unit"))) + } + + /** + * + * @return state store stream config while aggregating + */ + def stateStoreConfig: StateStoreConfiguration = { + + val stateStoreConfigs = config.getConfig("state.store") + + + val cacheSize = stateStoreConfigs.getInt("cache.size") + val enableChangeLog = stateStoreConfigs.getBoolean("enable.logging") + val changeLogDelayInSecs = stateStoreConfigs.getInt("logging.delay.seconds") + + val changeLogTopicConfiguration = if (stateStoreConfigs.getConfig("changelog.topic").isEmpty) { + Map[String, String]() + } else { + stateStoreConfigs.getConfig("changelog.topic").entrySet().asScala.map(entry => entry.getKey -> entry.getValue.unwrapped().toString).toMap + } + + StateStoreConfiguration(cacheSize, enableChangeLog, changeLogDelayInSecs, changeLogTopicConfiguration) + + + } + + /** + * + * @return streams configuration object + */ + def kafkaConfig: KafkaConfiguration = { + + // verify if the applicationId and bootstrap server config are non empty + def verifyRequiredProps(props: Properties): Unit = { + require(props.getProperty(StreamsConfig.APPLICATION_ID_CONFIG).nonEmpty) + require(props.getProperty(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG).nonEmpty) + } + + def addProps(config: Config, props: Properties, prefix: (String) => String = identity): Unit = { + config.entrySet().asScala.foreach(kv => { + val propKeyName = prefix(kv.getKey) + props.setProperty(propKeyName, kv.getValue.unwrapped().toString) + }) + } + + def getExternalKafkaProps(producerConfig: Config): Option[Properties] = { + + if (producerConfig.getBoolean("enable.external.kafka.produce")) { + val props = new Properties() + val kafkaProducerProps = producerConfig.getConfig("props") + + kafkaProducerProps.entrySet() forEach { + kv => { + props.setProperty(kv.getKey, kv.getValue.unwrapped().toString) + } + } + + props.put(KEY_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer") + props.put(VALUE_SERIALIZER_CLASS_CONFIG, classOf[MetricDataSerializer].getCanonicalName) + + require(props.getProperty(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG).nonEmpty) + Option(props) + } else { + Option.empty + } + } + + /** + * + * @return returns the kafka autoreset configuration + */ + def getKafkaAutoReset: AutoOffsetReset = { + if (streamsConfig.hasPath("auto.offset.reset")) AutoOffsetReset.valueOf(streamsConfig.getString("auto.offset.reset").toUpperCase) + else AutoOffsetReset.LATEST + } + + + val props = new Properties + // add stream specific properties + addProps(streamsConfig, props) + // validate props + verifyRequiredProps(props) + + val timestampExtractor = Option(props.getProperty("timestamp.extractor")) match { + case Some(timeStampExtractorClass) => + Class.forName(timeStampExtractorClass).newInstance().asInstanceOf[TimestampExtractor] + case None => + new MetricDataTimestampExtractor + } + + //set timestamp extractor + props.setProperty("timestamp.extractor", timestampExtractor.getClass.getName) + + val kafkaSinkTopicConfig = producerConfig.getConfigList("topics").asScala + + val kafkaSinkTopics = kafkaSinkTopicConfig.map(sinkTopic => + KafkaSinkTopic(sinkTopic.getString("topic"), sinkTopic.getString("serdeClassName"), + sinkTopic.getBoolean("enabled"))) + + KafkaConfiguration( + new StreamsConfig(props), + producerConfig = KafkaProduceConfiguration(kafkaSinkTopics.toList, getExternalKafkaProps(producerConfig), + producerConfig.getString("external.kafka.topic"), producerConfig.getBoolean("enable.external.kafka.produce")), + consumeTopic = consumerConfig.getString("topic"), + getKafkaAutoReset, + timestampExtractor, + kafka.getLong("close.timeout.ms")) + } + + def additionalTags: Map[String, String] = { + val additionalTagsConfig = config.getValue("additionalTags").valueType() match { + case ConfigValueType.OBJECT => config.getConfig("additionalTags") + case _ => ConfigFactory.parseString(config.getString("additionalTags")) + } + additionalTagsConfig.entrySet().asScala.map(entrySet => entrySet.getKey -> entrySet.getValue.unwrapped().toString) toMap + } + +} + +object AppConfiguration extends AppConfiguration + + diff --git a/trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/config/entities/HistogramMetricConfiguration.scala b/trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/config/entities/HistogramMetricConfiguration.scala new file mode 100644 index 000000000..27026ac59 --- /dev/null +++ b/trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/config/entities/HistogramMetricConfiguration.scala @@ -0,0 +1,47 @@ +/* + * + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.trends.config.entities + +import com.expedia.www.haystack.trends.config.entities.HistogramUnit.HistogramUnit + + +/** + *This configuration helps create the HistogramMetric. + * + * @param precision - Decimal precision required for the histogram,allowable precision of histogram must be 0 <= value <= 5 + * @param maxValue - maximum value for the incoming metric (should always be > than the maximum value you're expecting for a metricpoint) + * @param unit - unit of the value that will be given to histogram (can be micros, millis, seconds) + */ +case class HistogramMetricConfiguration(precision: Int, maxValue: Int, unit: HistogramUnit) + +object HistogramUnit extends Enumeration { + type HistogramUnit = Value + val MILLIS, MICROS, SECONDS = Value + + def from(unit: String): HistogramUnit = { + unit.toLowerCase() match { + case "millis" => HistogramUnit.MILLIS + case "micros" => HistogramUnit.MICROS + case "seconds" => HistogramUnit.SECONDS + case _ => throw new RuntimeException( + String.format("Fail to understand the histogram unit %s, should be one of [millis, micros or seconds]", unit)) + } + } + + def default: HistogramUnit = HistogramUnit.MICROS +} diff --git a/trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/config/entities/KafkaConfiguration.scala b/trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/config/entities/KafkaConfiguration.scala new file mode 100644 index 000000000..67b104738 --- /dev/null +++ b/trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/config/entities/KafkaConfiguration.scala @@ -0,0 +1,37 @@ +/* + * + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.trends.config.entities + +import org.apache.kafka.streams.StreamsConfig +import org.apache.kafka.streams.Topology.AutoOffsetReset +import org.apache.kafka.streams.processor.TimestampExtractor + +/** + * @param streamsConfig config object to be used for initializing KafkaStreams + * @param producerConfig producer config + * @param consumeTopic consumer topic + * @param autoOffsetReset auto offset reset policy + * @param timestampExtractor timestamp extractor + * @param closeTimeoutInMs timeout for closing kafka streams in ms + */ +case class KafkaConfiguration(streamsConfig: StreamsConfig, + producerConfig: KafkaProduceConfiguration, + consumeTopic: String, + autoOffsetReset: AutoOffsetReset, + timestampExtractor: TimestampExtractor, + closeTimeoutInMs: Long) diff --git a/trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/config/entities/KafkaProduceConfiguration.scala b/trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/config/entities/KafkaProduceConfiguration.scala new file mode 100644 index 000000000..4fccffae2 --- /dev/null +++ b/trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/config/entities/KafkaProduceConfiguration.scala @@ -0,0 +1,30 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trends.config.entities + +import java.util.Properties + +/** + *This configuration specifies if the stream topology writes the aggregated metrics to an external kafka cluster + * @param kafkaSinkTopics - list of all sinks along with the serdes. + * @param props - Kafka producer configuration + * @param enableExternalKafka - enable/disable external kafka sink + */ +case class KafkaProduceConfiguration(kafkaSinkTopics: List[KafkaSinkTopic], props: Option[Properties], externalKafkaTopic: String, enableExternalKafka: Boolean) + +case class KafkaSinkTopic(topic: String, serdeClassName:String, enabled: Boolean) diff --git a/trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/config/entities/StateStoreConfiguration.scala b/trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/config/entities/StateStoreConfiguration.scala new file mode 100644 index 000000000..d6c238006 --- /dev/null +++ b/trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/config/entities/StateStoreConfiguration.scala @@ -0,0 +1,30 @@ +/* + * + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.trends.config.entities + + +/** + * This class contains configurations specific to the kafka streams state store for keeping the trend metrics being computed + * + * @param stateStoreCacheSize - max number of trends which can be computed by single state store (number * number of stream tasks * size of a trendMetric in memory should be < heap of the process) + * @param enableChangeLogging - enable/disable chanelogging - This helps in recreating the state in the app crashes or partition reassignment + * @param changeLogDelayInSecs - Interval at which the state should be check pointed at the changelog topic in kafka + * @param changeLogTopicConfiguration - Configuration specific to kafka changelog topic - refer to + */ + +case class StateStoreConfiguration(stateStoreCacheSize: Int, enableChangeLogging: Boolean, changeLogDelayInSecs: Int, changeLogTopicConfiguration: Map[String, String]) diff --git a/trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/kstream/Streams.scala b/trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/kstream/Streams.scala new file mode 100644 index 000000000..483371eb1 --- /dev/null +++ b/trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/kstream/Streams.scala @@ -0,0 +1,119 @@ +/* + * + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trends.kstream + +import java.util.function.Supplier + +import com.expedia.metrics.MetricData +import com.expedia.www.haystack.commons.kstreams.serde.metricdata.{MetricDataSerde, MetricTankSerde} +import com.expedia.www.haystack.trends.aggregation.TrendMetric +import com.expedia.www.haystack.trends.config.AppConfiguration +import com.expedia.www.haystack.trends.kstream.processor.{AdditionalTagsProcessorSupplier, ExternalKafkaProcessorSupplier, MetricAggProcessorSupplier} +import com.expedia.www.haystack.trends.kstream.store.HaystackStoreBuilder +import org.apache.kafka.common.serialization.{Serde, StringDeserializer, StringSerializer} +import org.apache.kafka.streams.Topology +import org.apache.kafka.streams.state.{KeyValueStore, StoreBuilder} +import org.slf4j.LoggerFactory + +import scala.collection.JavaConverters + +class Streams(appConfiguration: AppConfiguration) extends Supplier[Topology] { + + private val LOGGER = LoggerFactory.getLogger(classOf[Streams]) + private val TOPOLOGY_SOURCE_NAME = "metricpoint-source" + private val TOPOLOGY_EXTERNAL_SINK_NAME = "metricpoint-aggegated-sink-external" + private val TOPOLOGY_INTERNAL_SINK_NAME = "metric-data-aggegated-sink-internal" + private val TOPOLOGY_AGGREGATOR_PROCESSOR_NAME = "metricpoint-aggregator-process" + private val TOPOLOGY_ADDITIONAL_TAGS_PROCESSOR_NAME = "additional-tags-process" + private val TOPOLOGY_AGGREGATOR_TREND_METRIC_STORE_NAME = "trend-metric-store" + private val kafkaConfig = appConfiguration.kafkaConfig + + private def initialize(topology: Topology): Topology = { + + //add source - topic where the raw metricpoints are pushed by the span-timeseries-transformer + topology.addSource( + kafkaConfig.autoOffsetReset, + TOPOLOGY_SOURCE_NAME, + kafkaConfig.timestampExtractor, + new StringDeserializer, + new MetricTankSerde().deserializer(), + kafkaConfig.consumeTopic) + + + //The processor which performs aggregations on the metrics + topology.addProcessor( + TOPOLOGY_AGGREGATOR_PROCESSOR_NAME, + new MetricAggProcessorSupplier(TOPOLOGY_AGGREGATOR_TREND_METRIC_STORE_NAME, appConfiguration.encoder), + TOPOLOGY_SOURCE_NAME) + + + //key-value, state store associated with each kstreams task(partition) + // which keeps the trend-metrics which are currently being computed in memory + topology.addStateStore(createTrendMetricStateStore(), TOPOLOGY_AGGREGATOR_PROCESSOR_NAME) + + // topology to add additional tags if any + topology.addProcessor(TOPOLOGY_ADDITIONAL_TAGS_PROCESSOR_NAME, new AdditionalTagsProcessorSupplier(appConfiguration.additionalTags), TOPOLOGY_AGGREGATOR_PROCESSOR_NAME) + + if (appConfiguration.kafkaConfig.producerConfig.enableExternalKafka) { + topology.addProcessor( + TOPOLOGY_EXTERNAL_SINK_NAME, + new ExternalKafkaProcessorSupplier(appConfiguration.kafkaConfig.producerConfig), + TOPOLOGY_ADDITIONAL_TAGS_PROCESSOR_NAME + ) + } + + // adding sinks + appConfiguration.kafkaConfig.producerConfig.kafkaSinkTopics.foreach(sinkTopic => { + if(sinkTopic.enabled){ + val serde = Class.forName(sinkTopic.serdeClassName).newInstance().asInstanceOf[Serde[MetricData]] + topology.addSink( + s"${TOPOLOGY_INTERNAL_SINK_NAME}-${sinkTopic.topic}", + sinkTopic.topic, + new StringSerializer, + serde.serializer(), + TOPOLOGY_ADDITIONAL_TAGS_PROCESSOR_NAME) + } + }) + + topology + } + + + private def createTrendMetricStateStore(): StoreBuilder[KeyValueStore[String, TrendMetric]] = { + + val stateStoreConfiguration = appConfiguration.stateStoreConfig + + val storeBuilder = new HaystackStoreBuilder(TOPOLOGY_AGGREGATOR_TREND_METRIC_STORE_NAME, stateStoreConfiguration.stateStoreCacheSize) + + if (stateStoreConfiguration.enableChangeLogging) { + storeBuilder + .withLoggingEnabled(JavaConverters.mapAsJavaMap(stateStoreConfiguration.changeLogTopicConfiguration)) + + } else { + storeBuilder + .withLoggingDisabled() + } + } + + + override def get(): Topology = { + val topology = new Topology + initialize(topology) + } +} diff --git a/trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/kstream/processor/AdditionalTagsProcessorSupplier.scala b/trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/kstream/processor/AdditionalTagsProcessorSupplier.scala new file mode 100644 index 000000000..1aa6abe80 --- /dev/null +++ b/trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/kstream/processor/AdditionalTagsProcessorSupplier.scala @@ -0,0 +1,32 @@ +package com.expedia.www.haystack.trends.kstream.processor + +import java.util + +import com.expedia.metrics.{MetricData, MetricDefinition, TagCollection} +import com.expedia.www.haystack.commons.util.MetricDefinitionKeyGenerator.generateKey +import org.apache.kafka.streams.processor._ + +import scala.collection.JavaConverters._ + +class AdditionalTagsProcessorSupplier(additionalTags: Map[String, String]) extends ProcessorSupplier[String, MetricData] { + override def get(): Processor[String, MetricData] = new AdditionalTagsProcessor(additionalTags) +} + + +class AdditionalTagsProcessor(additionalTags: Map[String, String]) extends AbstractProcessor[String, MetricData] { + + override def process(key: String, value: MetricData): Unit = { + if (additionalTags.isEmpty) { + context().forward(key, value) + } + else { + val tags = new util.LinkedHashMap[String, String] { + putAll(value.getMetricDefinition.getTags.getKv) + putAll(additionalTags.asJava) + } + val metricDefinition = new MetricDefinition(value.getMetricDefinition.getKey, new TagCollection(tags), TagCollection.EMPTY) + val metricData = new MetricData(metricDefinition, value.getValue, value.getTimestamp) + context.forward(generateKey(metricData.getMetricDefinition), metricData) + } + } +} diff --git a/trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/kstream/processor/ExternalKafkaProcessorSupplier.scala b/trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/kstream/processor/ExternalKafkaProcessorSupplier.scala new file mode 100644 index 000000000..2828b5d6b --- /dev/null +++ b/trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/kstream/processor/ExternalKafkaProcessorSupplier.scala @@ -0,0 +1,81 @@ +/* + * + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.trends.kstream.processor + +import com.expedia.metrics.MetricData +import com.expedia.www.haystack.trends.config.entities.KafkaProduceConfiguration +import com.expedia.www.haystack.trends.kstream.serde.TrendMetricSerde.metricRegistry +import org.apache.kafka.clients.producer.{Callback, KafkaProducer, ProducerRecord, RecordMetadata} +import org.apache.kafka.streams.processor.{AbstractProcessor, Processor, ProcessorContext, ProcessorSupplier} +import org.slf4j.LoggerFactory + +class ExternalKafkaProcessorSupplier(kafkaProduceConfig: KafkaProduceConfiguration) extends ProcessorSupplier[String, MetricData] { + + private val LOGGER = LoggerFactory.getLogger(this.getClass) + private val metricPointExternalKafkaSuccessMeter = metricRegistry.meter("metricpoint.kafka-external.success") + private val metricPointExternalKafkaFailureMeter = metricRegistry.meter("metricpoint.kafka-external.failure") + + def get: Processor[String, MetricData] = { + new ExternalKafkaProcessor(kafkaProduceConfig: KafkaProduceConfiguration) + } + + /** + * This is the Processor which contains the map of unique trends consumed from the assigned partition and the corresponding trend metric for each trend + * Each trend is uniquely identified by the metricPoint key - which is a combination of the name and the list of tags. Its backed by a state store which keeps this map and has the + * ability to restore the map if/when the app restarts or when the assigned kafka partitions change + * + * @param kafkaProduceConfig - configuration to create kafka producer + */ + private class ExternalKafkaProcessor(kafkaProduceConfig: KafkaProduceConfiguration) extends AbstractProcessor[String, MetricData] { + + private val kafkaProducer: KafkaProducer[String, MetricData] = new KafkaProducer[String, MetricData](kafkaProduceConfig.props.get) + private val kafkaProduceTopic = kafkaProduceConfig.externalKafkaTopic + + @SuppressWarnings(Array("unchecked")) + override def init(context: ProcessorContext) { + super.init(context) + } + + /** + * tries to fetch the trend metric based on the key, if it exists it updates the trend metric else it tries to create a new trend metric and adds it to the store * + * + * @param key - key in the kafka record - should be MetricDefinitionKeyGenerator.generateKey(metricData.getMetricDefinition) + * @param value - metricData + */ + def process(key: String, value: MetricData): Unit = { + + val kafkaMessage = new ProducerRecord(kafkaProduceTopic, + key, value) + kafkaProducer.send(kafkaMessage, new Callback { + override def onCompletion(recordMetadata: RecordMetadata, e: Exception): Unit = { + if (e != null) { + LOGGER.error(s"Failed to produce the message to kafka for topic=$kafkaProduceTopic, with reason=", e) + metricPointExternalKafkaFailureMeter.mark() + } else { + metricPointExternalKafkaSuccessMeter.mark() + } + } + }) + } + } +} + + + + + diff --git a/trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/kstream/processor/MetricAggProcessorSupplier.scala b/trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/kstream/processor/MetricAggProcessorSupplier.scala new file mode 100644 index 000000000..ccb464096 --- /dev/null +++ b/trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/kstream/processor/MetricAggProcessorSupplier.scala @@ -0,0 +1,132 @@ +/* + * + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.trends.kstream.processor + +import com.codahale.metrics.{Counter, Meter} +import com.expedia.metrics.MetricData +import com.expedia.www.haystack.commons.entities.encoders.Encoder +import com.expedia.www.haystack.commons.entities.Interval +import com.expedia.www.haystack.commons.metrics.MetricsSupport +import com.expedia.www.haystack.commons.util.MetricDefinitionKeyGenerator.generateKey +import com.expedia.www.haystack.trends.aggregation.TrendMetric +import com.expedia.www.haystack.trends.aggregation.metrics._ +import com.expedia.www.haystack.trends.aggregation.rules.MetricRuleEngine +import org.apache.kafka.streams.kstream.internals._ +import org.apache.kafka.streams.processor.{AbstractProcessor, Processor, ProcessorContext} +import org.apache.kafka.streams.state.KeyValueStore +import org.slf4j.LoggerFactory + +class MetricAggProcessorSupplier(trendMetricStoreName: String, encoder: Encoder) extends KStreamAggProcessorSupplier[String, String, MetricData, TrendMetric] with MetricRuleEngine with MetricsSupport { + + private var sendOldValues: Boolean = false + private val LOGGER = LoggerFactory.getLogger(this.getClass) + + def get: Processor[String, MetricData] = { + new MetricAggProcessor(trendMetricStoreName) + } + + + def enableSendingOldValues() { + sendOldValues = true + } + + override def view(): KTableValueGetterSupplier[String, TrendMetric] = new KTableValueGetterSupplier[String, TrendMetric]() { + + override def get(): KTableValueGetter[String, TrendMetric] = new TrendMetricAggregateValueGetter() + + override def storeNames(): Array[String] = Array[String](trendMetricStoreName) + + private class TrendMetricAggregateValueGetter extends KTableValueGetter[String, TrendMetric] { + + private var store: KeyValueStore[String, TrendMetric] = _ + + @SuppressWarnings(Array("unchecked")) def init(context: ProcessorContext) { + store = context.getStateStore(trendMetricStoreName).asInstanceOf[KeyValueStore[String, TrendMetric]] + } + + def get(key: String): TrendMetric = store.get(key) + } + } + + /** + * This is the Processor which contains the map of unique trends consumed from the assigned partition and the corresponding trend metric for each trend + * Each trend is uniquely identified by the metricPoint key - which is a combination of the name and the list of tags. Its backed by a state store which keeps this map and has the + * ability to restore the map if/when the app restarts or when the assigned kafka partitions change + * + * @param trendMetricStoreName - name of the key-value state store + */ + private class MetricAggProcessor(trendMetricStoreName: String) extends AbstractProcessor[String, MetricData] { + private var trendMetricStore: KeyValueStore[String, TrendMetric] = _ + + + private var trendsCount: Counter = _ + private val invalidMetricPointMeter: Meter = metricRegistry.meter("metricprocessor.invalid") + + @SuppressWarnings(Array("unchecked")) + override def init(context: ProcessorContext) { + super.init(context) + trendsCount = metricRegistry.counter(s"metricprocessor.trendcount.${context.taskId()}") + trendMetricStore = context.getStateStore(trendMetricStoreName).asInstanceOf[KeyValueStore[String, TrendMetric]] + trendsCount.dec(trendsCount.getCount) + trendsCount.inc(trendMetricStore.approximateNumEntries()) + LOGGER.info(s"Triggering init for metric agg processor for task id ${context.taskId()}") + } + + /** + * tries to fetch the trend metric based on the key, if it exists it updates the trend metric else it tries to create a new trend metric and adds it to the store * + * + * @param key - key in the kafka record - should be metricPoint.getKey + * @param metricData - metricData + */ + def process(key: String, metricData: MetricData): Unit = { + if (key != null && metricData.getValue > 0) { + + // first get the matching windows + Option(trendMetricStore.get(key)).orElse(createTrendMetric(metricData)).foreach(trendMetric => { + trendMetric.compute(metricData) + + /* + we finally put the updated trend metric back to the store since we want the changelog the state store with the latest state of the trend metric, if we don't put the metric + back and update the mutable metric, the kstreams would not capture the change and app wouldn't be able to restore to the same state when the app comes back again. + */ + if (trendMetric.shouldLogToStateStore) { + trendMetricStore.put(key, trendMetric) + } + + //retrieve the computed metrics and push it to the kafka topic. + trendMetric.getComputedMetricPoints(metricData).foreach(metricPoint => { + context().forward(generateKey(metricData.getMetricDefinition), metricPoint) + }) + }) + } else { + invalidMetricPointMeter.mark() + } + } + + private def createTrendMetric(value: MetricData): Option[TrendMetric] = { + findMatchingMetric(value).map { + case AggregationType.Histogram => + trendsCount.inc() + TrendMetric.createTrendMetric(Interval.all, value, HistogramMetricFactory) + case AggregationType.Count => + trendsCount.inc() + TrendMetric.createTrendMetric(Interval.all, value, CountMetricFactory) + } + } + } +} diff --git a/trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/kstream/serde/TrendMetricSerde.scala b/trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/kstream/serde/TrendMetricSerde.scala new file mode 100644 index 000000000..6777bae59 --- /dev/null +++ b/trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/kstream/serde/TrendMetricSerde.scala @@ -0,0 +1,145 @@ +/* + * + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trends.kstream.serde + +import java.util + +import com.expedia.www.haystack.commons.entities.Interval +import com.expedia.www.haystack.commons.metrics.MetricsSupport +import com.expedia.www.haystack.trends.aggregation.TrendMetric +import com.expedia.www.haystack.trends.aggregation.metrics.{AggregationType, CountMetricFactory, HistogramMetricFactory} +import org.apache.kafka.common.serialization.{Deserializer, Serde, Serializer} +import org.msgpack.core.MessagePack +import org.msgpack.value.ValueFactory +import org.slf4j.LoggerFactory + +import scala.collection.JavaConverters._ +import scala.util.Try + +object TrendMetricSerde extends Serde[TrendMetric] with MetricsSupport { + + private val LOGGER = LoggerFactory.getLogger(this.getClass) + + private val trendMetricStatsDeserFailureMeter = metricRegistry.meter("trendmetric.deser.failure") + private val trendMetricStatsSerSuccessMeter = metricRegistry.meter("trendmetric.ser.success") + private val trendMetricStatsDeserSuccessMeter = metricRegistry.meter("trendmetric.deser.success") + private val INTERVAL_KEY: String = "interval" + private val TREND_METRIC_KEY: String = "trendMetric" + private val AGGREGATION_TYPE_KEY = "aggregationType" + private val METRICS_KEY = "metrics" + + override def deserializer(): Deserializer[TrendMetric] = { + new Deserializer[TrendMetric] { + override def configure(map: util.Map[String, _], b: Boolean): Unit = () + + override def close(): Unit = () + + /** + * converts the messagepack encoded bytes into trendMetric object + * + * @param topic topic associated with data + * @param data serialized bytes of trendMetric + * @return + */ + override def deserialize(topic: String, data: Array[Byte]): TrendMetric = { + Try { + val unpacker = MessagePack.newDefaultUnpacker(data) + val serializedWindowedMetric = unpacker.unpackValue().asMapValue().map() + val aggregationType = AggregationType.withName(serializedWindowedMetric.get(ValueFactory.newString(AGGREGATION_TYPE_KEY)).asStringValue().toString) + + val metricFactory = aggregationType match { + case AggregationType.Histogram => HistogramMetricFactory + case AggregationType.Count => CountMetricFactory + } + + val trendMetricMap = serializedWindowedMetric.get(ValueFactory.newString(METRICS_KEY)).asArrayValue().asScala.map(mapValue => { + val map = mapValue.asMapValue().map() + val intervalVal = map.get(ValueFactory.newString(INTERVAL_KEY)).asIntegerValue().asLong() + val interval = Interval.fromVal(intervalVal) + + val windowedMetricByteArray = map.get(ValueFactory.newString(TREND_METRIC_KEY)).asBinaryValue().asByteArray() + val windowedMetric = WindowedMetricSerde.deserializer().deserialize(topic, windowedMetricByteArray) + + interval -> windowedMetric + }).toMap + + val metric = TrendMetric.restoreTrendMetric(trendMetricMap, metricFactory) + trendMetricStatsDeserSuccessMeter.mark() + metric + + }.recover { + case ex: Exception => + LOGGER.error("failed to deserialize trend metric with exception", ex) + trendMetricStatsDeserFailureMeter.mark() + throw ex + }.toOption.orNull + } + } + } + + override def serializer(): Serializer[TrendMetric] = { + new Serializer[TrendMetric] { + override def configure(map: util.Map[String, _], b: Boolean): Unit = () + + /** + * converts the trendMetric object to encoded bytes + * + * @param topic topic associated with data + * @param trendMetric trendMetric object + * @return + */ + override def serialize(topic: String, trendMetric: TrendMetric): Array[Byte] = { + + val packer = MessagePack.newDefaultBufferPacker() + + if (trendMetric == null) { + LOGGER.error("TrendMetric is null") + null + } else if (trendMetric.trendMetricsMap == null) { + LOGGER.error("TrendMetric map is null") + null + } + else { + val serializedTrendMetric = trendMetric.trendMetricsMap.map { + case (interval, windowedMetric) => + ValueFactory.newMap(Map( + ValueFactory.newString(INTERVAL_KEY) -> ValueFactory.newInteger(interval.timeInSeconds), + ValueFactory.newString(TREND_METRIC_KEY) -> ValueFactory.newBinary(WindowedMetricSerde.serializer().serialize(topic, windowedMetric)) + ).asJava) + } + + val windowedMetricMessagePack = Map( + ValueFactory.newString(METRICS_KEY) -> ValueFactory.newArray(serializedTrendMetric.toList.asJava), + ValueFactory.newString(AGGREGATION_TYPE_KEY) -> ValueFactory.newString(trendMetric.getMetricFactory.getAggregationType.toString) + ) + packer.packValue(ValueFactory.newMap(windowedMetricMessagePack.asJava)) + val data = packer.toByteArray + trendMetricStatsSerSuccessMeter.mark() + data + } + } + + override def close(): Unit = () + } + } + + override def close(): Unit = () + + override def configure(map: util.Map[String, _], b: Boolean): Unit = () +} diff --git a/trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/kstream/serde/WindowedMetricSerde.scala b/trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/kstream/serde/WindowedMetricSerde.scala new file mode 100644 index 000000000..2a77daf42 --- /dev/null +++ b/trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/kstream/serde/WindowedMetricSerde.scala @@ -0,0 +1,125 @@ +/* + * + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trends.kstream.serde + +import java.util + +import com.expedia.www.haystack.commons.entities.Interval +import com.expedia.www.haystack.commons.metrics.MetricsSupport +import com.expedia.www.haystack.trends.aggregation.metrics.{AggregationType, CountMetricFactory, HistogramMetricFactory, Metric} +import com.expedia.www.haystack.trends.aggregation.{TrendMetric, WindowedMetric} +import com.expedia.www.haystack.trends.aggregation.entities.TimeWindow +import org.apache.kafka.common.serialization.{Deserializer, Serde, Serializer} +import org.msgpack.core.MessagePack +import org.msgpack.value.ValueFactory + +import scala.collection.JavaConverters._ +import scala.collection.mutable + + +object WindowedMetricSerde extends Serde[WindowedMetric] with MetricsSupport { + + private val SERIALIZED_METRIC_KEY = "serializedMetric" + private val START_TIME_KEY = "startTime" + private val END_TIME_KEY = "endTime" + + private val aggregationTypeKey = "aggregationType" + private val metricsKey = "metrics" + + override def close(): Unit = () + + override def deserializer(): Deserializer[WindowedMetric] = { + new Deserializer[WindowedMetric] { + override def configure(map: util.Map[String, _], b: Boolean): Unit = () + + override def close(): Unit = () + + /** + * converts the messagepack encoded bytes into windowedMetric object + * + * @param data serialized bytes of windowedMetric + * @return + */ + override def deserialize(topic: String, data: Array[Byte]): WindowedMetric = { + val unpacker = MessagePack.newDefaultUnpacker(data) + val serializedWindowedMetric = unpacker.unpackValue().asMapValue().map() + val aggregationType = AggregationType.withName(serializedWindowedMetric.get(ValueFactory.newString(aggregationTypeKey)).asStringValue().toString) + + val metricFactory = aggregationType match { + case AggregationType.Histogram => HistogramMetricFactory + case AggregationType.Count => CountMetricFactory + } + + val windowedMetricMap = mutable.TreeMap[TimeWindow, Metric]() + serializedWindowedMetric.get(ValueFactory.newString(metricsKey)).asArrayValue().asScala.map(mapValue => { + val map = mapValue.asMapValue().map() + val startTime = map.get(ValueFactory.newString(START_TIME_KEY)).asIntegerValue().asLong() + val endTime = map.get(ValueFactory.newString(END_TIME_KEY)).asIntegerValue().asLong() + val window = TimeWindow(startTime, endTime) + val metric = metricFactory.getMetricSerde.deserialize(map.get(ValueFactory.newString(SERIALIZED_METRIC_KEY)).asBinaryValue().asByteArray()) + windowedMetricMap.put(window, metric) + }) + + val intervalVal = windowedMetricMap.firstKey.endTime - windowedMetricMap.firstKey.startTime + val interval = Interval.fromVal(intervalVal) + val metric = WindowedMetric.restoreWindowedMetric(windowedMetricMap, metricFactory, TrendMetric.trendMetricConfig(interval)._1, interval) + metric + } + } + } + + + override def serializer(): Serializer[WindowedMetric] = { + new Serializer[WindowedMetric] { + override def configure(map: util.Map[String, _], b: Boolean): Unit = () + + /** + * converts the windowedMetric object to encoded bytes + * + * @param topic topic associated with data + * @param windowedMetric windowedMetric object + * @return + */ + override def serialize(topic: String, windowedMetric: WindowedMetric): Array[Byte] = { + + val packer = MessagePack.newDefaultBufferPacker() + + val serializedMetrics = windowedMetric.windowedMetricsMap.map { + case (timeWindow, metric) => + ValueFactory.newMap(Map( + ValueFactory.newString(START_TIME_KEY) -> ValueFactory.newInteger(timeWindow.startTime), + ValueFactory.newString(END_TIME_KEY) -> ValueFactory.newInteger(timeWindow.endTime), + ValueFactory.newString(SERIALIZED_METRIC_KEY) -> ValueFactory.newBinary(windowedMetric.getMetricFactory.getMetricSerde.serialize(metric)) + ).asJava) + } + val windowedMetricMessagePack = Map( + ValueFactory.newString(metricsKey) -> ValueFactory.newArray(serializedMetrics.toList.asJava), + ValueFactory.newString(aggregationTypeKey) -> ValueFactory.newString(windowedMetric.getMetricFactory.getAggregationType.toString) + ) + packer.packValue(ValueFactory.newMap(windowedMetricMessagePack.asJava)) + val data = packer.toByteArray + data + } + + override def close(): Unit = () + } + } + + override def configure(map: util.Map[String, _], b: Boolean): Unit = () +} diff --git a/trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/kstream/serde/metric/CountMetricSerde.scala b/trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/kstream/serde/metric/CountMetricSerde.scala new file mode 100644 index 000000000..3a2851f28 --- /dev/null +++ b/trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/kstream/serde/metric/CountMetricSerde.scala @@ -0,0 +1,41 @@ +package com.expedia.www.haystack.trends.kstream.serde.metric + +import com.expedia.www.haystack.commons.entities.Interval +import com.expedia.www.haystack.commons.entities.Interval.Interval +import com.expedia.www.haystack.trends.aggregation.metrics.{CountMetric, Metric} +import org.msgpack.core.MessagePack +import org.msgpack.value.{Value, ValueFactory} + +import scala.collection.JavaConverters._ + +/** + * Serde which lets us serialize and deserilize the count metric, this is used when we serialize/deserialize the windowedMetric which can internally contain count or histogram metric + * It uses messagepack to pack the object into bytes + */ +object CountMetricSerde extends MetricSerde { + + private val currentCountKey = "currentCount" + private val intervalKey = "interval" + + + override def serialize(metric: Metric): Array[Byte] = { + + val countMetric = metric.asInstanceOf[CountMetric] + val packer = MessagePack.newDefaultBufferPacker() + val metricData = Map[Value, Value]( + ValueFactory.newString(currentCountKey) -> ValueFactory.newInteger(countMetric.getCurrentCount), + ValueFactory.newString(intervalKey) -> ValueFactory.newString(countMetric.getMetricInterval.name) + ) + packer.packValue(ValueFactory.newMap(metricData.asJava)) + packer.toByteArray + } + + override def deserialize(data: Array[Byte]): Metric = { + val metric = MessagePack.newDefaultUnpacker(data).unpackValue().asMapValue().map() + val currentCount:Long = metric.get(ValueFactory.newString(currentCountKey)).asIntegerValue().asLong() + val interval:Interval = Interval.fromName(metric.get(ValueFactory.newString(intervalKey)).asStringValue().toString) + new CountMetric(interval,currentCount) + } + + +} diff --git a/trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/kstream/serde/metric/HistogramMetricSerde.scala b/trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/kstream/serde/metric/HistogramMetricSerde.scala new file mode 100644 index 000000000..d429f87b5 --- /dev/null +++ b/trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/kstream/serde/metric/HistogramMetricSerde.scala @@ -0,0 +1,70 @@ +package com.expedia.www.haystack.trends.kstream.serde.metric + +import java.nio.ByteBuffer + +import com.expedia.www.haystack.commons.entities.Interval +import com.expedia.www.haystack.commons.entities.Interval.Interval +import com.expedia.www.haystack.trends.aggregation.TrendHdrHistogram +import com.expedia.www.haystack.trends.aggregation.metrics.{HistogramMetric, Metric} +import com.expedia.www.haystack.trends.config.AppConfiguration +import com.expedia.www.haystack.trends.config.entities.HistogramUnit +import org.HdrHistogram.Histogram +import org.msgpack.core.MessagePack +import org.msgpack.value.{Value, ValueFactory} +import org.slf4j.LoggerFactory + +import scala.collection.JavaConverters._ + +/** + * Serde which lets us serialize and deserilize the histogram metric, this is used when we serialize/deserialize the windowedMetric which can internally contain count or histogram metric + * It uses messagepack to pack the object into bytes + */ +object HistogramMetricSerde extends MetricSerde { + private val LOGGER = LoggerFactory.getLogger(HistogramMetricSerde.getClass) + + private val intHistogramKey = "intHistogram" + private val intervalKey = "interval" + private val unitKey = "unit" + private val highestTrackableValKey = "highestTrackableVal" + + override def serialize(metric: Metric): Array[Byte] = { + + val histogramMetric = metric.asInstanceOf[HistogramMetric] + val packer = MessagePack.newDefaultBufferPacker() + val runningHistogram = histogramMetric.getRunningHistogram + val serializedHistogram = ByteBuffer.allocate(runningHistogram.getEstimatedFootprintInBytes) + runningHistogram.encodeIntoByteBuffer(serializedHistogram) + val metricData = Map[Value, Value]( + ValueFactory.newString(intHistogramKey) -> ValueFactory.newBinary(serializedHistogram.array()), + ValueFactory.newString(intervalKey) -> ValueFactory.newString(metric.getMetricInterval.name), + ValueFactory.newString(unitKey) -> ValueFactory.newString(histogramMetric.getRunningHistogram.unit.toString), + ValueFactory.newString(highestTrackableValKey) -> ValueFactory.newInteger(histogramMetric.getRunningHistogram.getHighestTrackableValue) + ) + packer.packValue(ValueFactory.newMap(metricData.asJava)) + packer.toByteArray + + } + + override def deserialize(data: Array[Byte]): HistogramMetric = { + val metric = MessagePack.newDefaultUnpacker(data).unpackValue().asMapValue().map() + val serializedHistogram = metric.get(ValueFactory.newString(intHistogramKey)).asBinaryValue().asByteArray + val interval: Interval = Interval.fromName(metric.get(ValueFactory.newString(intervalKey)).asStringValue().toString) + + // before the unit concept is introduced, default histogram recorded value's unit was micros + val unitValue = metric.get(ValueFactory.newString(unitKey)) + val histogramUnit = if(unitValue == null) HistogramUnit.default else HistogramUnit.from(unitValue.asStringValue().toString) + + val highestTrackableVal = metric.get(ValueFactory.newString(highestTrackableValKey)) + val maxTrackableHistogramVal = if (highestTrackableVal == null) Int.MaxValue else highestTrackableVal.asIntegerValue().toInt + + try { + val hdrHistogram = Histogram.decodeFromByteBuffer(ByteBuffer.wrap(serializedHistogram), maxTrackableHistogramVal) + new HistogramMetric(interval, TrendHdrHistogram(hdrHistogram, histogramUnit)) + } catch { + case ex: Exception => + LOGGER.error("Fail to deserialize the hdr histogram with error", ex) + // create a default hdr histogram using the config + new HistogramMetric(interval, new TrendHdrHistogram(AppConfiguration.histogramMetricConfiguration)) + } + } +} diff --git a/trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/kstream/serde/metric/MetricSerde.scala b/trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/kstream/serde/metric/MetricSerde.scala new file mode 100644 index 000000000..f79ec3798 --- /dev/null +++ b/trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/kstream/serde/metric/MetricSerde.scala @@ -0,0 +1,8 @@ +package com.expedia.www.haystack.trends.kstream.serde.metric + +import com.expedia.www.haystack.trends.aggregation.metrics.Metric + +trait MetricSerde { + def serialize(metric:Metric): Array[Byte] + def deserialize(data: Array[Byte]) : Metric +} diff --git a/trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/kstream/store/HaystackStoreBuilder.scala b/trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/kstream/store/HaystackStoreBuilder.scala new file mode 100644 index 000000000..f5f0c358b --- /dev/null +++ b/trends/timeseries-aggregator/src/main/scala/com/expedia/www/haystack/trends/kstream/store/HaystackStoreBuilder.scala @@ -0,0 +1,52 @@ +package com.expedia.www.haystack.trends.kstream.store + +import java.util + +import com.expedia.www.haystack.trends.aggregation.TrendMetric +import com.expedia.www.haystack.trends.kstream.serde.TrendMetricSerde +import org.apache.kafka.common.serialization.Serdes.StringSerde +import org.apache.kafka.streams.state.internals.InMemoryLRUCacheStoreSupplier +import org.apache.kafka.streams.state.{KeyValueStore, StoreBuilder} + +import scala.collection.JavaConverters._ +import scala.collection.mutable + + +class HaystackStoreBuilder(storeName: String, maxCacheSize: Int) extends StoreBuilder[KeyValueStore[String, TrendMetric]] { + + private var changeLogEnabled = false + private var changeLogProperties = mutable.Map[String, String]() + + override def loggingEnabled(): Boolean = { + changeLogEnabled + } + + override def withLoggingEnabled(config: util.Map[String, String]): StoreBuilder[KeyValueStore[String, TrendMetric]] = { + changeLogEnabled = true + changeLogProperties = config.asScala + this + } + + override def logConfig(): util.Map[String, String] = changeLogProperties.asJava + + override def name(): String = { + storeName + } + + override def withCachingEnabled(): StoreBuilder[KeyValueStore[String, TrendMetric]] = { + changeLogEnabled = true + this + } + + override def build(): KeyValueStore[String, TrendMetric] = { + val lRUCacheStoreSupplier = new InMemoryLRUCacheStoreSupplier[String, TrendMetric](storeName, maxCacheSize, new StringSerde, TrendMetricSerde, loggingEnabled(), logConfig()) + lRUCacheStoreSupplier.get().asInstanceOf[KeyValueStore[String, TrendMetric]] + } + + + override def withLoggingDisabled(): StoreBuilder[KeyValueStore[String, TrendMetric]] = { + changeLogEnabled = false + changeLogProperties.clear() + this + } +} diff --git a/trends/timeseries-aggregator/src/test/resources/config/base.conf b/trends/timeseries-aggregator/src/test/resources/config/base.conf new file mode 100644 index 000000000..0bb8be6b0 --- /dev/null +++ b/trends/timeseries-aggregator/src/test/resources/config/base.conf @@ -0,0 +1,66 @@ +health.status.path = "/app/isHealthy" + +kafka { + close.timeout.ms = 30000 + + streams { + application.id = "haystack-timeseries-aggregator-dev" + bootstrap.servers = "192.168.99.100:9092" + num.stream.threads = 1 + commit.interval.ms = 3000 + auto.offset.reset = latest + timestamp.extractor = "com.expedia.www.haystack.commons.kstreams.MetricDataTimestampExtractor" + } + + + // For producing data to external and internal (both) kafka: set enable.external.kafka.produce to true and uncomment the props. + // For producing to same (internal) kafka: set enable.external.kafka.produce to false and comment the props. + producer { + topics : [ + { + topic: "metrics" + serdeClassName : "com.expedia.www.haystack.commons.kstreams.serde.metricdata.MetricDataSerde" + enabled: true + }, + { + topic: "mdm" + serdeClassName : "com.expedia.www.haystack.commons.kstreams.serde.metricdata.MetricTankSerde" + enabled: true + } + ] + enable.external.kafka.produce = true + external.kafka.topic = "mdm" + props { + bootstrap.servers = "kafkasvc:9092" + } + } + + consumer { + topic = "metric-data-points" + } +} + +state.store { + cleanup.policy = "compact,delete" + retention.ms = 14400000 // 4Hrs +} + +statestore { + enable.logging = true + logging.delay.seconds = 60 +} + +metricpoint.encoder.type = "periodreplacement" + +histogram { + max.value = 1800000 // 30 mins + precision = 2 + value.unit = "millis" // can be micros / millis / seconds +} + +// additional tags to be passed as part of metric data +// It can be of format typesafe hocon config such as +// additionalTags = {key: "value", key2:"value2"} +// or json such as +// additionalTags = """{"key": "value", "key2":"value2"}""" +additionalTags = """{"key1": "value1", "key2":"value2"}""" diff --git a/trends/timeseries-aggregator/src/test/resources/logback-test.xml b/trends/timeseries-aggregator/src/test/resources/logback-test.xml new file mode 100644 index 000000000..adfa02c68 --- /dev/null +++ b/trends/timeseries-aggregator/src/test/resources/logback-test.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/trends/timeseries-aggregator/src/test/scala/com/expedia/www/haystack/trends/feature/FeatureSpec.scala b/trends/timeseries-aggregator/src/test/scala/com/expedia/www/haystack/trends/feature/FeatureSpec.scala new file mode 100644 index 000000000..6d88865f5 --- /dev/null +++ b/trends/timeseries-aggregator/src/test/scala/com/expedia/www/haystack/trends/feature/FeatureSpec.scala @@ -0,0 +1,84 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.trends.feature + +import java.util +import java.util.Properties + +import com.expedia.metrics.{MetricData, MetricDefinition, TagCollection} +import com.expedia.www.haystack.commons.entities.encoders.PeriodReplacementEncoder +import com.expedia.www.haystack.trends.config.AppConfiguration +import com.expedia.www.haystack.trends.config.entities.{KafkaConfiguration, KafkaProduceConfiguration, KafkaSinkTopic, StateStoreConfiguration} +import org.apache.kafka.streams.StreamsConfig +import org.apache.kafka.streams.Topology.AutoOffsetReset +import org.apache.kafka.streams.processor.WallclockTimestampExtractor +import org.easymock.EasyMock +import org.scalatest._ +import org.scalatest.easymock.EasyMockSugar +import org.mockito.Mockito._ + +import scala.collection.JavaConverters._ + + +trait FeatureSpec extends FeatureSpecLike with GivenWhenThen with Matchers with EasyMockSugar { + + def currentTimeInSecs: Long = { + System.currentTimeMillis() / 1000l + } + + protected def mockAppConfig: AppConfiguration = { + val kafkaConsumeTopic = "test-consume" + val kafkaProduceTopic = "test-produce" + val kafkaMetricTankProduceTopic = "test-mdm-produce" + val streamsConfig = new Properties() + streamsConfig.put(StreamsConfig.APPLICATION_ID_CONFIG, "test-app") + streamsConfig.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, "test-kafka-broker") + + val kafkaSinkTopics = List(KafkaSinkTopic("metrics","com.expedia.www.haystack.commons.kstreams.serde.metricdata.MetricDataSerde",true), KafkaSinkTopic("mdm","com.expedia.www.haystack.commons.kstreams.serde.metricdata.MetricTankSerde",true)) + val kafkaConfig = KafkaConfiguration(new StreamsConfig(streamsConfig), KafkaProduceConfiguration(kafkaSinkTopics, None, "mdm", false), kafkaConsumeTopic, AutoOffsetReset.EARLIEST, new WallclockTimestampExtractor, 30000) + val projectConfiguration = mock[AppConfiguration] + + expecting { + projectConfiguration.kafkaConfig.andReturn(kafkaConfig).anyTimes() + projectConfiguration.encoder.andReturn(new PeriodReplacementEncoder).anyTimes() + projectConfiguration.stateStoreConfig.andReturn(StateStoreConfiguration(128, false, 60, Map())).anyTimes() + projectConfiguration.additionalTags.andReturn(Map("k1"->"v1", "k2"-> "v2")).anyTimes() + } + EasyMock.replay(projectConfiguration) + projectConfiguration + } + + protected def getMetricData(metricKey: String, tags: Map[String, String], value: Double, timeStamp: Long): MetricData = { + + val tagsMap = new java.util.LinkedHashMap[String, String] { + if (tags != null) putAll(tags.asJava) + put(MetricDefinition.MTYPE, "gauge") + put(MetricDefinition.UNIT, "short") + } + val metricDefinition = new MetricDefinition(metricKey, new TagCollection(tagsMap), TagCollection.EMPTY) + new MetricData(metricDefinition, value, timeStamp) + } + + protected def containsTagInMetricData(metricData: MetricData, tagKey: String, tagValue: String): Boolean = { + val tags = getTagsFromMetricData(metricData) + tags.containsKey(tagKey) && tags.get(tagKey).equalsIgnoreCase(tagValue) + } + + protected def getTagsFromMetricData(metricData: MetricData): util.Map[String, String] = { + metricData.getMetricDefinition.getTags.getKv + } +} diff --git a/trends/timeseries-aggregator/src/test/scala/com/expedia/www/haystack/trends/feature/tests/aggregation/TrendMetricSpec.scala b/trends/timeseries-aggregator/src/test/scala/com/expedia/www/haystack/trends/feature/tests/aggregation/TrendMetricSpec.scala new file mode 100644 index 000000000..63b488290 --- /dev/null +++ b/trends/timeseries-aggregator/src/test/scala/com/expedia/www/haystack/trends/feature/tests/aggregation/TrendMetricSpec.scala @@ -0,0 +1,151 @@ +/* + * + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trends.feature.tests.aggregation + +import com.expedia.metrics.MetricData +import com.expedia.www.haystack.commons.entities.Interval.Interval +import com.expedia.www.haystack.commons.entities.{Interval, TagKeys} +import com.expedia.www.haystack.trends.aggregation.TrendMetric +import com.expedia.www.haystack.trends.aggregation.entities.TimeWindow +import com.expedia.www.haystack.trends.aggregation.metrics.{CountMetric, CountMetricFactory, HistogramMetric, HistogramMetricFactory} +import com.expedia.www.haystack.trends.config.AppConfiguration +import com.expedia.www.haystack.trends.feature.FeatureSpec + +class TrendMetricSpec extends FeatureSpec { + + val SERVICE_NAME = "dummy_service" + val OPERATION_NAME = "dummy_operation" + val keys = Map(TagKeys.OPERATION_NAME_KEY -> OPERATION_NAME, + TagKeys.SERVICE_NAME_KEY -> SERVICE_NAME) + + val alternateMetricKeys = Map(TagKeys.OPERATION_NAME_KEY -> OPERATION_NAME.concat("_2"), + TagKeys.SERVICE_NAME_KEY -> SERVICE_NAME) + + + feature("Creating a TrendMetric") { + + scenario("should get Histogram aggregated MetricPoints post watermarked metrics") { + val DURATION_METRIC_NAME = "duration" + + Given("some duration MetricData points") + val intervals: List[Interval] = List(Interval.ONE_MINUTE) + val currentTime = 1 + val expectedMetric: HistogramMetric = new HistogramMetric(Interval.ONE_MINUTE) + val firstMetricData: MetricData = getMetricData(DURATION_METRIC_NAME, keys, 1, currentTime) + + When("creating a WindowedMetric and passing first MetricData") + val trendMetric: TrendMetric = TrendMetric.createTrendMetric(intervals, firstMetricData, HistogramMetricFactory) + trendMetric.compute(firstMetricData) + expectedMetric.compute(firstMetricData) + + Then("should return 0 MetricData points if we try to get within (watermark + 1) metrics") + trendMetric.getComputedMetricPoints(firstMetricData).size shouldBe 0 + trendMetric.shouldLogToStateStore shouldBe true + var i = TrendMetric.trendMetricConfig(intervals.head)._1 + while (i > 0) { + val secondMetricData: MetricData = getMetricData(DURATION_METRIC_NAME, keys, 2, currentTime + intervals.head.timeInSeconds * i) + trendMetric.compute(secondMetricData) + trendMetric.shouldLogToStateStore shouldBe true + trendMetric.getComputedMetricPoints(secondMetricData).size shouldEqual 0 + i = i - 1 + } + + When("adding another MetricData after watermark") + val metricDataAfterWatermark: MetricData = getMetricData(DURATION_METRIC_NAME, keys, 10, currentTime + intervals.head.timeInSeconds * (TrendMetric.trendMetricConfig(intervals.head)._1 + 1)) + trendMetric.compute(metricDataAfterWatermark) + val aggMetrics = trendMetric.getComputedMetricPoints(metricDataAfterWatermark) + aggMetrics.size shouldEqual 1 * 7 // HistogramMetric + + Then("values for histogram should same as expected") + expectedMetric.getRunningHistogram.getMean shouldEqual aggMetrics.find(metricData => containsTagInMetricData(metricData, TagKeys.STATS_KEY, "mean")).get.getValue + expectedMetric.getRunningHistogram.getMaxValue shouldEqual aggMetrics.find(metricData => containsTagInMetricData(metricData, TagKeys.STATS_KEY, "max")).get.getValue + expectedMetric.getRunningHistogram.getMinValue shouldEqual aggMetrics.find(metricData => containsTagInMetricData(metricData, TagKeys.STATS_KEY, "min")).get.getValue + expectedMetric.getRunningHistogram.getValueAtPercentile(99) shouldEqual aggMetrics.find(metricData => containsTagInMetricData(metricData, TagKeys.STATS_KEY, "*_99")).get.getValue + expectedMetric.getRunningHistogram.getValueAtPercentile(95) shouldEqual aggMetrics.find(metricData => containsTagInMetricData(metricData, TagKeys.STATS_KEY, "*_95")).get.getValue + expectedMetric.getRunningHistogram.getValueAtPercentile(50) shouldEqual aggMetrics.find(metricData => containsTagInMetricData(metricData, TagKeys.STATS_KEY, "*_50")).get.getValue + + Then("timestamp of the evicted metric should equal the endtime of that window") + aggMetrics.map(metricPoint => { + metricPoint.getTimestamp shouldEqual TimeWindow(firstMetricData.getTimestamp, intervals.head).endTime + }) + } + + scenario("should get count aggregated MetricPoint post watermarked metrics") { + val COUNT_METRIC_NAME = "span-received" + + Given("some count MetricPoints") + val intervals: List[Interval] = List(Interval.ONE_MINUTE, Interval.FIVE_MINUTE) + val currentTime = 1 + + val firstMetricData: MetricData = getMetricData(COUNT_METRIC_NAME, keys, 1, currentTime) + val trendMetric = TrendMetric.createTrendMetric(intervals, firstMetricData, CountMetricFactory) + trendMetric.compute(firstMetricData) + val expectedMetric: CountMetric = new CountMetric(Interval.FIVE_MINUTE) + expectedMetric.compute(firstMetricData) + + var i = TrendMetric.trendMetricConfig(intervals.last)._1 + while (i > 0) { + val secondMetricData: MetricData = getMetricData(COUNT_METRIC_NAME, keys, 1, currentTime + intervals.last.timeInSeconds * i) + trendMetric.compute(secondMetricData) + i = i - 1 + } + + When("adding another MetricPoint after watermark") + val metricDataAfterWatermark: MetricData = getMetricData(COUNT_METRIC_NAME, keys, 10, currentTime + intervals.last.timeInSeconds * (TrendMetric.trendMetricConfig(intervals.head)._1 + 1)) + trendMetric.compute(metricDataAfterWatermark) + val aggMetrics = trendMetric.getComputedMetricPoints(metricDataAfterWatermark) + + Then("values for count should same as expected") + expectedMetric.getCurrentCount shouldEqual aggMetrics.find(metricData => containsTagInMetricData(metricData, TagKeys.INTERVAL_KEY, "FiveMinute")).get.getValue + } + + scenario("should log to state store for different metrics based on timestamp") { + val COUNT_METRIC_NAME = "span-received" + + Given("multiple metricPoints for different operations") + val intervals: List[Interval] = List(Interval.ONE_MINUTE, Interval.FIVE_MINUTE) + val currentTime = 1 + + val firstMetricData: MetricData = getMetricData(COUNT_METRIC_NAME, keys, 1, currentTime) + val anotherMetricData: MetricData = getMetricData(COUNT_METRIC_NAME, alternateMetricKeys, 1, currentTime) + val trendMetric = TrendMetric.createTrendMetric(intervals, firstMetricData, CountMetricFactory) + val anotherTrendMetric = TrendMetric.createTrendMetric(intervals, anotherMetricData, CountMetricFactory) + trendMetric.compute(firstMetricData) + trendMetric.shouldLogToStateStore shouldBe true + + anotherTrendMetric.compute(anotherMetricData) + anotherTrendMetric.shouldLogToStateStore shouldBe true + + When("metricpoints are added to multiple trend metrics") + val secondMetricData: MetricData = getMetricData(COUNT_METRIC_NAME, keys, 1, currentTime + 1 + AppConfiguration.stateStoreConfig.changeLogDelayInSecs) + trendMetric.compute(secondMetricData) + val secondAnotherMetricData = getMetricData(COUNT_METRIC_NAME, alternateMetricKeys, 1, currentTime + 1 + AppConfiguration.stateStoreConfig.changeLogDelayInSecs) + anotherTrendMetric.compute(secondAnotherMetricData) + + Then("trend metric should log to state store") + trendMetric.shouldLogToStateStore shouldBe true + anotherTrendMetric.shouldLogToStateStore shouldBe true + + } + + + } + + +} diff --git a/trends/timeseries-aggregator/src/test/scala/com/expedia/www/haystack/trends/feature/tests/aggregation/WindowedMetricSpec.scala b/trends/timeseries-aggregator/src/test/scala/com/expedia/www/haystack/trends/feature/tests/aggregation/WindowedMetricSpec.scala new file mode 100644 index 000000000..8fb7dee83 --- /dev/null +++ b/trends/timeseries-aggregator/src/test/scala/com/expedia/www/haystack/trends/feature/tests/aggregation/WindowedMetricSpec.scala @@ -0,0 +1,163 @@ +/* + * + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trends.feature.tests.aggregation + +import com.expedia.metrics.MetricData +import com.expedia.www.haystack.commons.entities.Interval +import com.expedia.www.haystack.commons.entities.Interval.Interval +import com.expedia.www.haystack.trends.aggregation.WindowedMetric +import com.expedia.www.haystack.trends.aggregation.metrics.{CountMetric, HistogramMetric, HistogramMetricFactory} +import com.expedia.www.haystack.trends.feature.FeatureSpec + +class WindowedMetricSpec extends FeatureSpec { + + val DURATION_METRIC_NAME = "duration" + val SERVICE_NAME = "dummy_service" + val OPERATION_NAME = "dummy_operation" + val keys = Map(TagKeys.OPERATION_NAME_KEY -> OPERATION_NAME, + TagKeys.SERVICE_NAME_KEY -> SERVICE_NAME) + + object TagKeys { + val OPERATION_NAME_KEY = "operationName" + val SERVICE_NAME_KEY = "serviceName" + } + + feature("Creating a WindowedMetric") { + + scenario("should get aggregated MetricData List post watermarked metrics") { + + Given("some duration MetricData") + val durations: List[Long] = List(10, 140) + val intervals: List[Interval] = List(Interval.ONE_MINUTE, Interval.FIFTEEN_MINUTE) + + val metricDataList: List[MetricData] = durations.map(duration => getMetricData(DURATION_METRIC_NAME, keys, duration, currentTimeInSecs)) + + When("creating a WindowedMetric and passing some MetricData and aggregation type as Histogram") + val windowedMetric: WindowedMetric = WindowedMetric.createWindowedMetric(metricDataList.head, HistogramMetricFactory, watermarkedWindows = 1, Interval.ONE_MINUTE) + + metricDataList.indices.foreach(i => if (i > 0) { + windowedMetric.compute(metricDataList(i)) + }) + + val expectedMetric: HistogramMetric = new HistogramMetric(Interval.ONE_MINUTE) + metricDataList.foreach(metricData => expectedMetric.compute(metricData)) + + Then("should return 0 Metric Data Points if we try to get it before interval") + val aggregatedMetricPointsBefore: List[MetricData] = windowedMetric.getComputedMetricDataList(metricDataList.last) + aggregatedMetricPointsBefore.size shouldBe 0 + + When("adding a MetricData outside of first Interval") + val newMetricPointAfterFirstInterval: MetricData = getMetricData(DURATION_METRIC_NAME, keys, 80, currentTimeInSecs + intervals.head.timeInSeconds) + + windowedMetric.compute(newMetricPointAfterFirstInterval) + + val aggregatedMetricPointsAfterFirstInterval: List[MetricData] = windowedMetric.getComputedMetricDataList(metricDataList.last) + + //Have to fix dev code and then all the validation test + Then("should return the metric data for the previous interval") + + + When("adding a MetricData outside of second interval now") + expectedMetric.compute(newMetricPointAfterFirstInterval) + val newMetricPointAfterSecondInterval: MetricData = getMetricData(DURATION_METRIC_NAME, keys, 80, currentTimeInSecs + intervals(1).timeInSeconds) + windowedMetric.compute(newMetricPointAfterSecondInterval) + val aggregatedMetricPointsAfterSecondInterval: List[MetricData] = windowedMetric.getComputedMetricDataList(metricDataList.last) + + //Have to fix dev code and then all the validation test + Then("should return the metric points for the second interval") + } + + scenario("should skip aggregated MetricData List for duration values greater than permissible value post watermarked metrics") { + + Given("duration MetricData with duration values greater than permissible value") + val durations: List[Double] = List(4.576661E9, 5.57661E9) + val intervals: List[Interval] = List(Interval.ONE_MINUTE, Interval.FIFTEEN_MINUTE) + + val metricDataList: List[MetricData] = durations.map(duration => getMetricData(DURATION_METRIC_NAME, keys, duration, currentTimeInSecs)) + + When("creating a WindowedMetric and passing some MetricData and aggregation type as Histogram") + val windowedMetric: WindowedMetric = WindowedMetric.createWindowedMetric(metricDataList.head, HistogramMetricFactory, watermarkedWindows = 1, Interval.ONE_MINUTE) + + metricDataList.indices.foreach(i => if (i > 0) { + windowedMetric.compute(metricDataList(i)) + }) + + val expectedMetric: HistogramMetric = new HistogramMetric(Interval.ONE_MINUTE) + metricDataList.foreach(metricData => expectedMetric.compute(metricData)) + + Then("should return 0 Metric Data Points if we try to get it before interval") + val aggregatedMetricPointsBefore: List[MetricData] = windowedMetric.getComputedMetricDataList(metricDataList.last) + aggregatedMetricPointsBefore.size shouldBe 0 + + When("adding a MetricData outside of first Interval") + val newMetricPointAfterFirstInterval: MetricData = getMetricData(DURATION_METRIC_NAME, keys, 80, currentTimeInSecs + intervals.head.timeInSeconds) + + windowedMetric.compute(newMetricPointAfterFirstInterval) + + val aggregatedMetricPointsAfterFirstInterval: List[MetricData] = windowedMetric.getComputedMetricDataList(metricDataList.last) + + Then("should return the empty metric data for the previous interval") + aggregatedMetricPointsAfterFirstInterval.length shouldBe 0 + + + When("adding a MetricData outside of second interval now") + expectedMetric.compute(newMetricPointAfterFirstInterval) + val newMetricPointAfterSecondInterval: MetricData = getMetricData(DURATION_METRIC_NAME, keys, 80, currentTimeInSecs + intervals(1).timeInSeconds) + windowedMetric.compute(newMetricPointAfterSecondInterval) + val aggregatedMetricPointsAfterSecondInterval: List[MetricData] = windowedMetric.getComputedMetricDataList(metricDataList.last) + + //Have to fix dev code and then all the validation test + Then("should return the metric points for the second interval") + } + + scenario("should get aggregated MetricData points post maximum Interval") { + + Given("some duration MetricData points") + val durations: List[Long] = List(10, 140, 250) + val intervals: List[Interval] = List(Interval.ONE_MINUTE, Interval.FIFTEEN_MINUTE, Interval.ONE_HOUR) + + val metricDataList: List[MetricData] = durations.map(duration => getMetricData(DURATION_METRIC_NAME, keys, duration, currentTimeInSecs)) + + + When("creating a WindowedMetric and passing some MetricData points") + val windowedMetric: WindowedMetric = WindowedMetric.createWindowedMetric(metricDataList.head, HistogramMetricFactory, watermarkedWindows = 1, Interval.ONE_MINUTE) + + metricDataList.indices.foreach(i => if (i > 0) { + windowedMetric.compute(metricDataList(i)) + }) + + When("adding a MetricData point outside of max Interval") + val newMetricPointAfterMaxInterval: MetricData = getMetricData(DURATION_METRIC_NAME, keys, 80, currentTimeInSecs + intervals.last.timeInSeconds) + windowedMetric.compute(newMetricPointAfterMaxInterval) + val aggregatedMetricDataPointsAfterMaxInterval: List[MetricData] = windowedMetric.getComputedMetricDataList(metricDataList.last) + + Then("should return valid values for all count intervals") + + val expectedOneMinuteMetric: CountMetric = new CountMetric(Interval.ONE_MINUTE) + metricDataList.foreach(metricPoint => expectedOneMinuteMetric.compute(metricPoint)) + + val expectedFifteenMinuteMetric: CountMetric = new CountMetric(Interval.FIFTEEN_MINUTE) + metricDataList.foreach(metricPoint => expectedFifteenMinuteMetric.compute(metricPoint)) + + val expectedOneHourMetric: CountMetric = new CountMetric(Interval.ONE_HOUR) + metricDataList.foreach(metricPoint => expectedOneHourMetric.compute(metricPoint)) + } + } + +} diff --git a/trends/timeseries-aggregator/src/test/scala/com/expedia/www/haystack/trends/feature/tests/aggregation/metrics/CountMetricSpec.scala b/trends/timeseries-aggregator/src/test/scala/com/expedia/www/haystack/trends/feature/tests/aggregation/metrics/CountMetricSpec.scala new file mode 100644 index 000000000..336e79fb6 --- /dev/null +++ b/trends/timeseries-aggregator/src/test/scala/com/expedia/www/haystack/trends/feature/tests/aggregation/metrics/CountMetricSpec.scala @@ -0,0 +1,83 @@ +package com.expedia.www.haystack.trends.feature.tests.aggregation.metrics + +import com.expedia.metrics.MetricData +import com.expedia.www.haystack.commons.entities.Interval.Interval +import com.expedia.www.haystack.commons.entities.{Interval, TagKeys} +import com.expedia.www.haystack.trends.aggregation.metrics.{CountMetric, Metric} +import com.expedia.www.haystack.trends.aggregation.entities._ +import com.expedia.www.haystack.trends.feature.FeatureSpec +import scala.collection.JavaConverters._ + +class CountMetricSpec extends FeatureSpec { + + val DURATION_METRIC_NAME = "duration" + val SUCCESS_METRIC_NAME = "success-spans" + val INVALID_METRIC_NAME = "invalid_metric" + val SERVICE_NAME = "dummy_service" + val OPERATION_NAME = "dummy_operation" + + val keys = Map(TagKeys.OPERATION_NAME_KEY -> OPERATION_NAME, + TagKeys.SERVICE_NAME_KEY -> SERVICE_NAME) + + scenario("should compute the correct count for valid similar metric data points") { + + Given("some 'total-spans' metric data points") + val interval: Interval = Interval.FIFTEEN_MINUTE + + val metricDataList = List( + getMetricData(SUCCESS_METRIC_NAME, keys, 2, currentTimeInSecs), + getMetricData(SUCCESS_METRIC_NAME, keys, 4, currentTimeInSecs), + getMetricData(SUCCESS_METRIC_NAME, keys, 5, currentTimeInSecs)) + + + When("get metric is constructed") + val metric: Metric = new CountMetric(interval) + + When("MetricData are processed") + metricDataList.map(metricData => metric.compute(metricData)) + + val countMetricDataList: List[MetricData] = metric.mapToMetricDataList(metricDataList.last.getMetricDefinition.getKey, getTagsFromMetricData(metricDataList.last), metricDataList.last.getTimestamp) + + + Then("it should return a single aggregated metric data") + countMetricDataList.size shouldBe 1 + val countMetric = countMetricDataList.head + + Then("aggregated metric name be the original metric name") + metricDataList.foreach(metricData => { + countMetric.getMetricDefinition.getKey shouldEqual metricData.getMetricDefinition.getKey + }) + + Then("aggregated metric should contain of original metric tags") + metricDataList.foreach(metricData => { + getTagsFromMetricData(metricData).asScala.foreach(tag => { + val aggregatedMetricTag = countMetric.getMetricDefinition.getTags.getKv.get(tag._1) + aggregatedMetricTag should not be None + aggregatedMetricTag shouldBe tag._2 + }) + }) + + Then("aggregated metric name should be the same as the metricpoint name") + countMetricDataList + .map(countMetricPoint => + countMetricPoint.getMetricDefinition.getKey shouldEqual countMetric.getMetricDefinition.getKey) + + Then("aggregated metric should count metric type in tags") + getTagsFromMetricData(countMetric).get(TagKeys.STATS_KEY) should not be None + getTagsFromMetricData(countMetric).get(TagKeys.STATS_KEY) shouldEqual StatValue.COUNT.toString + + Then("aggregated metric should contain the correct interval name in tags") + getTagsFromMetricData(countMetric).get(TagKeys.INTERVAL_KEY) should not be None + getTagsFromMetricData(countMetric).get(TagKeys.INTERVAL_KEY) shouldEqual interval.name + + Then("should return valid aggregated value for count") + val totalSum = metricDataList.foldLeft(0f)((currentValue, point) => { + currentValue + point.getValue.toFloat + }) + totalSum shouldEqual countMetric.getValue + + + } + + +} diff --git a/trends/timeseries-aggregator/src/test/scala/com/expedia/www/haystack/trends/feature/tests/aggregation/metrics/HistogramMetricSpec.scala b/trends/timeseries-aggregator/src/test/scala/com/expedia/www/haystack/trends/feature/tests/aggregation/metrics/HistogramMetricSpec.scala new file mode 100644 index 000000000..c1ac9cd2b --- /dev/null +++ b/trends/timeseries-aggregator/src/test/scala/com/expedia/www/haystack/trends/feature/tests/aggregation/metrics/HistogramMetricSpec.scala @@ -0,0 +1,126 @@ +/* + * + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trends.feature.tests.aggregation.metrics + +import com.expedia.metrics.MetricData +import com.expedia.www.haystack.commons.entities.Interval.Interval +import com.expedia.www.haystack.commons.entities.{Interval, TagKeys} +import com.expedia.www.haystack.trends.aggregation.TrendHdrHistogram +import com.expedia.www.haystack.trends.aggregation.entities._ +import com.expedia.www.haystack.trends.aggregation.metrics.HistogramMetric +import com.expedia.www.haystack.trends.config.AppConfiguration +import com.expedia.www.haystack.trends.feature.FeatureSpec + +class HistogramMetricSpec extends FeatureSpec { + + val DURATION_METRIC_NAME = "duration" + val SUCCESS_METRIC_NAME = "success-spans" + val INVALID_METRIC_NAME = "invalid_metric" + val SERVICE_NAME = "dummy_service" + val OPERATION_NAME = "dummy_operation" + + val keys = Map(TagKeys.OPERATION_NAME_KEY -> OPERATION_NAME, + TagKeys.SERVICE_NAME_KEY -> SERVICE_NAME) + + feature("Creating a histogram metric") { + scenario("should get gauge metric type and stats for valid duration points") { + + Given("some duration Metric Data points") + val durations = List(10000000, 140000000) // in micros + val interval: Interval = Interval.ONE_MINUTE + + val metricDataList: List[MetricData] = durations.map(duration => getMetricData(DURATION_METRIC_NAME, keys, duration, currentTimeInSecs)) + + When("get metric is constructed") + val metric = new HistogramMetric(interval) + + When("MetricData points are processed") + metricDataList.map(metricData => metric.compute(metricData)) + val histMetricDataList: List[MetricData] = metric.mapToMetricDataList(metricDataList.last.getMetricDefinition.getKey, getTagsFromMetricData(metricDataList.last), metricDataList.last.getTimestamp) + + Then("aggregated metric name should be the same as the MetricData points name") + histMetricDataList + .map(histMetricData => + histMetricData.getMetricDefinition.getKey shouldEqual metricDataList.head.getMetricDefinition.getKey) + + Then("aggregated metric should contain of original metric tags") + histMetricDataList.foreach(histogramMetricData => { + val tags = histogramMetricData.getMetricDefinition.getTags.getKv + + keys.foreach(IncomingMetricPointTag => { + tags.get(IncomingMetricPointTag._1) should not be None + tags.get(IncomingMetricPointTag._1) shouldEqual IncomingMetricPointTag._2 + }) + + }) + + Then("aggregated metric should contain the correct interval name in tags") + histMetricDataList.map(histMetricData => { + getTagsFromMetricData(histMetricData).get(TagKeys.INTERVAL_KEY) should not be null + getTagsFromMetricData(histMetricData).get(TagKeys.INTERVAL_KEY) shouldEqual interval.name + }) + + Then("should return valid values for all stats types") + val expectedHistogram = new TrendHdrHistogram(AppConfiguration.histogramMetricConfiguration) + + metricDataList.foreach(metricPoint => { + expectedHistogram.recordValue(metricPoint.getValue.toLong) + }) + verifyHistogramMetricValues(histMetricDataList, expectedHistogram) + } + + scenario("should return nearest point to the maxTrackableValue as per the precision if point is larger than the Histogram maxValue") { + + Given("some duration Metric points") + + val maxTrackableValueInMillis = AppConfiguration.histogramMetricConfiguration.maxValue.toLong + val maxTrackableValueInMicros = maxTrackableValueInMillis * 1000 + val durations = List(10000, maxTrackableValueInMicros + 100000) // in micros + val interval: Interval = Interval.ONE_MINUTE + + val metricDataList: List[MetricData] = durations.map(duration => getMetricData(DURATION_METRIC_NAME, keys, duration, currentTimeInSecs)) + + When("get metric is constructed") + val metric = new HistogramMetric(interval) + + When("MetricData points are processed") + metricDataList.map(metricData => metric.compute(metricData)) + val histMetricDataList: List[MetricData] = metric.mapToMetricDataList(metricDataList.last.getMetricDefinition.getKey, getTagsFromMetricData(metricDataList.last), metricDataList.last.getTimestamp) + + + Then("the max should be the maxTrackableValue that was in the histogram boundaries") + histMetricDataList.filter(m => "max".equals(getTagsFromMetricData(m).get("stat").toString)).head.getValue shouldEqual 1794048000 + histMetricDataList.filter(m => "mean".equals(getTagsFromMetricData(m).get("stat").toString)).head.getValue shouldEqual 899077000 + histMetricDataList.filter(m => "*_95".equals(getTagsFromMetricData(m).get("stat").toString)).head.getValue shouldEqual 1794048000 + } + + def verifyHistogramMetricValues(resultingMetricPoints: List[MetricData], expectedHistogram: TrendHdrHistogram) = { + val resultingMetricPointsMap: Map[String, Float] = + resultingMetricPoints.map(resultingMetricPoint => getTagsFromMetricData(resultingMetricPoint).get(TagKeys.STATS_KEY) -> resultingMetricPoint.getValue.toFloat).toMap + + resultingMetricPointsMap(StatValue.MEAN.toString) shouldEqual expectedHistogram.getMean + resultingMetricPointsMap(StatValue.MAX.toString) shouldEqual expectedHistogram.getMaxValue + resultingMetricPointsMap(StatValue.MIN.toString) shouldEqual expectedHistogram.getMinValue + resultingMetricPointsMap(StatValue.PERCENTILE_95.toString) shouldEqual expectedHistogram.getValueAtPercentile(95) + resultingMetricPointsMap(StatValue.PERCENTILE_99.toString) shouldEqual expectedHistogram.getValueAtPercentile(99) + resultingMetricPointsMap(StatValue.STDDEV.toString) shouldEqual expectedHistogram.getStdDeviation + resultingMetricPointsMap(StatValue.MEDIAN.toString) shouldEqual expectedHistogram.getValueAtPercentile(50) + } + } +} diff --git a/trends/timeseries-aggregator/src/test/scala/com/expedia/www/haystack/trends/feature/tests/aggregation/rules/DurationMetricRuleSpec.scala b/trends/timeseries-aggregator/src/test/scala/com/expedia/www/haystack/trends/feature/tests/aggregation/rules/DurationMetricRuleSpec.scala new file mode 100644 index 000000000..f25b82f96 --- /dev/null +++ b/trends/timeseries-aggregator/src/test/scala/com/expedia/www/haystack/trends/feature/tests/aggregation/rules/DurationMetricRuleSpec.scala @@ -0,0 +1,55 @@ +/* + * + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trends.feature.tests.aggregation.rules + +import com.expedia.www.haystack.trends.aggregation.metrics.AggregationType +import com.expedia.www.haystack.trends.aggregation.rules.DurationMetricRule +import com.expedia.www.haystack.trends.feature.FeatureSpec + +class DurationMetricRuleSpec extends FeatureSpec with DurationMetricRule { + + val DURATION_METRIC_NAME = "duration" + val SERVICE_NAME = "dummy_service" + val OPERATION_NAME = "dummy_operation" + + object TagKeys { + val OPERATION_NAME_KEY = "operationName" + val SERVICE_NAME_KEY = "serviceName" + } + + feature("DurationMetricRule for identifying MetricRule") { + + scenario("should get Histogram AggregationType for duration MetricData") { + + Given("a duration MetricPoint") + val keys = Map(TagKeys.OPERATION_NAME_KEY -> OPERATION_NAME, + TagKeys.SERVICE_NAME_KEY -> SERVICE_NAME) + val duration = 10 + val startTime = currentTimeInSecs + + val metricData = getMetricData(DURATION_METRIC_NAME, keys, duration, startTime) + + When("trying to find matching AggregationType") + val aggregationType = isMatched(metricData) + + Then("should get Histogram AggregationType") + aggregationType shouldEqual Some(AggregationType.Histogram) + } + } +} diff --git a/trends/timeseries-aggregator/src/test/scala/com/expedia/www/haystack/trends/feature/tests/aggregation/rules/FailureMetricRuleSpec.scala b/trends/timeseries-aggregator/src/test/scala/com/expedia/www/haystack/trends/feature/tests/aggregation/rules/FailureMetricRuleSpec.scala new file mode 100644 index 000000000..e0ce90a32 --- /dev/null +++ b/trends/timeseries-aggregator/src/test/scala/com/expedia/www/haystack/trends/feature/tests/aggregation/rules/FailureMetricRuleSpec.scala @@ -0,0 +1,54 @@ +/* + * + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trends.feature.tests.aggregation.rules + +import com.expedia.www.haystack.trends.aggregation.metrics.AggregationType +import com.expedia.www.haystack.trends.aggregation.rules.FailureMetricRule +import com.expedia.www.haystack.trends.feature.FeatureSpec + +class FailureMetricRuleSpec extends FeatureSpec with FailureMetricRule { + + val FAILURE_METRIC_NAME = "failure-spans" + val SERVICE_NAME = "dummy_service" + val OPERATION_NAME = "dummy_operation" + + object TagKeys { + val OPERATION_NAME_KEY = "operationName" + val SERVICE_NAME_KEY = "serviceName" + } + + feature("DurationMetricRule for identifying MetricRule") { + + scenario("should get Aggregate AggregationType for Failure MetricData") { + + Given("a failure MetricPoint") + val keys = Map(TagKeys.OPERATION_NAME_KEY -> OPERATION_NAME, + TagKeys.SERVICE_NAME_KEY -> SERVICE_NAME) + val startTime = currentTimeInSecs + + val metricData = getMetricData(FAILURE_METRIC_NAME, keys, 1, startTime) + + When("trying to find matching AggregationType") + val aggregationType = isMatched(metricData) + + Then("should get Aggregate AggregationType") + aggregationType shouldEqual Some(AggregationType.Count) + } + } +} diff --git a/trends/timeseries-aggregator/src/test/scala/com/expedia/www/haystack/trends/feature/tests/aggregation/rules/LatencyMetricRuleSpec.scala b/trends/timeseries-aggregator/src/test/scala/com/expedia/www/haystack/trends/feature/tests/aggregation/rules/LatencyMetricRuleSpec.scala new file mode 100644 index 000000000..e6199da0e --- /dev/null +++ b/trends/timeseries-aggregator/src/test/scala/com/expedia/www/haystack/trends/feature/tests/aggregation/rules/LatencyMetricRuleSpec.scala @@ -0,0 +1,55 @@ +/* + * + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trends.feature.tests.aggregation.rules + +import com.expedia.www.haystack.trends.aggregation.metrics.AggregationType +import com.expedia.www.haystack.trends.aggregation.rules.LatencyMetricRule +import com.expedia.www.haystack.trends.feature.FeatureSpec + +class LatencyMetricRuleSpec extends FeatureSpec with LatencyMetricRule { + + val LATENCY_METRIC_NAME = "latency" + val SERVICE_NAME = "dummy_service" + val OPERATION_NAME = "dummy_operation" + + object TagKeys { + val OPERATION_NAME_KEY = "operationName" + val SERVICE_NAME_KEY = "serviceName" + } + + feature("LatencyMetricRule for identifying MetricRule") { + + scenario("should get Aggregate AggregationType for Latency MetricData") { + + Given("a Latency MetricPoint") + val keys = Map(TagKeys.OPERATION_NAME_KEY -> OPERATION_NAME, + TagKeys.SERVICE_NAME_KEY -> SERVICE_NAME) + val startTime = currentTimeInSecs + + val metricData = getMetricData(LATENCY_METRIC_NAME, keys, 1, startTime) + + When("trying to find matching AggregationType") + val aggregationType = isMatched(metricData) + + Then("should get Aggregate AggregationType") + aggregationType shouldEqual Some(AggregationType.Histogram) + } + + } +} diff --git a/trends/timeseries-aggregator/src/test/scala/com/expedia/www/haystack/trends/feature/tests/aggregation/rules/SuccessMetricRuleSpec.scala b/trends/timeseries-aggregator/src/test/scala/com/expedia/www/haystack/trends/feature/tests/aggregation/rules/SuccessMetricRuleSpec.scala new file mode 100644 index 000000000..fd0eb9bf1 --- /dev/null +++ b/trends/timeseries-aggregator/src/test/scala/com/expedia/www/haystack/trends/feature/tests/aggregation/rules/SuccessMetricRuleSpec.scala @@ -0,0 +1,55 @@ +/* + * + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trends.feature.tests.aggregation.rules + +import com.expedia.www.haystack.trends.aggregation.metrics.AggregationType +import com.expedia.www.haystack.trends.aggregation.rules.SuccessMetricRule +import com.expedia.www.haystack.trends.feature.FeatureSpec + +class SuccessMetricRuleSpec extends FeatureSpec with SuccessMetricRule { + + val SUCCESS_METRIC_NAME = "success-spans" + val SERVICE_NAME = "dummy_service" + val OPERATION_NAME = "dummy_operation" + + object TagKeys { + val OPERATION_NAME_KEY = "operationName" + val SERVICE_NAME_KEY = "serviceName" + } + + feature("SuccessMetricRule for identifying MetricRule") { + + + scenario("should get Aggregate AggregationType for Success MetricData") { + + Given("a success MetricPoint") + val keys = Map(TagKeys.OPERATION_NAME_KEY -> OPERATION_NAME, + TagKeys.SERVICE_NAME_KEY -> SERVICE_NAME) + val startTime = currentTimeInSecs + + val metricData = getMetricData(SUCCESS_METRIC_NAME, keys, 1, startTime) + + When("trying to find matching AggregationType") + val aggregationType = isMatched(metricData) + + Then("should get Aggregate AggregationType") + aggregationType shouldEqual Some(AggregationType.Count) + } + } +} diff --git a/trends/timeseries-aggregator/src/test/scala/com/expedia/www/haystack/trends/feature/tests/config/ConfigurationLoaderSpec.scala b/trends/timeseries-aggregator/src/test/scala/com/expedia/www/haystack/trends/feature/tests/config/ConfigurationLoaderSpec.scala new file mode 100644 index 000000000..3dc36b10d --- /dev/null +++ b/trends/timeseries-aggregator/src/test/scala/com/expedia/www/haystack/trends/feature/tests/config/ConfigurationLoaderSpec.scala @@ -0,0 +1,144 @@ +/* + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.expedia.www.haystack.trends.feature.tests.config + +import com.expedia.www.haystack.commons.entities.encoders.PeriodReplacementEncoder +import com.expedia.www.haystack.trends.config.AppConfiguration +import com.expedia.www.haystack.trends.config.entities.HistogramUnit +import com.expedia.www.haystack.trends.feature.FeatureSpec + +class ConfigurationLoaderSpec extends FeatureSpec { + + feature("Configuration loader") { + + scenario("should load the health status config from base.conf") { + + Given("A config file at base config file containing config for health status file path") + val healthStatusFilePath = "/app/isHealthy" + + When("When the configuration is loaded in project configuration") + val projectConfig = new AppConfiguration() + + Then("the healthStatusFilePath should be correct") + projectConfig.healthStatusFilePath shouldEqual healthStatusFilePath + } + + scenario("should load the metric point enable period replacement config from base.conf") { + + Given("A config file at base config file containing config for enable period replacement") + val enableMetricPointPeriodReplacement = true + + When("When the configuration is loaded in project configuration") + val projectConfig = new AppConfiguration() + + Then("the encoder should be correct") + projectConfig.encoder shouldBe an[PeriodReplacementEncoder] + } + + scenario("should load the kafka config from base.conf") { + + Given("A config file at base config file containing kafka ") + + When("When the configuration is loaded in project configuration") + val projectConfig = new AppConfiguration() + + Then("It should create the write configuration object based on the file contents") + val kafkaConfig = projectConfig.kafkaConfig + kafkaConfig.consumeTopic shouldBe "metric-data-points" + } + + scenario("should load additional tags config from base.conf") { + Given("A config file at base config file containing additionalTags ") + + When("When the configuration is loaded in project configuration") + val projectConfig = new AppConfiguration() + + Then("It should create the addtionalTags map based on the file contents") + val additionalTags = projectConfig.additionalTags + additionalTags.keySet.size shouldEqual 2 + additionalTags("key1") shouldEqual "value1" + additionalTags("key2") shouldEqual "value2" + + } + + scenario("should override configuration based on environment variable") { + + + Given("A config file at base config file containing config for kafka") + + When("When the configuration is loaded in project configuration") + val projectConfig = new AppConfiguration() + + Then("It should override the configuration object based on the environment variable if it exists") + + val kafkaProduceTopic = sys.env.getOrElse("HAYSTACK_PROP_KAFKA_PRODUCER_TOPIC", """{ + | topic: "metrics" + | serdeClassName : "com.expedia.www.haystack.commons.kstreams.serde.metricdata.MetricDataSerde" + | enabled: true + | }, + | { + | topic: "mdm" + | serdeClassName : "com.expedia.www.haystack.commons.kstreams.serde.metricdata.MetricTankSerde" + | enabled: true + | }""") + val kafkaConfig = projectConfig.kafkaConfig + kafkaConfig.producerConfig.kafkaSinkTopics.head.topic shouldBe "metrics" + } + + scenario("should load the state store configs from base.conf") { + + Given("A config file at base config file containing kafka ") + + When("When the configuration is loaded in project configuration") + val projectConfig = new AppConfiguration() + + Then("It should create the write configuration object based on the file contents") + val stateStoreConfigs = projectConfig.stateStoreConfig.changeLogTopicConfiguration + projectConfig.stateStoreConfig.enableChangeLogging shouldBe true + projectConfig.stateStoreConfig.changeLogDelayInSecs shouldBe 60 + stateStoreConfigs("cleanup.policy") shouldBe "compact,delete" + stateStoreConfigs("retention.ms") shouldBe "14400000" + } + + scenario("should load the external kafka configs from base.conf") { + + Given("A config file at base config file containing kafka ") + + When("When the configuration is loaded in project configuration") + val projectConfig = new AppConfiguration() + + Then("It should create the write configuration object based on the file contents") + projectConfig.kafkaConfig.producerConfig.enableExternalKafka shouldBe true + projectConfig.kafkaConfig.producerConfig.kafkaSinkTopics.length shouldBe 2 + projectConfig.kafkaConfig.producerConfig.kafkaSinkTopics.head.topic shouldBe "metrics" + projectConfig.kafkaConfig.producerConfig.props.get.getProperty("bootstrap.servers") shouldBe "kafkasvc:9092" + } + + scenario("should load the histogram configs from base.conf") { + + Given("A config file at base config file containing kafka ") + + When("When the configuration is loaded in project configuration") + val projectConfig = new AppConfiguration() + + Then("It should create the write configuration object based on the file contents") + projectConfig.histogramMetricConfiguration.maxValue shouldBe 1800000 + projectConfig.histogramMetricConfiguration.unit == HistogramUnit.MILLIS shouldBe true + } + } +} diff --git a/trends/timeseries-aggregator/src/test/scala/com/expedia/www/haystack/trends/feature/tests/kstreams/StreamsSpec.scala b/trends/timeseries-aggregator/src/test/scala/com/expedia/www/haystack/trends/feature/tests/kstreams/StreamsSpec.scala new file mode 100644 index 000000000..64b232998 --- /dev/null +++ b/trends/timeseries-aggregator/src/test/scala/com/expedia/www/haystack/trends/feature/tests/kstreams/StreamsSpec.scala @@ -0,0 +1,29 @@ +package com.expedia.www.haystack.trends.feature.tests.kstreams + +import com.expedia.www.haystack.trends.feature.FeatureSpec +import com.expedia.www.haystack.trends.kstream.Streams + +class StreamsSpec extends FeatureSpec { + + feature("Streams should build a topology") { + + scenario("a valid kafka configuration") { + + Given("an valid kafka configuration") + + val appConfig = mockAppConfig + val streams = new Streams(appConfig) + + + When("the stream topology is built") + val topology = streams.get() + + Then("it should be able to build a successful topology") + topology should not be null + + Then("it should create a state store") + topology.describe().globalStores().isEmpty shouldBe true + } + } + +} diff --git a/trends/timeseries-aggregator/src/test/scala/com/expedia/www/haystack/trends/feature/tests/kstreams/processor/AdditionalTagsProcessorSupplierSpec.scala b/trends/timeseries-aggregator/src/test/scala/com/expedia/www/haystack/trends/feature/tests/kstreams/processor/AdditionalTagsProcessorSupplierSpec.scala new file mode 100644 index 000000000..9be62b977 --- /dev/null +++ b/trends/timeseries-aggregator/src/test/scala/com/expedia/www/haystack/trends/feature/tests/kstreams/processor/AdditionalTagsProcessorSupplierSpec.scala @@ -0,0 +1,79 @@ +package com.expedia.www.haystack.trends.feature.tests.kstreams.processor + +import com.expedia.metrics.MetricData +import com.expedia.www.haystack.trends.feature.FeatureSpec +import com.expedia.www.haystack.trends.kstream.processor.AdditionalTagsProcessor +import org.apache.kafka.streams.processor.ProcessorContext +import org.mockito.ArgumentMatchers._ +import org.mockito.Mockito +import org.mockito.Mockito._ +import org.mockito.invocation.InvocationOnMock + +import scala.collection.mutable.ListBuffer + +class AdditionalTagsProcessorSupplierSpec extends FeatureSpec { + + feature("Additional Tags processor supplier should add additional tags") { + + val appConfiguration = mockAppConfig + + scenario("should add additional tags if any and forward metricData") { + + Given("an additional tags processor") + + val keys = Map("product" -> "haystack") + val listBuffer = ListBuffer[MetricData]() + val metricData = getMetricData("success-count", keys, 10, System.currentTimeMillis()) + val additionalTagsProcessor = new AdditionalTagsProcessor(appConfiguration.additionalTags) + val processorContext = Mockito.mock(classOf[ProcessorContext]) + additionalTagsProcessor.init(processorContext) + + when(processorContext.forward(anyString(), any(classOf[MetricData]))).thenAnswer((invocationOnMock: InvocationOnMock) => { + listBuffer += invocationOnMock.getArgument[MetricData](1) + }) + + + When("additional tags processor is passed with metric data") + additionalTagsProcessor.process("abc", metricData) + + + Then("additional tags are added to metric data") + val metricDataForwaded = listBuffer.toList + metricDataForwaded.length shouldEqual 1 + metricDataForwaded.head.getMetricDefinition.getTags.getKv.containsKey("k1") shouldEqual true + metricDataForwaded.head.getMetricDefinition.getTags.getKv.containsKey("k2") shouldEqual true + + } + + + scenario("should not add additional tags if additional tags and forward metricData") { + + Given("an additional tags processor") + + val keys = Map("product" -> "haystack") + val listBuffer = ListBuffer[MetricData]() + val metricData = getMetricData("success-count", keys, 10, System.currentTimeMillis()) + val additionalTagsProcessor = new AdditionalTagsProcessor(Map()) + val processorContext = Mockito.mock(classOf[ProcessorContext]) + additionalTagsProcessor.init(processorContext) + + when(processorContext.forward(anyString(), any(classOf[MetricData]))).thenAnswer((invocationOnMock: InvocationOnMock) => { + listBuffer += invocationOnMock.getArgument[MetricData](1) + }) + + + When("additional tags processor is passed with metric data") + additionalTagsProcessor.process("abc", metricData) + + + Then("additional tags are added to metric data") + val metricDataForwaded = listBuffer.toList + metricDataForwaded.length shouldEqual 1 + metricDataForwaded.head shouldEqual metricData + + } + + } + + +} diff --git a/trends/timeseries-aggregator/src/test/scala/com/expedia/www/haystack/trends/feature/tests/kstreams/processor/MetricAggProcessorSupplierSpec.scala b/trends/timeseries-aggregator/src/test/scala/com/expedia/www/haystack/trends/feature/tests/kstreams/processor/MetricAggProcessorSupplierSpec.scala new file mode 100644 index 000000000..8ab0e6e08 --- /dev/null +++ b/trends/timeseries-aggregator/src/test/scala/com/expedia/www/haystack/trends/feature/tests/kstreams/processor/MetricAggProcessorSupplierSpec.scala @@ -0,0 +1,82 @@ +package com.expedia.www.haystack.trends.feature.tests.kstreams.processor + +import com.expedia.metrics.MetricData +import com.expedia.www.haystack.commons.entities.Interval +import com.expedia.www.haystack.commons.entities.Interval.Interval +import com.expedia.www.haystack.commons.entities.encoders.PeriodReplacementEncoder +import com.expedia.www.haystack.commons.metrics.MetricsRegistries +import com.expedia.www.haystack.trends.aggregation.TrendMetric +import com.expedia.www.haystack.trends.feature.FeatureSpec +import com.expedia.www.haystack.trends.kstream.processor.MetricAggProcessorSupplier +import org.apache.kafka.streams.kstream.internals.KTableValueGetter +import org.apache.kafka.streams.processor.ProcessorContext +import org.apache.kafka.streams.state.KeyValueStore +import org.easymock.EasyMock + +class MetricAggProcessorSupplierSpec extends FeatureSpec { + + feature("Metric aggregator processor supplier should return windowed metric from store") { + + val windowedMetricStoreName = "dummy-windowed-metric-store" + + scenario("should return windowed metric for a given key") { + + Given("a metric aggregator supplier and metric processor") + val trendMetric = mock[TrendMetric] + val metricAggProcessorSupplier = new MetricAggProcessorSupplier(windowedMetricStoreName, new PeriodReplacementEncoder) + val keyValueStore: KeyValueStore[String, TrendMetric] = mock[KeyValueStore[String, TrendMetric]] + val processorContext = mock[ProcessorContext] + expecting { + keyValueStore.get("metrics").andReturn(trendMetric) + processorContext.getStateStore(windowedMetricStoreName).andReturn(keyValueStore) + } + EasyMock.replay(keyValueStore) + EasyMock.replay(processorContext) + + When("metric processor is initialised with processor context") + val kTableValueGetter: KTableValueGetter[String, TrendMetric] = metricAggProcessorSupplier.view().get() + kTableValueGetter.init(processorContext) + + Then("same windowed metric should be retrieved with the given key") + kTableValueGetter.get("metrics") shouldBe trendMetric + } + + scenario("should not return any AggregationType for invalid MetricData") { + + Given("a metric aggregator supplier and an invalid metric data") + val metricData = getMetricData("invalid-metric", null, 80, currentTimeInSecs) + val metricAggProcessorSupplier = new MetricAggProcessorSupplier(windowedMetricStoreName, new PeriodReplacementEncoder) + + When("find the AggregationType for the metric point") + val aggregationType = metricAggProcessorSupplier.findMatchingMetric(metricData) + + Then("no AggregationType should be returned") + aggregationType shouldEqual None + } + + scenario("jmx metric (metricpoints.invalid) should be set for invalid MetricPoints") { + val DURATION_METRIC_NAME = "duration" + val validMetricPoint: MetricData = getMetricData(DURATION_METRIC_NAME, null, 10, currentTimeInSecs) + val intervals: List[Interval] = List(Interval.ONE_MINUTE, Interval.FIFTEEN_MINUTE) + val metricAggProcessor = new MetricAggProcessorSupplier(windowedMetricStoreName, new PeriodReplacementEncoder).get + val metricsRegistry = MetricsRegistries.metricRegistry + + Given("metric points with invalid values") + val negativeValueMetricPoint: MetricData = getMetricData(DURATION_METRIC_NAME, null, -1, currentTimeInSecs) + val zeroValueMetricPoint: MetricData = getMetricData(DURATION_METRIC_NAME, null, 0, currentTimeInSecs) + + When("computing a negative value MetricPoint") + metricAggProcessor.process(negativeValueMetricPoint.getMetricDefinition.getKey, negativeValueMetricPoint) + + Then("metric for invalid value should get incremented") + metricsRegistry.getMeters.get("metricprocessor.invalid").getCount shouldEqual 1 + + When("computing a zero value MetricPoint") + metricAggProcessor.process(negativeValueMetricPoint.getMetricDefinition.getKey, zeroValueMetricPoint) + + Then("metric for invalid value should get incremented") + metricsRegistry.getMeters.get("metricprocessor.invalid").getCount shouldEqual 2 + } + } + +} diff --git a/trends/timeseries-aggregator/src/test/scala/com/expedia/www/haystack/trends/feature/tests/kstreams/serde/TrendMetricSerdeSpec.scala b/trends/timeseries-aggregator/src/test/scala/com/expedia/www/haystack/trends/feature/tests/kstreams/serde/TrendMetricSerdeSpec.scala new file mode 100644 index 000000000..ed24ed1e2 --- /dev/null +++ b/trends/timeseries-aggregator/src/test/scala/com/expedia/www/haystack/trends/feature/tests/kstreams/serde/TrendMetricSerdeSpec.scala @@ -0,0 +1,114 @@ +/* + * + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.trends.feature.tests.kstreams.serde + +import com.expedia.metrics.MetricData +import com.expedia.www.haystack.commons.entities.Interval.Interval +import com.expedia.www.haystack.commons.entities.{Interval, TagKeys} +import com.expedia.www.haystack.trends.aggregation.TrendMetric +import com.expedia.www.haystack.trends.aggregation.metrics.{CountMetric, CountMetricFactory, HistogramMetric, HistogramMetricFactory} +import com.expedia.www.haystack.trends.feature.FeatureSpec +import com.expedia.www.haystack.trends.kstream.serde.TrendMetricSerde + +class TrendMetricSerdeSpec extends FeatureSpec { + + val DURATION_METRIC_NAME = "duration" + val SUCCESS_METRIC_NAME = "success-spans" + val SERVICE_NAME = "dummy_service" + val TOPIC_NAME = "dummy" + val OPERATION_NAME = "dummy_operation" + val keys = Map(TagKeys.OPERATION_NAME_KEY -> OPERATION_NAME, + TagKeys.SERVICE_NAME_KEY -> SERVICE_NAME) + val currentTime = 0 + + feature("Serializing/Deserializing Trend Metric") { + + scenario("should be able to serialize and deserialize a valid trend metric computing histograms") { + val durations: List[Long] = List(10, 140) + val intervals: List[Interval] = List(Interval.ONE_MINUTE, Interval.FIFTEEN_MINUTE) + + val metricPoints: List[MetricData] = durations.map(duration => getMetricData(DURATION_METRIC_NAME, keys, duration, currentTime)) + + When("creating a TrendMetric and passing some MetricPoints and aggregation type as Histogram") + val trendMetric: TrendMetric = TrendMetric.createTrendMetric(intervals, metricPoints.head, HistogramMetricFactory) + metricPoints.indices.foreach(i => if (i > 0) { + trendMetric.compute(metricPoints(i)) + }) + + When("the trend metric is serialized and then deserialized back") + val serializedMetric = TrendMetricSerde.serializer().serialize(TOPIC_NAME, trendMetric) + val deserializedMetric = TrendMetricSerde.deserializer().deserialize(TOPIC_NAME, serializedMetric) + + Then("Then it should deserialize the metric back in the same state") + deserializedMetric should not be null + trendMetric.trendMetricsMap.map { + case (interval, windowedMetric) => + deserializedMetric.trendMetricsMap.get(interval) should not be None + windowedMetric.windowedMetricsMap.map { + case (timeWindow, metric) => + val histogram = metric.asInstanceOf[HistogramMetric] + val deserializedHistogram = deserializedMetric.trendMetricsMap(interval).windowedMetricsMap(timeWindow).asInstanceOf[HistogramMetric] + histogram.getMetricInterval shouldEqual deserializedHistogram.getMetricInterval + histogram.getRunningHistogram.getTotalCount shouldEqual deserializedHistogram.getRunningHistogram.getTotalCount + histogram.getRunningHistogram.getMaxValue shouldEqual deserializedHistogram.getRunningHistogram.getMaxValue + histogram.getRunningHistogram.getValueAtPercentile(50) shouldEqual deserializedHistogram.getRunningHistogram.getValueAtPercentile(50) + } + } + } + + scenario("should be able to serialize and deserialize a valid trend metric computing counts") { + + Given("some count Metric points") + val counts: List[Long] = List(10, 140) + val intervals: List[Interval] = List(Interval.ONE_MINUTE, Interval.FIFTEEN_MINUTE) + val metricPoints: List[MetricData] = counts.map(count => getMetricData(SUCCESS_METRIC_NAME, keys, count, currentTime)) + + + When("creating a TrendMetric and passing some MetricPoints and aggregation type as Count") + val trendMetric: TrendMetric = TrendMetric.createTrendMetric(intervals, metricPoints.head, CountMetricFactory) + metricPoints.indices.foreach(i => if (i > 0) { + trendMetric.compute(metricPoints(i)) + }) + + When("the trend metric is serialized and then deserialized back") + val serializer = TrendMetricSerde.serializer() + val deserializer = TrendMetricSerde.deserializer() + val serializedMetric = serializer.serialize(TOPIC_NAME, trendMetric) + val deserializedMetric = deserializer.deserialize(TOPIC_NAME, serializedMetric) + + + Then("Then it should deserialize the metric back in the same state") + deserializedMetric should not be null + trendMetric.trendMetricsMap.map { + case (interval, windowedMetric) => + deserializedMetric.trendMetricsMap.get(interval) should not be None + windowedMetric.windowedMetricsMap.map { + case (timeWindow, metric) => + + val countMetric = metric.asInstanceOf[CountMetric] + val deserializedCountMetric = deserializedMetric.trendMetricsMap(interval).windowedMetricsMap(timeWindow).asInstanceOf[CountMetric] + countMetric.getMetricInterval shouldEqual deserializedCountMetric.getMetricInterval + countMetric.getCurrentCount shouldEqual deserializedCountMetric.getCurrentCount + } + } + serializer.close() + deserializer.close() + TrendMetricSerde.close() + } + } +} diff --git a/trends/timeseries-aggregator/src/test/scala/com/expedia/www/haystack/trends/feature/tests/kstreams/serde/WindowedMetricSerdeSpec.scala b/trends/timeseries-aggregator/src/test/scala/com/expedia/www/haystack/trends/feature/tests/kstreams/serde/WindowedMetricSerdeSpec.scala new file mode 100644 index 000000000..ff8754c3f --- /dev/null +++ b/trends/timeseries-aggregator/src/test/scala/com/expedia/www/haystack/trends/feature/tests/kstreams/serde/WindowedMetricSerdeSpec.scala @@ -0,0 +1,88 @@ +package com.expedia.www.haystack.trends.feature.tests.kstreams.serde + +import com.expedia.metrics.MetricData +import com.expedia.www.haystack.commons.entities.{Interval, TagKeys} +import com.expedia.www.haystack.trends.aggregation.WindowedMetric +import com.expedia.www.haystack.trends.aggregation.metrics.{CountMetric, CountMetricFactory, HistogramMetric, HistogramMetricFactory} +import com.expedia.www.haystack.trends.feature.FeatureSpec +import com.expedia.www.haystack.trends.kstream.serde.WindowedMetricSerde + +class WindowedMetricSerdeSpec extends FeatureSpec { + + val DURATION_METRIC_NAME = "duration" + val SUCCESS_METRIC_NAME = "success-spans" + val SERVICE_NAME = "dummy_service" + val TOPIC_NAME = "dummy" + val OPERATION_NAME = "dummy_operation" + val keys = Map(TagKeys.OPERATION_NAME_KEY -> OPERATION_NAME, + TagKeys.SERVICE_NAME_KEY -> SERVICE_NAME) + + feature("Serializing/Deserializing Windowed Metric") { + + scenario("should be able to serialize and deserialize a valid windowed metric computing histograms") { + val durations: List[Long] = List(10, 140) + val metricPoints: List[MetricData] = durations.map(duration => getMetricData(DURATION_METRIC_NAME, keys, duration, currentTimeInSecs)) + + When("creating a WindowedMetric and passing some MetricPoints and aggregation type as Histogram") + val windowedMetric: WindowedMetric = WindowedMetric.createWindowedMetric(metricPoints.head, HistogramMetricFactory, 1, Interval.ONE_MINUTE) + metricPoints.indices.foreach(i => if (i > 0) { + windowedMetric.compute(metricPoints(i)) + }) + + When("the windowed metric is serialized and then deserialized back") + val serializedMetric = WindowedMetricSerde.serializer().serialize(TOPIC_NAME, windowedMetric) + val deserializedMetric = WindowedMetricSerde.deserializer().deserialize(TOPIC_NAME, serializedMetric) + + Then("Then it should deserialize the metric back in the same state") + deserializedMetric should not be null + windowedMetric.windowedMetricsMap.map { + case (window, metric) => + deserializedMetric.windowedMetricsMap.get(window) should not be None + + val histogram = metric.asInstanceOf[HistogramMetric] + val deserializedHistogram = deserializedMetric.windowedMetricsMap(window).asInstanceOf[HistogramMetric] + histogram.getMetricInterval shouldEqual deserializedHistogram.getMetricInterval + histogram.getRunningHistogram.getTotalCount shouldEqual deserializedHistogram.getRunningHistogram.getTotalCount + histogram.getRunningHistogram.getMaxValue shouldEqual deserializedHistogram.getRunningHistogram.getMaxValue + histogram.getRunningHistogram.getMinValue shouldEqual deserializedHistogram.getRunningHistogram.getMinValue + histogram.getRunningHistogram.getValueAtPercentile(99) shouldEqual deserializedHistogram.getRunningHistogram.getValueAtPercentile(99) + } + } + + scenario("should be able to serialize and deserialize a valid windowed metric computing counts") { + + Given("some count Metric points") + val counts: List[Long] = List(10, 140) + val metricPoints: List[MetricData] = counts.map(count => getMetricData(SUCCESS_METRIC_NAME, keys, count, currentTimeInSecs)) + + + When("creating a WindowedMetric and passing some MetricPoints and aggregation type as Count") + val windowedMetric: WindowedMetric = WindowedMetric.createWindowedMetric(metricPoints.head, CountMetricFactory, 1, Interval.ONE_MINUTE) + metricPoints.indices.foreach(i => if (i > 0) { + windowedMetric.compute(metricPoints(i)) + }) + + When("the windowed metric is serialized and then deserialized back") + val serializer = WindowedMetricSerde.serializer() + val deserializer = WindowedMetricSerde.deserializer() + val serializedMetric = serializer.serialize(TOPIC_NAME, windowedMetric) + val deserializedMetric = deserializer.deserialize(TOPIC_NAME, serializedMetric) + + + Then("Then it should deserialize the metric back in the same state") + deserializedMetric should not be null + windowedMetric.windowedMetricsMap.map { + case (window, metric) => + deserializedMetric.windowedMetricsMap.get(window) should not be None + + val countMetric = metric.asInstanceOf[CountMetric] + val deserializedCountMetric = deserializedMetric.windowedMetricsMap(window).asInstanceOf[CountMetric] + countMetric.getMetricInterval shouldEqual deserializedCountMetric.getMetricInterval + countMetric.getCurrentCount shouldEqual deserializedCountMetric.getCurrentCount + } + serializer.close() + deserializer.close() + WindowedMetricSerde.close() + } + } +} diff --git a/trends/timeseries-aggregator/src/test/scala/com/expedia/www/haystack/trends/feature/tests/kstreams/store/HaystackStoreBuilderSpec.scala b/trends/timeseries-aggregator/src/test/scala/com/expedia/www/haystack/trends/feature/tests/kstreams/store/HaystackStoreBuilderSpec.scala new file mode 100644 index 000000000..c45d85745 --- /dev/null +++ b/trends/timeseries-aggregator/src/test/scala/com/expedia/www/haystack/trends/feature/tests/kstreams/store/HaystackStoreBuilderSpec.scala @@ -0,0 +1,27 @@ +package com.expedia.www.haystack.trends.feature.tests.kstreams.store + +import com.expedia.www.haystack.trends.aggregation.TrendMetric +import com.expedia.www.haystack.trends.feature.FeatureSpec +import com.expedia.www.haystack.trends.kstream.store.HaystackStoreBuilder +import org.apache.kafka.streams.state.internals.{InMemoryKeyValueLoggedStore, MemoryNavigableLRUCache, MeteredKeyValueStore} + +class HaystackStoreBuilderSpec extends FeatureSpec { + + feature("Haystack Store Builder should build appropriate store for haystack metrics") { + + scenario("build store with changelog enabled") { + + Given("a haystack store builder") + val storeName = "test-store" + val cacheSize = 100 + val storeBuilder = new HaystackStoreBuilder(storeName, cacheSize) + + When("change logging is enabled") + storeBuilder.withCachingEnabled() + val store = storeBuilder.build() + + Then("it should build a metered lru-cache based changelogging store") + store.isInstanceOf[MeteredKeyValueStore[String, TrendMetric]] shouldBe true + } + } +} diff --git a/trends/timeseries-aggregator/src/test/scala/com/expedia/www/haystack/trends/integration/IntegrationTestSpec.scala b/trends/timeseries-aggregator/src/test/scala/com/expedia/www/haystack/trends/integration/IntegrationTestSpec.scala new file mode 100644 index 000000000..422eac08d --- /dev/null +++ b/trends/timeseries-aggregator/src/test/scala/com/expedia/www/haystack/trends/integration/IntegrationTestSpec.scala @@ -0,0 +1,227 @@ +/* + * + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.trends.integration + +import java.util +import java.util.Properties +import java.util.concurrent.{Executors, ScheduledExecutorService, ScheduledFuture, TimeUnit} + +import com.expedia.metrics.{MetricData, MetricDefinition, TagCollection} +import com.expedia.www.haystack.commons.entities.Interval +import com.expedia.www.haystack.commons.entities.encoders.PeriodReplacementEncoder +import com.expedia.www.haystack.commons.health.HealthStatusController +import com.expedia.www.haystack.commons.kstreams.app.{StateChangeListener, StreamsFactory, StreamsRunner} +import com.expedia.www.haystack.commons.kstreams.serde.metricdata.{MetricDataSerde, MetricTankSerde} +import com.expedia.www.haystack.commons.util.MetricDefinitionKeyGenerator +import com.expedia.www.haystack.commons.util.MetricDefinitionKeyGenerator._ +import com.expedia.www.haystack.trends.config.AppConfiguration +import com.expedia.www.haystack.trends.config.entities.{KafkaConfiguration, KafkaProduceConfiguration, KafkaSinkTopic, StateStoreConfiguration} +import com.expedia.www.haystack.trends.kstream.Streams +import org.apache.kafka.clients.consumer.ConsumerConfig +import org.apache.kafka.clients.producer.ProducerConfig +import org.apache.kafka.common.serialization.{StringDeserializer, StringSerializer} +import org.apache.kafka.streams.Topology.AutoOffsetReset +import org.apache.kafka.streams.integration.utils.{EmbeddedKafkaCluster, IntegrationTestUtils} +import org.apache.kafka.streams.processor.WallclockTimestampExtractor +import org.apache.kafka.streams.{KeyValue, StreamsConfig} +import org.easymock.EasyMock +import org.scalatest._ +import org.scalatest.easymock.EasyMockSugar + +import scala.collection.JavaConverters._ +import scala.concurrent.duration.FiniteDuration +import scala.util.Random + +class IntegrationTestSpec extends WordSpec with GivenWhenThen with Matchers with BeforeAndAfterAll with BeforeAndAfterEach with EasyMockSugar { + + protected val PUNCTUATE_INTERVAL_SEC = 2000 + protected val PRODUCER_CONFIG = new Properties() + protected val RESULT_CONSUMER_CONFIG = new Properties() + protected val STREAMS_CONFIG = new Properties() + protected val scheduledJobFuture: ScheduledFuture[_] = null + protected val INPUT_TOPIC = "metric-data-points" + protected val OUTPUT_TOPIC = "metrics" + protected val OUTPUT_METRICTANK_TOPIC = "mdm" + protected var scheduler: ScheduledExecutorService = _ + protected var APP_ID = "haystack-trends" + protected var CHANGELOG_TOPIC = s"$APP_ID-trend-metric-store-changelog" + protected var embeddedKafkaCluster: EmbeddedKafkaCluster = _ + + override def beforeAll(): Unit = { + scheduler = Executors.newScheduledThreadPool(1) + } + + override def afterAll(): Unit = { + scheduler.shutdownNow() + } + + override def beforeEach() { + val metricDataSerde = new MetricDataSerde() + + embeddedKafkaCluster = new EmbeddedKafkaCluster(1) + embeddedKafkaCluster.start() + embeddedKafkaCluster.createTopic(INPUT_TOPIC, 1, 1) + embeddedKafkaCluster.createTopic(OUTPUT_TOPIC, 1, 1) + + PRODUCER_CONFIG.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, embeddedKafkaCluster.bootstrapServers) + PRODUCER_CONFIG.put(ProducerConfig.ACKS_CONFIG, "all") + PRODUCER_CONFIG.put(ProducerConfig.RETRIES_CONFIG, "0") + PRODUCER_CONFIG.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, classOf[StringSerializer]) + PRODUCER_CONFIG.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, metricDataSerde.serializer().getClass) + + RESULT_CONSUMER_CONFIG.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, embeddedKafkaCluster.bootstrapServers) + RESULT_CONSUMER_CONFIG.put(ConsumerConfig.GROUP_ID_CONFIG, APP_ID + "-result-consumer") + RESULT_CONSUMER_CONFIG.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest") + RESULT_CONSUMER_CONFIG.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, classOf[StringDeserializer]) + RESULT_CONSUMER_CONFIG.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, metricDataSerde.deserializer().getClass) + + STREAMS_CONFIG.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, embeddedKafkaCluster.bootstrapServers) + STREAMS_CONFIG.put(StreamsConfig.APPLICATION_ID_CONFIG, APP_ID) + STREAMS_CONFIG.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest") + STREAMS_CONFIG.put(StreamsConfig.CACHE_MAX_BYTES_BUFFERING_CONFIG, "0") + STREAMS_CONFIG.put(StreamsConfig.NUM_STREAM_THREADS_CONFIG, "1") + STREAMS_CONFIG.put(StreamsConfig.COMMIT_INTERVAL_MS_CONFIG, "300") + STREAMS_CONFIG.put(StreamsConfig.REPLICATION_FACTOR_CONFIG, "1") + STREAMS_CONFIG.put(StreamsConfig.STATE_DIR_CONFIG, "/tmp/kafka-streams") + + IntegrationTestUtils.purgeLocalStreamsState(STREAMS_CONFIG) + } + + override def afterEach(): Unit = { + embeddedKafkaCluster.deleteTopics(INPUT_TOPIC, OUTPUT_TOPIC) + } + + def currentTimeInSecs: Long = { + System.currentTimeMillis() / 1000l + } + + protected val stateStoreConfigs = Map("cleanup.policy" -> "compact,delete") + + + protected def mockAppConfig: AppConfiguration = { + val kafkaSinkTopics = List(KafkaSinkTopic("metrics","com.expedia.www.haystack.commons.kstreams.serde.metricdata.MetricDataSerde",true), KafkaSinkTopic("mdm","com.expedia.www.haystack.commons.kstreams.serde.metricdata.MetricTankSerde",true)) + val kafkaConfig = KafkaConfiguration(new StreamsConfig(STREAMS_CONFIG),KafkaProduceConfiguration(kafkaSinkTopics, None, "mdm", false), INPUT_TOPIC, AutoOffsetReset.EARLIEST, new WallclockTimestampExtractor, 30000) + val projectConfiguration = mock[AppConfiguration] + + expecting { + projectConfiguration.kafkaConfig.andReturn(kafkaConfig).anyTimes() + projectConfiguration.stateStoreConfig.andReturn(StateStoreConfiguration(128, enableChangeLogging = true, 60, stateStoreConfigs)).anyTimes() + projectConfiguration.encoder.andReturn(new PeriodReplacementEncoder).anyTimes() + projectConfiguration.additionalTags.andReturn(Map("k1"->"v1", "k2"-> "v2")).anyTimes() + } + EasyMock.replay(projectConfiguration) + projectConfiguration + } + + protected def validateAggregatedMetricPoints(producedRecords: List[KeyValue[String, MetricData]], + expectedOneMinAggregatedPoints: Int, + expectedFiveMinAggregatedPoints: Int, + expectedFifteenMinAggregatedPoints: Int, + expectedOneHourAggregatedPoints: Int): Unit = { + + val oneMinAggMetricPoints = producedRecords.filter(record => getTags(record.value).get("interval").equals(Interval.ONE_MINUTE.toString())) + val fiveMinAggMetricPoints = producedRecords.filter(record => getTags(record.value).get("interval").equals(Interval.FIVE_MINUTE.toString())) + val fifteenMinAggMetricPoints = producedRecords.filter(record => getTags(record.value).get("interval").equals(Interval.FIFTEEN_MINUTE.toString())) + val oneHourAggMetricPoints = producedRecords.filter(record => getTags(record.value).get("interval").equals(Interval.ONE_HOUR.toString())) + + oneMinAggMetricPoints.size shouldEqual expectedOneMinAggregatedPoints + fiveMinAggMetricPoints.size shouldEqual expectedFiveMinAggregatedPoints + fifteenMinAggMetricPoints.size shouldEqual expectedFifteenMinAggregatedPoints + oneHourAggMetricPoints.size shouldEqual expectedOneHourAggregatedPoints + validateAdditionalTags(List(oneMinAggMetricPoints, fiveMinAggMetricPoints, fifteenMinAggMetricPoints, oneHourAggMetricPoints).flatten) + } + + protected def validateAdditionalTags(kvPair: List[KeyValue[String, MetricData]]): Unit = { + val additionalTags = mockAppConfig.additionalTags + kvPair.foreach(kv => { + val tags = kv.value.getMetricDefinition.getTags.getKv.asScala + additionalTags.toSet subsetOf tags.toSet shouldEqual true + }) + } + + protected def produceMetricPointsAsync(maxMetricPoints: Int, + produceInterval: FiniteDuration, + metricName: String, + totalIntervalInSecs: Long = PUNCTUATE_INTERVAL_SEC + ): Unit = { + var epochTimeInSecs = 0l + var idx = 0 + scheduler.scheduleWithFixedDelay(() => { + if (idx < maxMetricPoints) { + val metricData = randomMetricData(metricName = metricName, timestamp = epochTimeInSecs) + val keyValue = List(new KeyValue[String, MetricData](generateKey(metricData.getMetricDefinition), metricData)).asJava + IntegrationTestUtils.produceKeyValuesSynchronouslyWithTimestamp( + INPUT_TOPIC, + keyValue, + PRODUCER_CONFIG, + epochTimeInSecs) + epochTimeInSecs = epochTimeInSecs + (totalIntervalInSecs / (maxMetricPoints - 1)) + } + idx = idx + 1 + + }, 0, produceInterval.toMillis, TimeUnit.MILLISECONDS) + } + + protected def produceMetricData(metricName: String, + epochTimeInSecs: Long, + produceTimeInSecs: Long + ): Unit = { + val metricPoint = randomMetricData(metricName = metricName, timestamp = epochTimeInSecs) + val keyValue = List(new KeyValue[String, MetricData](generateKey(metricPoint.getMetricDefinition), metricPoint)).asJava + IntegrationTestUtils.produceKeyValuesSynchronouslyWithTimestamp( + INPUT_TOPIC, + keyValue, + PRODUCER_CONFIG, + produceTimeInSecs) + } + + def randomMetricData(metricName: String, + value: Long = Math.abs(Random.nextInt()), + timestamp: Long = currentTimeInSecs): MetricData = { + getMetricData(metricName, Map[String, String](), value, timestamp) + } + + protected def createStreamRunner(): StreamsRunner = { + val appConfig = mockAppConfig + val streams = new Streams(appConfig) + val factory = new StreamsFactory(streams, appConfig.kafkaConfig.streamsConfig, appConfig.kafkaConfig.consumeTopic) + new StreamsRunner(factory, new StateChangeListener(new HealthStatusController)) + + + } + + protected def getMetricData(metricKey: String, tags: Map[String, String], value: Double, timeStamp: Long): MetricData = { + + val tagsMap = new java.util.LinkedHashMap[String, String] { + putAll(tags.asJava) + put(MetricDefinition.MTYPE, "gauge") + put(MetricDefinition.UNIT, "short") + } + val metricDefinition = new MetricDefinition(metricKey, new TagCollection(tagsMap), TagCollection.EMPTY) + new MetricData(metricDefinition, value, timeStamp) + } + + protected def containsTag(metricData: MetricData, tagKey: String, tagValue: String): Boolean = { + val tags = getTags(metricData) + tags.containsKey(tagKey) && tags.get(tagKey).equalsIgnoreCase(tagValue) + } + + protected def getTags(metricData: MetricData): util.Map[String, String] = { + metricData.getMetricDefinition.getTags.getKv + } +} diff --git a/trends/timeseries-aggregator/src/test/scala/com/expedia/www/haystack/trends/integration/tests/CountTrendsSpec.scala b/trends/timeseries-aggregator/src/test/scala/com/expedia/www/haystack/trends/integration/tests/CountTrendsSpec.scala new file mode 100644 index 000000000..4569a6465 --- /dev/null +++ b/trends/timeseries-aggregator/src/test/scala/com/expedia/www/haystack/trends/integration/tests/CountTrendsSpec.scala @@ -0,0 +1,62 @@ +/* + * + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.trends.integration.tests + +import com.expedia.metrics.MetricData +import com.expedia.www.haystack.trends.integration.IntegrationTestSpec +import org.apache.kafka.streams.KeyValue +import org.apache.kafka.streams.integration.utils.IntegrationTestUtils +import org.scalatest.Sequential + +import scala.collection.JavaConverters._ +import scala.concurrent.duration._ + +@Sequential +class CountTrendsSpec extends IntegrationTestSpec { + + private val MAX_METRICPOINTS = 62 + private val numberOfWatermarkedWindows = 1 + + "TimeSeriesAggregatorTopology" should { + + "aggregate count type metricPoints from input topic based on rules" in { + + Given("a set of metricPoints with type metric and kafka specific configurations") + val METRIC_NAME = "success-span" + // CountMetric + val expectedOneMinAggregatedPoints: Int = MAX_METRICPOINTS - numberOfWatermarkedWindows - 1 + // Why one less -> won't be generated for last (MAX_METRICPOINTS * 60)th second metric point + val expectedFiveMinAggregatedPoints: Int = (MAX_METRICPOINTS / 5) - numberOfWatermarkedWindows + val expectedFifteenMinAggregatedPoints: Int = (MAX_METRICPOINTS / 15) + val expectedOneHourAggregatedPoints: Int = (MAX_METRICPOINTS / 60) + val expectedTotalAggregatedPoints: Int = expectedOneMinAggregatedPoints + expectedFiveMinAggregatedPoints + expectedFifteenMinAggregatedPoints + expectedOneHourAggregatedPoints + val streamsRunner = createStreamRunner() + + When("metricPoints are produced in 'input' topic async, and kafka-streams topology is started") + produceMetricPointsAsync(MAX_METRICPOINTS, 10.milli, METRIC_NAME, MAX_METRICPOINTS * 60) + streamsRunner.start() + + Then("we should read all aggregated metricData from 'output' topic") + val waitTimeMs = 15000 + val result: List[KeyValue[String, MetricData]] = + IntegrationTestUtils.waitUntilMinKeyValueRecordsReceived[String, MetricData](RESULT_CONSUMER_CONFIG, OUTPUT_TOPIC, expectedTotalAggregatedPoints, waitTimeMs).asScala.toList + print(result.length) + validateAggregatedMetricPoints(result, expectedOneMinAggregatedPoints, expectedFiveMinAggregatedPoints, expectedFifteenMinAggregatedPoints, expectedOneHourAggregatedPoints) + } + } +} diff --git a/trends/timeseries-aggregator/src/test/scala/com/expedia/www/haystack/trends/integration/tests/HistogramTrendsSpec.scala b/trends/timeseries-aggregator/src/test/scala/com/expedia/www/haystack/trends/integration/tests/HistogramTrendsSpec.scala new file mode 100644 index 000000000..d904a9426 --- /dev/null +++ b/trends/timeseries-aggregator/src/test/scala/com/expedia/www/haystack/trends/integration/tests/HistogramTrendsSpec.scala @@ -0,0 +1,61 @@ +/* + * + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.trends.integration.tests + +import com.expedia.metrics.MetricData +import com.expedia.www.haystack.trends.integration.IntegrationTestSpec +import org.apache.kafka.streams.KeyValue +import org.apache.kafka.streams.integration.utils.IntegrationTestUtils +import org.scalatest.{Ignore, Sequential} + +import scala.collection.JavaConverters._ +import scala.concurrent.duration._ + +@Ignore +@Sequential +class HistogramTrendsSpec extends IntegrationTestSpec { + + private val MAX_METRICPOINTS = 62 + private val numberOfWatermarkedWindows = 1 + + "TimeSeriesAggregatorTopology" should { + + "aggregate histogram type metricPoints from input topic based on rules" in { + Given("a set of metricPoints with type metric and kafka specific configurations") + val METRIC_NAME = "duration" + //HistogramMetric + val expectedOneMinAggregatedPoints: Int = (MAX_METRICPOINTS - 1 - numberOfWatermarkedWindows) * 7 + // Why one less -> won't be generated for last (MAX_METRICPOINTS * 60)th second metric point + val expectedFiveMinAggregatedPoints: Int = (MAX_METRICPOINTS / 5 - numberOfWatermarkedWindows) * 7 + val expectedFifteenMinAggregatedPoints: Int = (MAX_METRICPOINTS / 15) * 7 + val expectedOneHourAggregatedPoints: Int = (MAX_METRICPOINTS / 60) * 7 + val expectedTotalAggregatedPoints: Int = expectedOneMinAggregatedPoints + expectedFiveMinAggregatedPoints + expectedFifteenMinAggregatedPoints + expectedOneHourAggregatedPoints + val streamsRunner = createStreamRunner() + + When("metricPoints are produced in 'input' topic async, and kafka-streams topology is started") + produceMetricPointsAsync(MAX_METRICPOINTS, 10.milli, METRIC_NAME, MAX_METRICPOINTS * 60) + streamsRunner.start() + + Then("we should read all aggregated metricData from 'output' topic") + val waitTimeMs = 15000 + val result: List[KeyValue[String, MetricData]] = + IntegrationTestUtils.waitUntilMinKeyValueRecordsReceived[String, MetricData](RESULT_CONSUMER_CONFIG, OUTPUT_TOPIC, expectedTotalAggregatedPoints, waitTimeMs).asScala.toList + validateAggregatedMetricPoints(result, expectedOneMinAggregatedPoints, expectedFiveMinAggregatedPoints, expectedFifteenMinAggregatedPoints, expectedOneHourAggregatedPoints) + } + } +} diff --git a/trends/timeseries-aggregator/src/test/scala/com/expedia/www/haystack/trends/integration/tests/StateStoreSpec.scala b/trends/timeseries-aggregator/src/test/scala/com/expedia/www/haystack/trends/integration/tests/StateStoreSpec.scala new file mode 100644 index 000000000..7c1083ec2 --- /dev/null +++ b/trends/timeseries-aggregator/src/test/scala/com/expedia/www/haystack/trends/integration/tests/StateStoreSpec.scala @@ -0,0 +1,59 @@ +/* + * + * Copyright 2017 Expedia, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.expedia.www.haystack.trends.integration.tests + +import com.expedia.metrics.MetricData +import com.expedia.www.haystack.trends.integration.IntegrationTestSpec +import org.apache.kafka.clients.admin.{AdminClient, Config} +import org.apache.kafka.common.config.ConfigResource +import org.apache.kafka.streams.integration.utils.IntegrationTestUtils +import org.scalatest.Sequential + +import scala.collection.JavaConverters._ +import scala.concurrent.duration._ + +@Sequential +class StateStoreSpec extends IntegrationTestSpec { + + private val MAX_METRICPOINTS = 62 + private val numberOfWatermarkedWindows = 1 + + "TimeSeriesAggregatorTopology" should { + + + "have state store (change log) configuration be set by the topology" in { + Given("a set of metricPoints with type metric and state store specific configurations") + val METRIC_NAME = "success-span" + // CountMetric + val streamsRunner = createStreamRunner() + + When("metricPoints are produced in 'input' topic async, and kafka-streams topology is started") + produceMetricPointsAsync(3, 10.milli, METRIC_NAME, 3 * 60) + streamsRunner.start() + + Then("we should see the state store topic created with specified properties") + val waitTimeMs = 15000 + IntegrationTestUtils.waitUntilMinKeyValueRecordsReceived[String, MetricData](RESULT_CONSUMER_CONFIG, OUTPUT_TOPIC, 1, waitTimeMs).asScala.toList + val adminClient = AdminClient.create(STREAMS_CONFIG) + val configResource = new ConfigResource(ConfigResource.Type.TOPIC, CHANGELOG_TOPIC) + val describeConfigResult: java.util.Map[ConfigResource, Config] = adminClient.describeConfigs(java.util.Arrays.asList(configResource)).all().get() + describeConfigResult.get(configResource).get(stateStoreConfigs.head._1).value() shouldBe stateStoreConfigs.head._2 + } + } + +} diff --git a/trends/timeseries-aggregator/src/test/scala/com/expedia/www/haystack/trends/integration/tests/WatermarkingSpec.scala b/trends/timeseries-aggregator/src/test/scala/com/expedia/www/haystack/trends/integration/tests/WatermarkingSpec.scala new file mode 100644 index 000000000..43e0a0864 --- /dev/null +++ b/trends/timeseries-aggregator/src/test/scala/com/expedia/www/haystack/trends/integration/tests/WatermarkingSpec.scala @@ -0,0 +1,42 @@ +package com.expedia.www.haystack.trends.integration.tests + +import com.expedia.metrics.MetricData +import com.expedia.www.haystack.trends.integration.IntegrationTestSpec +import org.apache.kafka.streams.KeyValue +import org.apache.kafka.streams.integration.utils.IntegrationTestUtils + +import scala.collection.JavaConverters._ + +class WatermarkingSpec extends IntegrationTestSpec { + + "TimeSeriesAggregatorTopology" should { + "watermark metrics for aggregate count type metricPoints from input topic" in { + Given("a set of metricPoints with type metric and kafka specific configurations") + val METRIC_NAME = "success-span" + // CountMetric + val expectedOneMinAggregatedPoints: Int = 3 + // Why one less -> won't be generated for last (MAX_METRICPOINTS * 60)th second metric point + val expectedFiveMinAggregatedPoints: Int = 1 + val expectedFifteenMinAggregatedPoints: Int = 0 + val expectedOneHourAggregatedPoints: Int = 0 + val expectedTotalAggregatedPoints: Int = expectedOneMinAggregatedPoints + expectedFiveMinAggregatedPoints + expectedFifteenMinAggregatedPoints + expectedOneHourAggregatedPoints + val streamsRunner = createStreamRunner() + + + When("metricPoints are produced in 'input' topic async, and kafka-streams topology is started") + produceMetricData(METRIC_NAME, 1l, 1l) + produceMetricData(METRIC_NAME, 65l, 2l) + produceMetricData(METRIC_NAME, 2l, 3l) + produceMetricData(METRIC_NAME, 130l, 4l) + produceMetricData(METRIC_NAME, 310l, 5l) + produceMetricData(METRIC_NAME, 610l, 6l) + streamsRunner.start() + + Then("we should read all aggregated metricData from 'output' topic") + val waitTimeMs = 15000 + val result: List[KeyValue[String, MetricData]] = + IntegrationTestUtils.waitUntilMinKeyValueRecordsReceived[String, MetricData](RESULT_CONSUMER_CONFIG, OUTPUT_TOPIC, expectedTotalAggregatedPoints, waitTimeMs).asScala.toList + validateAggregatedMetricPoints(result, expectedOneMinAggregatedPoints, expectedFiveMinAggregatedPoints, expectedFifteenMinAggregatedPoints, expectedOneHourAggregatedPoints) + } + } +} diff --git a/ui/.babelrc b/ui/.babelrc new file mode 100644 index 000000000..8ca3d4e31 --- /dev/null +++ b/ui/.babelrc @@ -0,0 +1,26 @@ +{ + "plugins": [ + "lodash", + [ + "@babel/plugin-proposal-decorators", + { + "legacy": true + } + ], + [ + "@babel/plugin-proposal-class-properties", + { + "loose": true + } + ], + "dynamic-import-node" + ], + "presets": [ + ["@babel/preset-env", { + "targets": { + "node": "current" + } + }], + "@babel/preset-react" + ] +} diff --git a/ui/.coveralls.yml b/ui/.coveralls.yml new file mode 100644 index 000000000..346a7f8cb --- /dev/null +++ b/ui/.coveralls.yml @@ -0,0 +1,2 @@ +service_name: travis-ci +repo_token: rmv39G8JKUPr9W8whITKKlHLl3x4CEi50 \ No newline at end of file diff --git a/ui/.dockerignore b/ui/.dockerignore new file mode 100644 index 000000000..b512c09d4 --- /dev/null +++ b/ui/.dockerignore @@ -0,0 +1 @@ +node_modules \ No newline at end of file diff --git a/ui/.eslintignore b/ui/.eslintignore new file mode 100644 index 000000000..3a298b383 --- /dev/null +++ b/ui/.eslintignore @@ -0,0 +1,4 @@ +public/* +haystack-idl/* +static_codegen/* +coverage/*' \ No newline at end of file diff --git a/ui/.eslintrc b/ui/.eslintrc new file mode 100644 index 000000000..03b171cd5 --- /dev/null +++ b/ui/.eslintrc @@ -0,0 +1,75 @@ +{ + "root": true, + "parser": "babel-eslint", + "extends": "airbnb", + "plugins": ["json", "prettier", "import", "jsx-a11y", "react"], + "parserOptions": { + "ecmaVersion": 7, + "sourceType": "module", + "ecmaFeatures": { + "legacyDecorators": true + } + }, + "env": { + "node": true, + "browser": true, + "mocha": true, + "jquery": true + }, + "rules": { + "no-unused-vars": ["error"], + "import/no-extraneous-dependencies": [ + "error", + { + "devDependencies": true + } + ], + "arrow-parens": 0, + "comma-dangle": ["error", "never"], + "comma-style": 0, + "function-paren-newline": 0, + "implicit-arrow-linebreak": 0, + "import/no-cycle": 0, + "import/no-named-as-default": 0, + "import/no-useless-path-segments": 0, + "import/order": 0, + "indent": 0, + "jsx-a11y/anchor-is-valid": 0, + "jsx-a11y/click-events-have-key-events": 0, + "jsx-a11y/href-no-hash": 0, + "lines-between-class-members": 0, + "max-len": ["error", 250], + "no-confusing-arrow": 0, + "no-else-return": 0, + "no-mixed-operators": 0, + "no-multi-spaces": 0, + "no-multiple-empty-lines": 0, + "no-param-reassign": 0, + "no-plusplus": 0, + "no-restricted-globals": 0, + "no-trailing-spaces": 0, + "object-curly-newline": 0, + "object-curly-spacing": [0, "never"], + "operator-linebreak": 0, + "prefer-destructuring": 0, + "react/button-has-type": 0, + "react/default-props-match-prop-types": 0, + "react/destructuring-assignment": 0, + "react/forbid-prop-types": 0, + "react/jsx-closing-tag-location": 0, + "react/jsx-curly-brace-presence": 0, + "react/jsx-indent-props": 0, + "react/jsx-indent": 0, + "react/jsx-no-target-blank": 0, + "react/jsx-one-expression-per-line": 0, + "react/jsx-props-no-multi-spaces": 0, + "react/jsx-tag-spacing": 0, + "react/jsx-wrap-multilines": 0, + "react/no-access-state-in-setstate": 0, + "react/no-string-refs": 0, + "react/no-unescaped-entities": 0, + "react/no-unused-state": 0, + "react/require-default-props": 0, + "react/sort-comp": 0 + } +} diff --git a/ui/.gitignore b/ui/.gitignore new file mode 100644 index 000000000..1d48cce82 --- /dev/null +++ b/ui/.gitignore @@ -0,0 +1,31 @@ +# Logs +logs +sample +*.log + +# Mac Trash directories +*.DS_Store + +# Dependency directory +node_modules + +#intellij +.idea/ +*.iml +*.ipr +*.iws + +# generated scripts/css in public should not be commited +public/bundles +static_codegen + +# Code coverage reports +.nyc_output/ +coverage + +# HTTPS Certs +server.cert +server.key + +# Zipkin runner +zipkin-workspace \ No newline at end of file diff --git a/ui/.gitmodules b/ui/.gitmodules new file mode 100644 index 000000000..2a8a7b89a --- /dev/null +++ b/ui/.gitmodules @@ -0,0 +1,3 @@ +[submodule "haystack-idl"] + path = haystack-idl + url = https://github.com/ExpediaDotCom/haystack-idl diff --git a/ui/.npmrc b/ui/.npmrc new file mode 100644 index 000000000..5660f81af --- /dev/null +++ b/ui/.npmrc @@ -0,0 +1 @@ +registry=https://registry.npmjs.org/ \ No newline at end of file diff --git a/ui/.prettierignore b/ui/.prettierignore new file mode 100644 index 000000000..515bcd4f5 --- /dev/null +++ b/ui/.prettierignore @@ -0,0 +1,2 @@ +package.json +package-lock.json \ No newline at end of file diff --git a/ui/.prettierrc b/ui/.prettierrc new file mode 100644 index 000000000..22348c05a --- /dev/null +++ b/ui/.prettierrc @@ -0,0 +1,7 @@ +{ + "tabWidth": 4, + "printWidth": 150, + "singleQuote": true, + "bracketSpacing": false, + "arrowParens": "always" +} diff --git a/ui/.travis.yml b/ui/.travis.yml new file mode 100644 index 000000000..1f3a04b37 --- /dev/null +++ b/ui/.travis.yml @@ -0,0 +1,29 @@ +language: node_js + +node_js: + - "12" + +services: + - docker + +dist: xenial + +env: + global: + - BRANCH=${TRAVIS_BRANCH} + - TAG=${TRAVIS_TAG} + - SHA=${TRAVIS_COMMIT} + +before_install: + - sudo apt-get update + - sudo apt-get -y -o Dpkg::Options::="--force-confnew" install docker-ce + - sudo apt-get install libcairo2-dev libjpeg8-dev libpango1.0-dev libgif-dev build-essential g++ + +script: + # build, create docker image + # upload to dockerhub only for master(non PR) and tag scenario + - if ([ "$TRAVIS_BRANCH" == "master" ] && [ "$TRAVIS_PULL_REQUEST" == "false" ]) || [ -n "$TRAVIS_TAG" ]; then make release; else make all; fi + +notifications: + email: + - haystack-notifications@expedia.com diff --git a/ui/.vscode/settings.json b/ui/.vscode/settings.json new file mode 100644 index 000000000..a820ff1bd --- /dev/null +++ b/ui/.vscode/settings.json @@ -0,0 +1,8 @@ +// Place your settings in this file to overwrite default and user settings. +{ + // Prettier + "editor.formatOnSave": true, + "[less]": { + "editor.formatOnSave": true + } +} diff --git a/ui/CONTRIBUTING.md b/ui/CONTRIBUTING.md new file mode 100644 index 000000000..317757128 --- /dev/null +++ b/ui/CONTRIBUTING.md @@ -0,0 +1,14 @@ +##Bugs +We use Github Issues for our bug reporting. Please make sure the bug isn't already listed before opening a new issue. + +##Development +All work on Haystack happens directly on Github. Core Haystack team members will review opened pull requests. + +##Requests +If you see a feature that you would like to be added, please open an issue in the respective repository or in the general Haystack repo. + +##Contributing to Documentation +To contribute to documentation, you can directly modify the corresponding .md files in the docs directory under the base haystack repository, and submit a pull request. Once your PR is merged, the documentation is automatically built and deployed to https://expediadotcom.github.io/haystack. + +##License +By contributing to Haystack, you agree that your contributions will be licensed under its Apache License. \ No newline at end of file diff --git a/ui/LICENSE b/ui/LICENSE new file mode 100644 index 000000000..8dada3eda --- /dev/null +++ b/ui/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/ui/Makefile b/ui/Makefile new file mode 100644 index 000000000..3af339e21 --- /dev/null +++ b/ui/Makefile @@ -0,0 +1,23 @@ +.PHONY: clean build docker_build all release + +# docker namespace +export DOCKER_ORG := expediadotcom +export DOCKER_IMAGE_NAME := haystack-ui + +clean: + npm run clean + +install: + npm install + +build: clean install + npm run build + +docker_build: + docker build -t $(DOCKER_IMAGE_NAME) -f build/docker/Dockerfile . + +all: build docker_build + +# build all and release +release: all + ./build/docker/publish-to-docker-hub.sh diff --git a/ui/README.md b/ui/README.md new file mode 100644 index 000000000..e0abf792c --- /dev/null +++ b/ui/README.md @@ -0,0 +1,122 @@ +[![Build Status](https://travis-ci.org/ExpediaDotCom/haystack-ui.svg?branch=master)](https://travis-ci.org/ExpediaDotCom/haystack-ui) +[![Coverage Status](https://coveralls.io/repos/github/ExpediaDotCom/haystack-ui/badge.svg?branch=master)](https://coveralls.io/github/ExpediaDotCom/haystack-ui?branch=master&service=github) + + + +# Haystack-UI + +Haystack-ui is the web UI for haystack. It is the central place for visualizing processed data from various haystack sub-systems. +Visualization tools in haystack-ui include - + +| Traces | +| :------------------------------------------------------------: | +| Distributed tracing visualization for easy root cause analysis | +| ![Trace Timeline](./public/images/assets/trace_timeline.png) | + +| Trends | +| :---------------------------------------------------: | +| Visualization of vital service and operation trending | +| ![Trends](./public/images/assets/trends.png) | + +| Service Graph | +| :----------------------------------------------------------------: | +| Real time dependency graph with health and connectivity indicators | +| ![Service Graph](./public/images/assets/service_graph.png) | + +| Alerts and Anomaly Detection | +| :-------------------------------------------------------: | +| UI for displaying, configuring, and subscribing to alerts | +| ![Alerts](./public/images/assets/alerts.png) | + +| Universal Search | +| :--------------------------------------------------------------: | +| Intuitive, sandbox-style searching for accurate results. | +| ![Universal Search](./public/images/assets/universal_search.png) | + +## Development + +It is a expressjs based single page client side app written in ES6 + React and using Mobx for data flow. + +### Pre-requisites + +Ensure you have `node >= 10.0` and `npm >= 6.0` installed. + +Clone the repository including recursive submodules: + +``` +$ git clone --recurse-submodules https://github.com/ExpediaDotCom/haystack-ui.git +$ cd haystack-ui +``` + +If the repository was already cloned, you can initialize and update submodules with `git submodule update --init --recursive` + +### Build and Run + +This application uses [webpack](https://webpack.github.io/) for building + bundling resources. To run in developer mode with client and server side hotloading, use: + +``` +$ npm install # install dependencies +$ npm run start:dev # start server in dev mode with hotloading +``` + +Once start is successful you can visit [http://localhost:8080/](http://localhost:8080/) + +For running in production mode, use: + +``` +$ npm install # install dependencies +$ npm run build # run tests(with coverage), build client side code and emit produciton optimized bundles +$ npm start # start node server +``` + +#### Autoformatting in your favorite IDE with Prettier Integration + +This projects supports auto-formatting of source code! Simply find your favorite IDE from the list in the following list: https://prettier.io/docs/en/editors.html + +For VSCode support, perform the following steps: + +- Launch VS Code Quick Open (Ctrl+P) +- Paste the following command, and press enter: + +``` +ext install esbenp.prettier-vscode +``` + +This projects has a pre-configured `.vscode/settings.json` which enables format on save. Auto-formatting should execute everytime you save a file. + +Prettier is also configured to run in a pre-commit hook to make enforcing consistency of source code between developers easy. + +## Testing + +Haystack-ui utilizes [Mocha](https://github.com/mochajs/mocha) as it's testing framework, with [Chai](https://github.com/chaijs/chai) as the assertation library, [Enzyme](https://github.com/airbnb/enzyme) for utility, and [JSDOM](https://github.com/tmpvar/jsdom) as a headless browser for rendering React components. +[ESLint](https://github.com/eslint/eslint) is used as a linter and insurance of code quality. + +To run the test suite, enter the command `npm test`. + +To check code coverage, run `npm run coverage` and open the generated index.html in the created coverage folder + +**Note**- +You may have to install Cairo dependencies separately for tests to work. + +- **OS X Users** : `brew install pkg-config cairo pango libpng jpeg giflib` + - _NOTE_: If you run into `Package libffi was not found in the pkg-config search path.` errors while running `npm install`, you will need to addtionally run the following command: `export PKG_CONFIG_PATH="${PKG_CONFIG_PATH}:/usr/local/opt/libffi/lib/pkgconfig"` +- **Others**: Refer [https://www.npmjs.com/package/canvas#installation](https://www.npmjs.com/package/canvas#installation) + +### Docker + +We have provided `make` commands to facilitate building. For creating docker image use - + +``` +$ make all + +``` + +## Configuration + +Haystack UI can be configured to use one or more stores, each providing user interface for one subsystem in Haystack. Based on what subsystems you have available in your haystack cluster, you can configure corresponding stores and UI will adapt to show interfaces only for the configured subsystems. +For more details on this refer - [https://github.com/ExpediaDotCom/haystack-ui/wiki/Configuring-Subsystem-Connectors](https://github.com/ExpediaDotCom/haystack-ui/wiki/Configuring-Subsystem-Stores) + +## Haystack-ui as drop-in replacement for Zipkin UI + +If you have an existing zipkin cluster you can use haystack UI as a drop-in replacement for zipkin's UI. +For more details on this refer - [https://github.com/ExpediaDotCom/haystack-ui/wiki/Configuring-Subsystem-Connectors#using-haystack-ui-as-replacement-for-zipkin-ui](https://github.com/ExpediaDotCom/haystack-ui/wiki/Configuring-Subsystem-Connectors#using-haystack-ui-as-replacement-for-zipkin-ui) diff --git a/ui/Release.md b/ui/Release.md new file mode 100644 index 000000000..f231bf6f2 --- /dev/null +++ b/ui/Release.md @@ -0,0 +1,10 @@ +#Releasing +Currently we publish the repo to only docker hub. We don't publish it to nexus central repository since its a npm module. + +#How to release and publish + +* Git tagging: + +```git tag -a 1.x.x -m "Release description..."``` + +Or you can also tag using UI: https://github.com/ExpediaDotCom/haystack-ui/releases \ No newline at end of file diff --git a/ui/build/docker/Dockerfile b/ui/build/docker/Dockerfile new file mode 100644 index 000000000..e318e2112 --- /dev/null +++ b/ui/build/docker/Dockerfile @@ -0,0 +1,28 @@ +FROM node:12 AS base + +ENV APP_HOME /app +ENV PUBLIC_PATH /${APP_HOME}/public +ENV SERVER_PATH /${APP_HOME}/server +ENV UNIVERSAL_PATH /${APP_HOME}/universal +ENV NODE_MODULES_PATH /${APP_HOME}/node_modules +ENV STATIC_CODEGEN_PATH /${APP_HOME}/static_codegen +ENV PACKAGE_JSON_PATH /${APP_HOME}/package.json +WORKDIR ${APP_HOME} + +# generating proto code, building bundles and running tests +FROM base AS builder +COPY . . +RUN npm -q install +RUN npm -q run build + +# creating release image +FROM base AS release +COPY --from=builder ${PUBLIC_PATH} ${PUBLIC_PATH} +COPY --from=builder ${SERVER_PATH} ${SERVER_PATH} +COPY --from=builder ${UNIVERSAL_PATH} ${UNIVERSAL_PATH} +COPY --from=builder ${STATIC_CODEGEN_PATH} ${STATIC_CODEGEN_PATH} +COPY --from=builder ${PACKAGE_JSON_PATH} ${PACKAGE_JSON_PATH} +RUN npm -q install --only=prod + +EXPOSE 8080 +CMD node server/start.js diff --git a/ui/build/docker/publish-to-docker-hub.sh b/ui/build/docker/publish-to-docker-hub.sh new file mode 100755 index 000000000..473b61c41 --- /dev/null +++ b/ui/build/docker/publish-to-docker-hub.sh @@ -0,0 +1,38 @@ +#!/bin/bash + +set -e + +QUALIFIED_DOCKER_IMAGE_NAME=$DOCKER_ORG/$DOCKER_IMAGE_NAME +echo "DOCKER_ORG=$DOCKER_ORG, DOCKER_IMAGE_NAME=$DOCKER_IMAGE_NAME, QUALIFIED_DOCKER_IMAGE_NAME=$QUALIFIED_DOCKER_IMAGE_NAME" +echo "BRANCH=$BRANCH, TAG=$TAG, SHA=$SHA" + +# login +docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD + +# Add tags +if [[ $TAG =~ ([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then + echo "releasing semantic versions" + + unset MAJOR MINOR PATCH + MAJOR="${BASH_REMATCH[1]}" + MINOR="${BASH_REMATCH[2]}" + PATCH="${BASH_REMATCH[3]}" + + # for tag, add MAJOR, MAJOR.MINOR, MAJOR.MINOR.PATCH and latest as tag + docker tag $DOCKER_IMAGE_NAME $QUALIFIED_DOCKER_IMAGE_NAME:$MAJOR + docker tag $DOCKER_IMAGE_NAME $QUALIFIED_DOCKER_IMAGE_NAME:$MAJOR.$MINOR + docker tag $DOCKER_IMAGE_NAME $QUALIFIED_DOCKER_IMAGE_NAME:$MAJOR.$MINOR.$PATCH + docker tag $DOCKER_IMAGE_NAME $QUALIFIED_DOCKER_IMAGE_NAME:latest + + # publish image with tags + docker push $QUALIFIED_DOCKER_IMAGE_NAME + +elif [[ "$BRANCH" == "master" ]]; then + echo "releasing master branch" + + # for 'master' branch, add SHA as tags + docker tag $DOCKER_IMAGE_NAME $QUALIFIED_DOCKER_IMAGE_NAME:$SHA + + # publish image with tags + docker push $QUALIFIED_DOCKER_IMAGE_NAME +fi diff --git a/ui/build/zipkin/README.md b/ui/build/zipkin/README.md new file mode 100644 index 000000000..5fc8ba6da --- /dev/null +++ b/ui/build/zipkin/README.md @@ -0,0 +1,26 @@ + +# Zipkin haystack-ui Quickstart Utility + +Utility script to run haystack-ui with a zipkin instance as backend for traces. It spins sleuth-webmvc-example services for feeding traces in Zipkin cluster and generates some examples. It configures haystack-ui point to Zipkin V2 api and has only traces subsystem. + + +### PREREQUISITES + +- Assumes that you have mvn and git available on your machine. +- haystack-ui must be already installed (npm install) and built (npm build), if not please install and build before running this script + + +### USAGE + +```> ./zipkin-quickstart``` + +Wait for couple of minutes till you see `Express server listening : 8080` message. Then you can hit [http://localhost:8080/search?serviceName=backend](http://localhost:8080/search?serviceName=backend) to use haystack-ui. Search for `serviceName=backend` to see pre-feeded traces coming from Zipkin backend. + + +### OPTIONS + +``` +-h help +-d debug mode, will emit out all logs from zipkin and sleuth-webmvc-example +``` + diff --git a/ui/build/zipkin/base.json b/ui/build/zipkin/base.json new file mode 100644 index 000000000..f94fe1d72 --- /dev/null +++ b/ui/build/zipkin/base.json @@ -0,0 +1,15 @@ +{ + "port": 8080, + "cluster": false, + "upstreamTimeout": 30000, + "connectors": { + "traces": { + "connectorName": "zipkin", + "zipkinUrl": "http://localhost:9411/api/v2" + }, + "serviceGraph": { + "connectorName": "zipkin", + "zipkinUrl": "http://localhost:9411/api/v2" + } + } +} diff --git a/ui/build/zipkin/zipkin-quickstart.sh b/ui/build/zipkin/zipkin-quickstart.sh new file mode 100755 index 000000000..f65bab107 --- /dev/null +++ b/ui/build/zipkin/zipkin-quickstart.sh @@ -0,0 +1,116 @@ +#!/usr/bin/env bash + +usage() { + cat < downloading sleuth webmvc example\n\n' + git clone https://github.com/openzipkin/sleuth-webmvc-example.git +} + +runSleuthExample() { + printf '\n=> %s\n\n' "running sleuth webmvc ${1}" + if [[ "$2" = '-d' ]]; then + mvn compile exec:java -Dexec.mainClass=sleuth.webmvc.${1} & + else + mvn compile exec:java -Dexec.mainClass=sleuth.webmvc.${1} > /dev/null 2>&1 & + fi +} + +downloadAndRunZipkin() { + printf '\n=> downloading and running zipkin\n\n' + curl -sSL https://zipkin.io/quickstart.sh | bash -s + + if [[ "$1" = '-d' ]]; then + java -jar zipkin.jar & + else + java -jar zipkin.jar > /dev/null 2>&1 & + fi +} + +downloadAndRunZipkin() { + printf '\n=> downloading and running zipkin\n\n' + curl -sSL https://zipkin.io/quickstart.sh | bash -s + java -jar zipkin.jar > /dev/null 2>&1 & +} + +feedTraces() { + printf '\n=> waiting for zipkin to start and pushing sample traces\n\n' + sleep 60 + curl http://localhost:8081 + curl http://localhost:8081 + curl http://localhost:8081 + curl http://localhost:8081 + curl http://localhost:8081 + curl http://localhost:8081 + curl http://localhost:9000/api + curl http://localhost:9000/api + curl http://localhost:9000/api + curl http://localhost:9000/api + sleep 60 +} + +startHaystackUi() { + printf '\n=> starting haystack-ui\n\n' + cd ../../../ + HAYSTACK_OVERRIDES_CONFIG_PATH=../../build/zipkin/base.json npm start +} + +main() { + if [[ "$1" = '-h' || "$1" = '--help' ]]; then + usage + exit + fi + + # execution directory + local WORKSPACE=./zipkin-workspace + rm -rf $WORKSPACE + mkdir $WORKSPACE + cd $WORKSPACE + + # download and run zipkin + downloadAndRunZipkin "$1" + ZIPKIN_PROC_ID=$! + printf '\n=> zipkin proc id %s\n\n' "${ZIPKIN_PROC_ID}" + + # download and run sleuth example backend and frontend + fetchSleuthExample + cd sleuth-webmvc-example + runSleuthExample "Backend" "$1" + SLEUTH_BACKEND_PROC_ID=$! + printf '\n=> backend proc id %s\n\n' "${SLEUTH_BACKEND_PROC_ID}" + runSleuthExample "Frontend" "$1" + SLEUTH_FRONTEND_PROC_ID=$! + printf '\n=> frontend proc id %s\n\n' "${SLEUTH_FRONTEND_PROC_ID}" + cd .. + + # feed traces to zipkin + feedTraces + + # run haystack ui + startHaystackUi + + # teardown services + printf '\n=> tearing down\n\n' + kill $SLEUTH_BACKEND_PROC_ID + kill $SLEUTH_FRONTEND_PROC_ID + kill $ZIPKIN_PROC_ID +} + +main "$@" diff --git a/ui/deployment/terraform/main.tf b/ui/deployment/terraform/main.tf new file mode 100644 index 000000000..2a34dc082 --- /dev/null +++ b/ui/deployment/terraform/main.tf @@ -0,0 +1,79 @@ +locals { + app_name = "haystack-ui" + count = "${var.enabled?1:0}" + config_file_path = "${path.module}/templates/haystack-ui_json.tpl" + container_config_path = "/config/haystack-ui.json" + deployment_yaml_file_path = "${path.module}/templates/deployment_yaml.tpl" + checksum = "${sha1("${data.template_file.config_data.rendered}")}" + configmap_name = "ui-${local.checksum}" +} + + +resource "kubernetes_config_map" "haystack-config" { + metadata { + name = "${local.configmap_name}" + namespace = "${var.namespace}" + } + data { + "haystack-ui.json" = "${data.template_file.config_data.rendered}" + } + count = "${local.count}" +} + +data "template_file" "config_data" { + template = "${file("${local.config_file_path}")}" + + vars { + trace_reader_hostname = "${var.trace_reader_hostname}" + trace_reader_service_port = "${var.trace_reader_service_port}" + metrictank_hostname = "${var.metrictank_hostname}" + metrictank_port = "${var.metrictank_port}" + graphite_port = "${var.graphite_port}" + graphite_hostname = "${var.graphite_hostname}" + whitelisted_fields = "${var.whitelisted_fields}" + ui_enable_sso = "${var.ui_enable_sso}" + ui_saml_callback_url = "${var.ui_saml_callback_url}" + ui_saml_entry_point = "${var.ui_saml_entry_point}" + ui_saml_issuer = "${var.ui_saml_issuer}" + ui_session_secret = "${var.ui_session_secret}" + encoder_type = "${var.encoder_type}" + } +} + +data "template_file" "deployment_yaml" { + template = "${file("${local.deployment_yaml_file_path}")}" + vars { + app_name = "${local.app_name}" + namespace = "${var.namespace}" + node_selecter_label = "${var.node_selecter_label}" + image = "${var.image}" + replicas = "${var.replicas}" + memory_limit = "${var.memory_limit}" + memory_request = "${var.memory_request}" + cpu_limit = "${var.cpu_limit}" + cpu_request = "${var.cpu_request}" + service_port = "${var.service_port}" + container_port = "${var.container_port}" + configmap_name = "${local.configmap_name}" + } +} + +resource "null_resource" "kubectl_apply" { + triggers { + template = "${data.template_file.deployment_yaml.rendered}" + } + provisioner "local-exec" { + command = "echo '${data.template_file.deployment_yaml.rendered}' | ${var.kubectl_executable_name} apply -f - --context ${var.kubectl_context_name}" + } + count = "${local.count}" +} + + +resource "null_resource" "kubectl_destroy" { + + provisioner "local-exec" { + command = "echo '${data.template_file.deployment_yaml.rendered}' | ${var.kubectl_executable_name} delete -f - --context ${var.kubectl_context_name}" + when = "destroy" + } + count = "${local.count}" +} diff --git a/ui/deployment/terraform/outputs.tf b/ui/deployment/terraform/outputs.tf new file mode 100644 index 000000000..e69de29bb diff --git a/ui/deployment/terraform/templates/deployment_yaml.tpl b/ui/deployment/terraform/templates/deployment_yaml.tpl new file mode 100644 index 000000000..619501734 --- /dev/null +++ b/ui/deployment/terraform/templates/deployment_yaml.tpl @@ -0,0 +1,59 @@ +# ------------------- Deployment ------------------- # + +kind: Deployment +apiVersion: apps/v1beta2 +metadata: + labels: + k8s-app: ${app_name} + name: ${app_name} + namespace: ${namespace} +spec: + replicas: ${replicas} + revisionHistoryLimit: 10 + selector: + matchLabels: + k8s-app: ${app_name} + template: + metadata: + labels: + k8s-app: ${app_name} + spec: + containers: + - name: ${app_name} + image: ${image} + volumeMounts: + # Create on-disk volume to store exec logs + - mountPath: /config + name: config-volume + resources: + limits: + cpu: ${cpu_limit} + memory: ${memory_limit}Mi + requests: + cpu: ${cpu_request} + memory: ${memory_limit}Mi + env: + - name: "HAYSTACK_OVERRIDES_CONFIG_PATH" + value: "/config/haystack-ui.json" + nodeSelector: + ${node_selecter_label} + volumes: + - name: config-volume + configMap: + name: ${configmap_name} + +# ------------------- Service ------------------- # +--- +apiVersion: v1 +kind: Service +metadata: + labels: + k8s-app: ${app_name} + name: ${app_name} + namespace: ${namespace} +spec: + ports: + - port: ${service_port} + targetPort: ${container_port} + selector: + k8s-app: ${app_name} diff --git a/ui/deployment/terraform/templates/haystack-ui_json.tpl b/ui/deployment/terraform/templates/haystack-ui_json.tpl new file mode 100644 index 000000000..92910c485 --- /dev/null +++ b/ui/deployment/terraform/templates/haystack-ui_json.tpl @@ -0,0 +1,47 @@ +{ + "port": 8080, + "cluster": true, + "upstreamTimeout": 30000, + "encoder": "${encoder_type}", + "enableServicePerformance": false, + "enableServiceLevelTrends": false, + "enableLatencyCostViewer": true, + "graphite": { + "host": "${graphite_hostname}", + "port": ${graphite_port} + }, + "grpcOptions": { + "grpc.max_receive_message_length": 52428800 + }, + "connectors": { + "traces": { + "connectorName": "haystack", + "haystackHost": "${trace_reader_hostname}", + "haystackPort": ${trace_reader_service_port}, + "serviceRefreshIntervalInSecs": 60, + "fieldKeys": [${whitelisted_fields}] + }, + "trends": { + "connectorName": "haystack", + "metricTankUrl": "http://${metrictank_hostname}:${metrictank_port}" + }, + "alerts": { + "connectorName": "haystack", + "metricTankUrl": "http://${metrictank_hostname}:${metrictank_port}", + "alertFreqInSec": 300, + "alertMergeBufferTimeInSec": 60, + "subscriptions": { + "connectorName": "stub", + "enabled": false + } + } + }, + "enableSSO": ${ui_enable_sso}, + "saml": { + "callbackUrl": "${ui_saml_callback_url}", + "entry_point": "${ui_saml_entry_point}", + "issuer": "${ui_saml_issuer}" + }, + "sessionTimeout": 3600000, + "sessionSecret": "${ui_session_secret}" +} diff --git a/ui/deployment/terraform/variables.tf b/ui/deployment/terraform/variables.tf new file mode 100644 index 000000000..2a1bf3f83 --- /dev/null +++ b/ui/deployment/terraform/variables.tf @@ -0,0 +1,51 @@ +variable "enabled" { + default = true +} + +variable "image" {} +variable "replicas" {} +variable "namespace" {} +variable "kubectl_executable_name" {} +variable "kubectl_context_name" {} +variable "node_selecter_label"{} +variable "memory_request"{} +variable "memory_limit"{} +variable "cpu_request"{} +variable "cpu_limit"{} +variable "graphite_hostname" {} +variable "graphite_port" {} +variable "encoder_type" {} + +variable "termination_grace_period" { + default = 30 +} +variable "service_port" { + default = 80 +} +variable "container_port" { + default = 8080 +} + +variable "k8s_cluster_name" {} + +variable "trace_reader_hostname" {} + +variable "trace_reader_service_port" {} + +variable "metrictank_hostname" {} + +variable "metrictank_port" {} + +variable "whitelisted_fields" {} + +variable "ui_enable_sso" { + default = false +} + +variable "ui_saml_callback_url" {} + +variable "ui_saml_entry_point" {} + +variable "ui_saml_issuer" {} + +variable "ui_session_secret" {} diff --git a/ui/package-lock.json b/ui/package-lock.json new file mode 100644 index 000000000..56e9b40d8 --- /dev/null +++ b/ui/package-lock.json @@ -0,0 +1,14057 @@ +{ + "name": "haystack-ui", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@babel/cli": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.8.4.tgz", + "integrity": "sha512-XXLgAm6LBbaNxaGhMAznXXaxtCWfuv6PIDJ9Alsy9JYTOh+j2jJz+L/162kkfU1j/pTSxK1xGmlwI4pdIMkoag==", + "dev": true, + "requires": { + "chokidar": "^2.1.8", + "commander": "^4.0.1", + "convert-source-map": "^1.1.0", + "fs-readdir-recursive": "^1.1.0", + "glob": "^7.0.0", + "lodash": "^4.17.13", + "make-dir": "^2.1.0", + "slash": "^2.0.0", + "source-map": "^0.5.0" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "@babel/code-frame": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz", + "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==", + "dev": true, + "requires": { + "@babel/highlight": "^7.8.3" + } + }, + "@babel/compat-data": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.8.5.tgz", + "integrity": "sha512-jWYUqQX/ObOhG1UiEkbH5SANsE/8oKXiQWjj7p7xgj9Zmnt//aUvyz4dBkK0HNsS8/cbyC5NmmH87VekW+mXFg==", + "dev": true, + "requires": { + "browserslist": "^4.8.5", + "invariant": "^2.2.4", + "semver": "^5.5.0" + } + }, + "@babel/core": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.8.4.tgz", + "integrity": "sha512-0LiLrB2PwrVI+a2/IEskBopDYSd8BCb3rOvH7D5tzoWd696TBEduBvuLVm4Nx6rltrLZqvI3MCalB2K2aVzQjA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.8.3", + "@babel/generator": "^7.8.4", + "@babel/helpers": "^7.8.4", + "@babel/parser": "^7.8.4", + "@babel/template": "^7.8.3", + "@babel/traverse": "^7.8.4", + "@babel/types": "^7.8.3", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.1", + "json5": "^2.1.0", + "lodash": "^4.17.13", + "resolve": "^1.3.2", + "semver": "^5.4.1", + "source-map": "^0.5.0" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "@babel/generator": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.8.4.tgz", + "integrity": "sha512-PwhclGdRpNAf3IxZb0YVuITPZmmrXz9zf6fH8lT4XbrmfQKr6ryBzhv593P5C6poJRciFCL/eHGW2NuGrgEyxA==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3", + "jsesc": "^2.5.1", + "lodash": "^4.17.13", + "source-map": "^0.5.0" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "@babel/helper-annotate-as-pure": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.8.3.tgz", + "integrity": "sha512-6o+mJrZBxOoEX77Ezv9zwW7WV8DdluouRKNY/IR5u/YTMuKHgugHOzYWlYvYLpLA9nPsQCAAASpCIbjI9Mv+Uw==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3" + } + }, + "@babel/helper-builder-binary-assignment-operator-visitor": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.8.3.tgz", + "integrity": "sha512-5eFOm2SyFPK4Rh3XMMRDjN7lBH0orh3ss0g3rTYZnBQ+r6YPj7lgDyCvPphynHvUrobJmeMignBr6Acw9mAPlw==", + "dev": true, + "requires": { + "@babel/helper-explode-assignable-expression": "^7.8.3", + "@babel/types": "^7.8.3" + } + }, + "@babel/helper-builder-react-jsx": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-react-jsx/-/helper-builder-react-jsx-7.8.3.tgz", + "integrity": "sha512-JT8mfnpTkKNCboTqZsQTdGo3l3Ik3l7QIt9hh0O9DYiwVel37VoJpILKM4YFbP2euF32nkQSb+F9cUk9b7DDXQ==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3", + "esutils": "^2.0.0" + } + }, + "@babel/helper-call-delegate": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-call-delegate/-/helper-call-delegate-7.8.3.tgz", + "integrity": "sha512-6Q05px0Eb+N4/GTyKPPvnkig7Lylw+QzihMpws9iiZQv7ZImf84ZsZpQH7QoWN4n4tm81SnSzPgHw2qtO0Zf3A==", + "dev": true, + "requires": { + "@babel/helper-hoist-variables": "^7.8.3", + "@babel/traverse": "^7.8.3", + "@babel/types": "^7.8.3" + } + }, + "@babel/helper-compilation-targets": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.8.4.tgz", + "integrity": "sha512-3k3BsKMvPp5bjxgMdrFyq0UaEO48HciVrOVF0+lon8pp95cyJ2ujAh0TrBHNMnJGT2rr0iKOJPFFbSqjDyf/Pg==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.8.4", + "browserslist": "^4.8.5", + "invariant": "^2.2.4", + "levenary": "^1.1.1", + "semver": "^5.5.0" + } + }, + "@babel/helper-create-class-features-plugin": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.8.3.tgz", + "integrity": "sha512-qmp4pD7zeTxsv0JNecSBsEmG1ei2MqwJq4YQcK3ZWm/0t07QstWfvuV/vm3Qt5xNMFETn2SZqpMx2MQzbtq+KA==", + "dev": true, + "requires": { + "@babel/helper-function-name": "^7.8.3", + "@babel/helper-member-expression-to-functions": "^7.8.3", + "@babel/helper-optimise-call-expression": "^7.8.3", + "@babel/helper-plugin-utils": "^7.8.3", + "@babel/helper-replace-supers": "^7.8.3", + "@babel/helper-split-export-declaration": "^7.8.3" + } + }, + "@babel/helper-create-regexp-features-plugin": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.8.3.tgz", + "integrity": "sha512-Gcsm1OHCUr9o9TcJln57xhWHtdXbA2pgQ58S0Lxlks0WMGNXuki4+GLfX0p+L2ZkINUGZvfkz8rzoqJQSthI+Q==", + "dev": true, + "requires": { + "@babel/helper-regex": "^7.8.3", + "regexpu-core": "^4.6.0" + } + }, + "@babel/helper-define-map": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-define-map/-/helper-define-map-7.8.3.tgz", + "integrity": "sha512-PoeBYtxoZGtct3md6xZOCWPcKuMuk3IHhgxsRRNtnNShebf4C8YonTSblsK4tvDbm+eJAw2HAPOfCr+Q/YRG/g==", + "dev": true, + "requires": { + "@babel/helper-function-name": "^7.8.3", + "@babel/types": "^7.8.3", + "lodash": "^4.17.13" + } + }, + "@babel/helper-explode-assignable-expression": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.8.3.tgz", + "integrity": "sha512-N+8eW86/Kj147bO9G2uclsg5pwfs/fqqY5rwgIL7eTBklgXjcOJ3btzS5iM6AitJcftnY7pm2lGsrJVYLGjzIw==", + "dev": true, + "requires": { + "@babel/traverse": "^7.8.3", + "@babel/types": "^7.8.3" + } + }, + "@babel/helper-function-name": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.8.3.tgz", + "integrity": "sha512-BCxgX1BC2hD/oBlIFUgOCQDOPV8nSINxCwM3o93xP4P9Fq6aV5sgv2cOOITDMtCfQ+3PvHp3l689XZvAM9QyOA==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.8.3", + "@babel/template": "^7.8.3", + "@babel/types": "^7.8.3" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz", + "integrity": "sha512-FVDR+Gd9iLjUMY1fzE2SR0IuaJToR4RkCDARVfsBBPSP53GEqSFjD8gNyxg246VUyc/ALRxFaAK8rVG7UT7xRA==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3" + } + }, + "@babel/helper-hoist-variables": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.8.3.tgz", + "integrity": "sha512-ky1JLOjcDUtSc+xkt0xhYff7Z6ILTAHKmZLHPxAhOP0Nd77O+3nCsd6uSVYur6nJnCI029CrNbYlc0LoPfAPQg==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3" + } + }, + "@babel/helper-member-expression-to-functions": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.8.3.tgz", + "integrity": "sha512-fO4Egq88utkQFjbPrSHGmGLFqmrshs11d46WI+WZDESt7Wu7wN2G2Iu+NMMZJFDOVRHAMIkB5SNh30NtwCA7RA==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3" + } + }, + "@babel/helper-module-imports": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.8.3.tgz", + "integrity": "sha512-R0Bx3jippsbAEtzkpZ/6FIiuzOURPcMjHp+Z6xPe6DtApDJx+w7UYyOLanZqO8+wKR9G10s/FmHXvxaMd9s6Kg==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3" + } + }, + "@babel/helper-module-transforms": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.8.3.tgz", + "integrity": "sha512-C7NG6B7vfBa/pwCOshpMbOYUmrYQDfCpVL/JCRu0ek8B5p8kue1+BCXpg2vOYs7w5ACB9GTOBYQ5U6NwrMg+3Q==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.8.3", + "@babel/helper-simple-access": "^7.8.3", + "@babel/helper-split-export-declaration": "^7.8.3", + "@babel/template": "^7.8.3", + "@babel/types": "^7.8.3", + "lodash": "^4.17.13" + } + }, + "@babel/helper-optimise-call-expression": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.8.3.tgz", + "integrity": "sha512-Kag20n86cbO2AvHca6EJsvqAd82gc6VMGule4HwebwMlwkpXuVqrNRj6CkCV2sKxgi9MyAUnZVnZ6lJ1/vKhHQ==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3" + } + }, + "@babel/helper-plugin-utils": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz", + "integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==", + "dev": true + }, + "@babel/helper-regex": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-regex/-/helper-regex-7.8.3.tgz", + "integrity": "sha512-BWt0QtYv/cg/NecOAZMdcn/waj/5P26DR4mVLXfFtDokSR6fyuG0Pj+e2FqtSME+MqED1khnSMulkmGl8qWiUQ==", + "dev": true, + "requires": { + "lodash": "^4.17.13" + } + }, + "@babel/helper-remap-async-to-generator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.8.3.tgz", + "integrity": "sha512-kgwDmw4fCg7AVgS4DukQR/roGp+jP+XluJE5hsRZwxCYGg+Rv9wSGErDWhlI90FODdYfd4xG4AQRiMDjjN0GzA==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.8.3", + "@babel/helper-wrap-function": "^7.8.3", + "@babel/template": "^7.8.3", + "@babel/traverse": "^7.8.3", + "@babel/types": "^7.8.3" + } + }, + "@babel/helper-replace-supers": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.8.3.tgz", + "integrity": "sha512-xOUssL6ho41U81etpLoT2RTdvdus4VfHamCuAm4AHxGr+0it5fnwoVdwUJ7GFEqCsQYzJUhcbsN9wB9apcYKFA==", + "dev": true, + "requires": { + "@babel/helper-member-expression-to-functions": "^7.8.3", + "@babel/helper-optimise-call-expression": "^7.8.3", + "@babel/traverse": "^7.8.3", + "@babel/types": "^7.8.3" + } + }, + "@babel/helper-simple-access": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.8.3.tgz", + "integrity": "sha512-VNGUDjx5cCWg4vvCTR8qQ7YJYZ+HBjxOgXEl7ounz+4Sn7+LMD3CFrCTEU6/qXKbA2nKg21CwhhBzO0RpRbdCw==", + "dev": true, + "requires": { + "@babel/template": "^7.8.3", + "@babel/types": "^7.8.3" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.8.3.tgz", + "integrity": "sha512-3x3yOeyBhW851hroze7ElzdkeRXQYQbFIb7gLK1WQYsw2GWDay5gAJNw1sWJ0VFP6z5J1whqeXH/WCdCjZv6dA==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3" + } + }, + "@babel/helper-wrap-function": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.8.3.tgz", + "integrity": "sha512-LACJrbUET9cQDzb6kG7EeD7+7doC3JNvUgTEQOx2qaO1fKlzE/Bf05qs9w1oXQMmXlPO65lC3Tq9S6gZpTErEQ==", + "dev": true, + "requires": { + "@babel/helper-function-name": "^7.8.3", + "@babel/template": "^7.8.3", + "@babel/traverse": "^7.8.3", + "@babel/types": "^7.8.3" + } + }, + "@babel/helpers": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.8.4.tgz", + "integrity": "sha512-VPbe7wcQ4chu4TDQjimHv/5tj73qz88o12EPkO2ValS2QiQS/1F2SsjyIGNnAD0vF/nZS6Cf9i+vW6HIlnaR8w==", + "dev": true, + "requires": { + "@babel/template": "^7.8.3", + "@babel/traverse": "^7.8.4", + "@babel/types": "^7.8.3" + } + }, + "@babel/highlight": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.8.3.tgz", + "integrity": "sha512-PX4y5xQUvy0fnEVHrYOarRPXVWafSjTW9T0Hab8gVIawpl2Sj0ORyrygANq+KjcNlSSTw0YCLSNA8OyZ1I4yEg==", + "dev": true, + "requires": { + "chalk": "^2.0.0", + "esutils": "^2.0.2", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.8.4.tgz", + "integrity": "sha512-0fKu/QqildpXmPVaRBoXOlyBb3MC+J0A66x97qEfLOMkn3u6nfY5esWogQwi/K0BjASYy4DbnsEWnpNL6qT5Mw==", + "dev": true + }, + "@babel/plugin-proposal-async-generator-functions": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.8.3.tgz", + "integrity": "sha512-NZ9zLv848JsV3hs8ryEh7Uaz/0KsmPLqv0+PdkDJL1cJy0K4kOCFa8zc1E3mp+RHPQcpdfb/6GovEsW4VDrOMw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.3", + "@babel/helper-remap-async-to-generator": "^7.8.3", + "@babel/plugin-syntax-async-generators": "^7.8.0" + } + }, + "@babel/plugin-proposal-class-properties": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.8.3.tgz", + "integrity": "sha512-EqFhbo7IosdgPgZggHaNObkmO1kNUe3slaKu54d5OWvy+p9QIKOzK1GAEpAIsZtWVtPXUHSMcT4smvDrCfY4AA==", + "dev": true, + "requires": { + "@babel/helper-create-class-features-plugin": "^7.8.3", + "@babel/helper-plugin-utils": "^7.8.3" + } + }, + "@babel/plugin-proposal-decorators": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.8.3.tgz", + "integrity": "sha512-e3RvdvS4qPJVTe288DlXjwKflpfy1hr0j5dz5WpIYYeP7vQZg2WfAEIp8k5/Lwis/m5REXEteIz6rrcDtXXG7w==", + "dev": true, + "requires": { + "@babel/helper-create-class-features-plugin": "^7.8.3", + "@babel/helper-plugin-utils": "^7.8.3", + "@babel/plugin-syntax-decorators": "^7.8.3" + } + }, + "@babel/plugin-proposal-dynamic-import": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.8.3.tgz", + "integrity": "sha512-NyaBbyLFXFLT9FP+zk0kYlUlA8XtCUbehs67F0nnEg7KICgMc2mNkIeu9TYhKzyXMkrapZFwAhXLdnt4IYHy1w==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.3", + "@babel/plugin-syntax-dynamic-import": "^7.8.0" + } + }, + "@babel/plugin-proposal-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.8.3.tgz", + "integrity": "sha512-KGhQNZ3TVCQG/MjRbAUwuH+14y9q0tpxs1nWWs3pbSleRdDro9SAMMDyye8HhY1gqZ7/NqIc8SKhya0wRDgP1Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.3", + "@babel/plugin-syntax-json-strings": "^7.8.0" + } + }, + "@babel/plugin-proposal-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-TS9MlfzXpXKt6YYomudb/KU7nQI6/xnapG6in1uZxoxDghuSMZsPb6D2fyUwNYSAp4l1iR7QtFOjkqcRYcUsfw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.3", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.0" + } + }, + "@babel/plugin-proposal-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-8qvuPwU/xxUCt78HocNlv0mXXo0wdh9VT1R04WU8HGOfaOob26pF+9P5/lYjN/q7DHOX1bvX60hnhOvuQUJdbA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.3", + "@babel/plugin-syntax-object-rest-spread": "^7.8.0" + } + }, + "@babel/plugin-proposal-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-0gkX7J7E+AtAw9fcwlVQj8peP61qhdg/89D5swOkjYbkboA2CVckn3kiyum1DE0wskGb7KJJxBdyEBApDLLVdw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.0" + } + }, + "@babel/plugin-proposal-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.8.3.tgz", + "integrity": "sha512-QIoIR9abkVn+seDE3OjA08jWcs3eZ9+wJCKSRgo3WdEU2csFYgdScb+8qHB3+WXsGJD55u+5hWCISI7ejXS+kg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.0" + } + }, + "@babel/plugin-proposal-unicode-property-regex": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.8.3.tgz", + "integrity": "sha512-1/1/rEZv2XGweRwwSkLpY+s60za9OZ1hJs4YDqFHCw0kYWYwL5IFljVY1MYBL+weT1l9pokDO2uhSTLVxzoHkQ==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.8.3", + "@babel/helper-plugin-utils": "^7.8.3" + } + }, + "@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-decorators": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.8.3.tgz", + "integrity": "sha512-8Hg4dNNT9/LcA1zQlfwuKR8BUc/if7Q7NkTam9sGTcJphLwpf2g4S42uhspQrIrR+dpzE0dtTqBVFoHl8GtnnQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.3" + } + }, + "@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", + "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-jsx": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.8.3.tgz", + "integrity": "sha512-WxdW9xyLgBdefoo0Ynn3MRSkhe5tFVxxKNVdnZSh318WrG2e2jH+E9wd/++JsqcLJZPfz87njQJ8j2Upjm0M0A==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.3" + } + }, + "@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-top-level-await": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.8.3.tgz", + "integrity": "sha512-kwj1j9lL/6Wd0hROD3b/OZZ7MSrZLqqn9RAZ5+cYYsflQ9HZBIKCUkr3+uL1MEJ1NePiUbf98jjiMQSv0NMR9g==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.3" + } + }, + "@babel/plugin-transform-arrow-functions": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.8.3.tgz", + "integrity": "sha512-0MRF+KC8EqH4dbuITCWwPSzsyO3HIWWlm30v8BbbpOrS1B++isGxPnnuq/IZvOX5J2D/p7DQalQm+/2PnlKGxg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.3" + } + }, + "@babel/plugin-transform-async-to-generator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.8.3.tgz", + "integrity": "sha512-imt9tFLD9ogt56Dd5CI/6XgpukMwd/fLGSrix2httihVe7LOGVPhyhMh1BU5kDM7iHD08i8uUtmV2sWaBFlHVQ==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.8.3", + "@babel/helper-plugin-utils": "^7.8.3", + "@babel/helper-remap-async-to-generator": "^7.8.3" + } + }, + "@babel/plugin-transform-block-scoped-functions": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.8.3.tgz", + "integrity": "sha512-vo4F2OewqjbB1+yaJ7k2EJFHlTP3jR634Z9Cj9itpqNjuLXvhlVxgnjsHsdRgASR8xYDrx6onw4vW5H6We0Jmg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.3" + } + }, + "@babel/plugin-transform-block-scoping": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.8.3.tgz", + "integrity": "sha512-pGnYfm7RNRgYRi7bids5bHluENHqJhrV4bCZRwc5GamaWIIs07N4rZECcmJL6ZClwjDz1GbdMZFtPs27hTB06w==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.3", + "lodash": "^4.17.13" + } + }, + "@babel/plugin-transform-classes": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.8.3.tgz", + "integrity": "sha512-SjT0cwFJ+7Rbr1vQsvphAHwUHvSUPmMjMU/0P59G8U2HLFqSa082JO7zkbDNWs9kH/IUqpHI6xWNesGf8haF1w==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.8.3", + "@babel/helper-define-map": "^7.8.3", + "@babel/helper-function-name": "^7.8.3", + "@babel/helper-optimise-call-expression": "^7.8.3", + "@babel/helper-plugin-utils": "^7.8.3", + "@babel/helper-replace-supers": "^7.8.3", + "@babel/helper-split-export-declaration": "^7.8.3", + "globals": "^11.1.0" + } + }, + "@babel/plugin-transform-computed-properties": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.8.3.tgz", + "integrity": "sha512-O5hiIpSyOGdrQZRQ2ccwtTVkgUDBBiCuK//4RJ6UfePllUTCENOzKxfh6ulckXKc0DixTFLCfb2HVkNA7aDpzA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.3" + } + }, + "@babel/plugin-transform-destructuring": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.8.3.tgz", + "integrity": "sha512-H4X646nCkiEcHZUZaRkhE2XVsoz0J/1x3VVujnn96pSoGCtKPA99ZZA+va+gK+92Zycd6OBKCD8tDb/731bhgQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.3" + } + }, + "@babel/plugin-transform-dotall-regex": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.8.3.tgz", + "integrity": "sha512-kLs1j9Nn4MQoBYdRXH6AeaXMbEJFaFu/v1nQkvib6QzTj8MZI5OQzqmD83/2jEM1z0DLilra5aWO5YpyC0ALIw==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.8.3", + "@babel/helper-plugin-utils": "^7.8.3" + } + }, + "@babel/plugin-transform-duplicate-keys": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.8.3.tgz", + "integrity": "sha512-s8dHiBUbcbSgipS4SMFuWGqCvyge5V2ZeAWzR6INTVC3Ltjig/Vw1G2Gztv0vU/hRG9X8IvKvYdoksnUfgXOEQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.3" + } + }, + "@babel/plugin-transform-exponentiation-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.8.3.tgz", + "integrity": "sha512-zwIpuIymb3ACcInbksHaNcR12S++0MDLKkiqXHl3AzpgdKlFNhog+z/K0+TGW+b0w5pgTq4H6IwV/WhxbGYSjQ==", + "dev": true, + "requires": { + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.8.3", + "@babel/helper-plugin-utils": "^7.8.3" + } + }, + "@babel/plugin-transform-for-of": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.8.4.tgz", + "integrity": "sha512-iAXNlOWvcYUYoV8YIxwS7TxGRJcxyl8eQCfT+A5j8sKUzRFvJdcyjp97jL2IghWSRDaL2PU2O2tX8Cu9dTBq5A==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.3" + } + }, + "@babel/plugin-transform-function-name": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.8.3.tgz", + "integrity": "sha512-rO/OnDS78Eifbjn5Py9v8y0aR+aSYhDhqAwVfsTl0ERuMZyr05L1aFSCJnbv2mmsLkit/4ReeQ9N2BgLnOcPCQ==", + "dev": true, + "requires": { + "@babel/helper-function-name": "^7.8.3", + "@babel/helper-plugin-utils": "^7.8.3" + } + }, + "@babel/plugin-transform-literals": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.8.3.tgz", + "integrity": "sha512-3Tqf8JJ/qB7TeldGl+TT55+uQei9JfYaregDcEAyBZ7akutriFrt6C/wLYIer6OYhleVQvH/ntEhjE/xMmy10A==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.3" + } + }, + "@babel/plugin-transform-member-expression-literals": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.8.3.tgz", + "integrity": "sha512-3Wk2EXhnw+rP+IDkK6BdtPKsUE5IeZ6QOGrPYvw52NwBStw9V1ZVzxgK6fSKSxqUvH9eQPR3tm3cOq79HlsKYA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.3" + } + }, + "@babel/plugin-transform-modules-amd": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.8.3.tgz", + "integrity": "sha512-MadJiU3rLKclzT5kBH4yxdry96odTUwuqrZM+GllFI/VhxfPz+k9MshJM+MwhfkCdxxclSbSBbUGciBngR+kEQ==", + "dev": true, + "requires": { + "@babel/helper-module-transforms": "^7.8.3", + "@babel/helper-plugin-utils": "^7.8.3", + "babel-plugin-dynamic-import-node": "^2.3.0" + }, + "dependencies": { + "babel-plugin-dynamic-import-node": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.0.tgz", + "integrity": "sha512-o6qFkpeQEBxcqt0XYlWzAVxNCSCZdUgcR8IRlhD/8DylxjjO4foPcvTW0GGKa/cVt3rvxZ7o5ippJ+/0nvLhlQ==", + "dev": true, + "requires": { + "object.assign": "^4.1.0" + } + } + } + }, + "@babel/plugin-transform-modules-commonjs": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.8.3.tgz", + "integrity": "sha512-JpdMEfA15HZ/1gNuB9XEDlZM1h/gF/YOH7zaZzQu2xCFRfwc01NXBMHHSTT6hRjlXJJs5x/bfODM3LiCk94Sxg==", + "dev": true, + "requires": { + "@babel/helper-module-transforms": "^7.8.3", + "@babel/helper-plugin-utils": "^7.8.3", + "@babel/helper-simple-access": "^7.8.3", + "babel-plugin-dynamic-import-node": "^2.3.0" + }, + "dependencies": { + "babel-plugin-dynamic-import-node": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.0.tgz", + "integrity": "sha512-o6qFkpeQEBxcqt0XYlWzAVxNCSCZdUgcR8IRlhD/8DylxjjO4foPcvTW0GGKa/cVt3rvxZ7o5ippJ+/0nvLhlQ==", + "dev": true, + "requires": { + "object.assign": "^4.1.0" + } + } + } + }, + "@babel/plugin-transform-modules-systemjs": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.8.3.tgz", + "integrity": "sha512-8cESMCJjmArMYqa9AO5YuMEkE4ds28tMpZcGZB/jl3n0ZzlsxOAi3mC+SKypTfT8gjMupCnd3YiXCkMjj2jfOg==", + "dev": true, + "requires": { + "@babel/helper-hoist-variables": "^7.8.3", + "@babel/helper-module-transforms": "^7.8.3", + "@babel/helper-plugin-utils": "^7.8.3", + "babel-plugin-dynamic-import-node": "^2.3.0" + }, + "dependencies": { + "babel-plugin-dynamic-import-node": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.0.tgz", + "integrity": "sha512-o6qFkpeQEBxcqt0XYlWzAVxNCSCZdUgcR8IRlhD/8DylxjjO4foPcvTW0GGKa/cVt3rvxZ7o5ippJ+/0nvLhlQ==", + "dev": true, + "requires": { + "object.assign": "^4.1.0" + } + } + } + }, + "@babel/plugin-transform-modules-umd": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.8.3.tgz", + "integrity": "sha512-evhTyWhbwbI3/U6dZAnx/ePoV7H6OUG+OjiJFHmhr9FPn0VShjwC2kdxqIuQ/+1P50TMrneGzMeyMTFOjKSnAw==", + "dev": true, + "requires": { + "@babel/helper-module-transforms": "^7.8.3", + "@babel/helper-plugin-utils": "^7.8.3" + } + }, + "@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.8.3.tgz", + "integrity": "sha512-f+tF/8UVPU86TrCb06JoPWIdDpTNSGGcAtaD9mLP0aYGA0OS0j7j7DHJR0GTFrUZPUU6loZhbsVZgTh0N+Qdnw==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.8.3" + } + }, + "@babel/plugin-transform-new-target": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.8.3.tgz", + "integrity": "sha512-QuSGysibQpyxexRyui2vca+Cmbljo8bcRckgzYV4kRIsHpVeyeC3JDO63pY+xFZ6bWOBn7pfKZTqV4o/ix9sFw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.3" + } + }, + "@babel/plugin-transform-object-super": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.8.3.tgz", + "integrity": "sha512-57FXk+gItG/GejofIyLIgBKTas4+pEU47IXKDBWFTxdPd7F80H8zybyAY7UoblVfBhBGs2EKM+bJUu2+iUYPDQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.3", + "@babel/helper-replace-supers": "^7.8.3" + } + }, + "@babel/plugin-transform-parameters": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.8.4.tgz", + "integrity": "sha512-IsS3oTxeTsZlE5KqzTbcC2sV0P9pXdec53SU+Yxv7o/6dvGM5AkTotQKhoSffhNgZ/dftsSiOoxy7evCYJXzVA==", + "dev": true, + "requires": { + "@babel/helper-call-delegate": "^7.8.3", + "@babel/helper-get-function-arity": "^7.8.3", + "@babel/helper-plugin-utils": "^7.8.3" + } + }, + "@babel/plugin-transform-property-literals": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.8.3.tgz", + "integrity": "sha512-uGiiXAZMqEoQhRWMK17VospMZh5sXWg+dlh2soffpkAl96KAm+WZuJfa6lcELotSRmooLqg0MWdH6UUq85nmmg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.3" + } + }, + "@babel/plugin-transform-react-display-name": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.8.3.tgz", + "integrity": "sha512-3Jy/PCw8Fe6uBKtEgz3M82ljt+lTg+xJaM4og+eyu83qLT87ZUSckn0wy7r31jflURWLO83TW6Ylf7lyXj3m5A==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.3" + } + }, + "@babel/plugin-transform-react-jsx": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.8.3.tgz", + "integrity": "sha512-r0h+mUiyL595ikykci+fbwm9YzmuOrUBi0b+FDIKmi3fPQyFokWVEMJnRWHJPPQEjyFJyna9WZC6Viv6UHSv1g==", + "dev": true, + "requires": { + "@babel/helper-builder-react-jsx": "^7.8.3", + "@babel/helper-plugin-utils": "^7.8.3", + "@babel/plugin-syntax-jsx": "^7.8.3" + } + }, + "@babel/plugin-transform-react-jsx-self": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.8.3.tgz", + "integrity": "sha512-01OT7s5oa0XTLf2I8XGsL8+KqV9lx3EZV+jxn/L2LQ97CGKila2YMroTkCEIE0HV/FF7CMSRsIAybopdN9NTdg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.3", + "@babel/plugin-syntax-jsx": "^7.8.3" + } + }, + "@babel/plugin-transform-react-jsx-source": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.8.3.tgz", + "integrity": "sha512-PLMgdMGuVDtRS/SzjNEQYUT8f4z1xb2BAT54vM1X5efkVuYBf5WyGUMbpmARcfq3NaglIwz08UVQK4HHHbC6ag==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.3", + "@babel/plugin-syntax-jsx": "^7.8.3" + } + }, + "@babel/plugin-transform-regenerator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.8.3.tgz", + "integrity": "sha512-qt/kcur/FxrQrzFR432FGZznkVAjiyFtCOANjkAKwCbt465L6ZCiUQh2oMYGU3Wo8LRFJxNDFwWn106S5wVUNA==", + "dev": true, + "requires": { + "regenerator-transform": "^0.14.0" + } + }, + "@babel/plugin-transform-reserved-words": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.8.3.tgz", + "integrity": "sha512-mwMxcycN3omKFDjDQUl+8zyMsBfjRFr0Zn/64I41pmjv4NJuqcYlEtezwYtw9TFd9WR1vN5kiM+O0gMZzO6L0A==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.3" + } + }, + "@babel/plugin-transform-shorthand-properties": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.8.3.tgz", + "integrity": "sha512-I9DI6Odg0JJwxCHzbzW08ggMdCezoWcuQRz3ptdudgwaHxTjxw5HgdFJmZIkIMlRymL6YiZcped4TTCB0JcC8w==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.3" + } + }, + "@babel/plugin-transform-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.8.3.tgz", + "integrity": "sha512-CkuTU9mbmAoFOI1tklFWYYbzX5qCIZVXPVy0jpXgGwkplCndQAa58s2jr66fTeQnA64bDox0HL4U56CFYoyC7g==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.3" + } + }, + "@babel/plugin-transform-sticky-regex": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.8.3.tgz", + "integrity": "sha512-9Spq0vGCD5Bb4Z/ZXXSK5wbbLFMG085qd2vhL1JYu1WcQ5bXqZBAYRzU1d+p79GcHs2szYv5pVQCX13QgldaWw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.3", + "@babel/helper-regex": "^7.8.3" + } + }, + "@babel/plugin-transform-template-literals": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.8.3.tgz", + "integrity": "sha512-820QBtykIQOLFT8NZOcTRJ1UNuztIELe4p9DCgvj4NK+PwluSJ49we7s9FB1HIGNIYT7wFUJ0ar2QpCDj0escQ==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.8.3", + "@babel/helper-plugin-utils": "^7.8.3" + } + }, + "@babel/plugin-transform-typeof-symbol": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.8.4.tgz", + "integrity": "sha512-2QKyfjGdvuNfHsb7qnBBlKclbD4CfshH2KvDabiijLMGXPHJXGxtDzwIF7bQP+T0ysw8fYTtxPafgfs/c1Lrqg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.3" + } + }, + "@babel/plugin-transform-unicode-regex": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.8.3.tgz", + "integrity": "sha512-+ufgJjYdmWfSQ+6NS9VGUR2ns8cjJjYbrbi11mZBTaWm+Fui/ncTLFF28Ei1okavY+xkojGr1eJxNsWYeA5aZw==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.8.3", + "@babel/helper-plugin-utils": "^7.8.3" + } + }, + "@babel/preset-env": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.8.4.tgz", + "integrity": "sha512-HihCgpr45AnSOHRbS5cWNTINs0TwaR8BS8xIIH+QwiW8cKL0llV91njQMpeMReEPVs+1Ao0x3RLEBLtt1hOq4w==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.8.4", + "@babel/helper-compilation-targets": "^7.8.4", + "@babel/helper-module-imports": "^7.8.3", + "@babel/helper-plugin-utils": "^7.8.3", + "@babel/plugin-proposal-async-generator-functions": "^7.8.3", + "@babel/plugin-proposal-dynamic-import": "^7.8.3", + "@babel/plugin-proposal-json-strings": "^7.8.3", + "@babel/plugin-proposal-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-proposal-object-rest-spread": "^7.8.3", + "@babel/plugin-proposal-optional-catch-binding": "^7.8.3", + "@babel/plugin-proposal-optional-chaining": "^7.8.3", + "@babel/plugin-proposal-unicode-property-regex": "^7.8.3", + "@babel/plugin-syntax-async-generators": "^7.8.0", + "@babel/plugin-syntax-dynamic-import": "^7.8.0", + "@babel/plugin-syntax-json-strings": "^7.8.0", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.0", + "@babel/plugin-syntax-object-rest-spread": "^7.8.0", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.0", + "@babel/plugin-syntax-optional-chaining": "^7.8.0", + "@babel/plugin-syntax-top-level-await": "^7.8.3", + "@babel/plugin-transform-arrow-functions": "^7.8.3", + "@babel/plugin-transform-async-to-generator": "^7.8.3", + "@babel/plugin-transform-block-scoped-functions": "^7.8.3", + "@babel/plugin-transform-block-scoping": "^7.8.3", + "@babel/plugin-transform-classes": "^7.8.3", + "@babel/plugin-transform-computed-properties": "^7.8.3", + "@babel/plugin-transform-destructuring": "^7.8.3", + "@babel/plugin-transform-dotall-regex": "^7.8.3", + "@babel/plugin-transform-duplicate-keys": "^7.8.3", + "@babel/plugin-transform-exponentiation-operator": "^7.8.3", + "@babel/plugin-transform-for-of": "^7.8.4", + "@babel/plugin-transform-function-name": "^7.8.3", + "@babel/plugin-transform-literals": "^7.8.3", + "@babel/plugin-transform-member-expression-literals": "^7.8.3", + "@babel/plugin-transform-modules-amd": "^7.8.3", + "@babel/plugin-transform-modules-commonjs": "^7.8.3", + "@babel/plugin-transform-modules-systemjs": "^7.8.3", + "@babel/plugin-transform-modules-umd": "^7.8.3", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.8.3", + "@babel/plugin-transform-new-target": "^7.8.3", + "@babel/plugin-transform-object-super": "^7.8.3", + "@babel/plugin-transform-parameters": "^7.8.4", + "@babel/plugin-transform-property-literals": "^7.8.3", + "@babel/plugin-transform-regenerator": "^7.8.3", + "@babel/plugin-transform-reserved-words": "^7.8.3", + "@babel/plugin-transform-shorthand-properties": "^7.8.3", + "@babel/plugin-transform-spread": "^7.8.3", + "@babel/plugin-transform-sticky-regex": "^7.8.3", + "@babel/plugin-transform-template-literals": "^7.8.3", + "@babel/plugin-transform-typeof-symbol": "^7.8.4", + "@babel/plugin-transform-unicode-regex": "^7.8.3", + "@babel/types": "^7.8.3", + "browserslist": "^4.8.5", + "core-js-compat": "^3.6.2", + "invariant": "^2.2.2", + "levenary": "^1.1.1", + "semver": "^5.5.0" + } + }, + "@babel/preset-react": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.8.3.tgz", + "integrity": "sha512-9hx0CwZg92jGb7iHYQVgi0tOEHP/kM60CtWJQnmbATSPIQQ2xYzfoCI3EdqAhFBeeJwYMdWQuDUHMsuDbH9hyQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.3", + "@babel/plugin-transform-react-display-name": "^7.8.3", + "@babel/plugin-transform-react-jsx": "^7.8.3", + "@babel/plugin-transform-react-jsx-self": "^7.8.3", + "@babel/plugin-transform-react-jsx-source": "^7.8.3" + } + }, + "@babel/register": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/register/-/register-7.8.3.tgz", + "integrity": "sha512-t7UqebaWwo9nXWClIPLPloa5pN33A2leVs8Hf0e9g9YwUP8/H9NeR7DJU+4CXo23QtjChQv5a3DjEtT83ih1rg==", + "dev": true, + "requires": { + "find-cache-dir": "^2.0.0", + "lodash": "^4.17.13", + "make-dir": "^2.1.0", + "pirates": "^4.0.0", + "source-map-support": "^0.5.16" + } + }, + "@babel/runtime": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.8.4.tgz", + "integrity": "sha512-neAp3zt80trRVBI1x0azq6c57aNBqYZH8KhMm3TaB7wEI5Q4A2SHfBHE8w9gOhI/lrqxtEbXZgQIrHP+wvSGwQ==", + "dev": true, + "requires": { + "regenerator-runtime": "^0.13.2" + }, + "dependencies": { + "regenerator-runtime": { + "version": "0.13.3", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz", + "integrity": "sha512-naKIZz2GQ8JWh///G7L3X6LaQUAMp2lvb1rvwwsURe/VXwD6VMfr+/1NuNw3ag8v2kY1aQ/go5SNn79O9JU7yw==", + "dev": true + } + } + }, + "@babel/runtime-corejs3": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.8.4.tgz", + "integrity": "sha512-+wpLqy5+fbQhvbllvlJEVRIpYj+COUWnnsm+I4jZlA8Lo7/MJmBhGTCHyk1/RWfOqBRJ2MbadddG6QltTKTlrg==", + "dev": true, + "requires": { + "core-js-pure": "^3.0.0", + "regenerator-runtime": "^0.13.2" + }, + "dependencies": { + "regenerator-runtime": { + "version": "0.13.3", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz", + "integrity": "sha512-naKIZz2GQ8JWh///G7L3X6LaQUAMp2lvb1rvwwsURe/VXwD6VMfr+/1NuNw3ag8v2kY1aQ/go5SNn79O9JU7yw==", + "dev": true + } + } + }, + "@babel/template": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.8.3.tgz", + "integrity": "sha512-04m87AcQgAFdvuoyiQ2kgELr2tV8B4fP/xJAVUL3Yb3bkNdMedD3d0rlSQr3PegP0cms3eHjl1F7PWlvWbU8FQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.8.3", + "@babel/parser": "^7.8.3", + "@babel/types": "^7.8.3" + } + }, + "@babel/traverse": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.8.4.tgz", + "integrity": "sha512-NGLJPZwnVEyBPLI+bl9y9aSnxMhsKz42so7ApAv9D+b4vAFPpY013FTS9LdKxcABoIYFU52HcYga1pPlx454mg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.8.3", + "@babel/generator": "^7.8.4", + "@babel/helper-function-name": "^7.8.3", + "@babel/helper-split-export-declaration": "^7.8.3", + "@babel/parser": "^7.8.4", + "@babel/types": "^7.8.3", + "debug": "^4.1.0", + "globals": "^11.1.0", + "lodash": "^4.17.13" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "@babel/types": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.3.tgz", + "integrity": "sha512-jBD+G8+LWpMBBWvVcdr4QysjUE4mU/syrhN17o1u3gx0/WzJB1kwiVZAXRtWbsIPOwW8pF/YJV5+nmetPzepXg==", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.17.13", + "to-fast-properties": "^2.0.0" + }, + "dependencies": { + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", + "dev": true + } + } + }, + "@tweenjs/tween.js": { + "version": "16.11.0", + "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-16.11.0.tgz", + "integrity": "sha1-bnqKPWx4oFfs1WBQh5MEBtTgWAA=", + "dev": true + }, + "@types/babel-types": { + "version": "7.0.7", + "resolved": "https://registry.npmjs.org/@types/babel-types/-/babel-types-7.0.7.tgz", + "integrity": "sha512-dBtBbrc+qTHy1WdfHYjBwRln4+LWqASWakLHsWHR2NWHIFkv4W3O070IGoGLEBrJBvct3r0L1BUPuvURi7kYUQ==" + }, + "@types/babylon": { + "version": "6.16.5", + "resolved": "https://registry.npmjs.org/@types/babylon/-/babylon-6.16.5.tgz", + "integrity": "sha512-xH2e58elpj1X4ynnKp9qSnWlsRTIs6n3tgLGNfwAGHwePw0mulHQllV34n0T25uYSu1k0hRKkWXF890B1yS47w==", + "requires": { + "@types/babel-types": "*" + } + }, + "@types/bytebuffer": { + "version": "5.0.40", + "resolved": "https://registry.npmjs.org/@types/bytebuffer/-/bytebuffer-5.0.40.tgz", + "integrity": "sha512-h48dyzZrPMz25K6Q4+NCwWaxwXany2FhQg/ErOcdZS1ZpsaDnDMZg8JYLMTGz7uvXKrcKGJUZJlZObyfgdaN9g==", + "requires": { + "@types/long": "*", + "@types/node": "*" + } + }, + "@types/jquery": { + "version": "2.0.54", + "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-2.0.54.tgz", + "integrity": "sha512-D/PomKwNkDfSKD13DEVQT/pq2TUjN54c6uB341fEZanIzkjfGe7UaFuuaLZbpEiS5j7Wk2MUHAZqZIoECw29lg==", + "dev": true + }, + "@types/long": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz", + "integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==" + }, + "@types/node": { + "version": "13.7.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-13.7.0.tgz", + "integrity": "sha512-GnZbirvmqZUzMgkFn70c74OQpTTUcCzlhQliTzYjQMqg+hVKcDnxdL19Ne3UdYzdMA/+W3eb646FWn/ZaT1NfQ==" + }, + "@types/normalize-package-data": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz", + "integrity": "sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA==", + "dev": true + }, + "@webassemblyjs/ast": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.8.5.tgz", + "integrity": "sha512-aJMfngIZ65+t71C3y2nBBg5FFG0Okt9m0XEgWZ7Ywgn1oMAT8cNwx00Uv1cQyHtidq0Xn94R4TAywO+LCQ+ZAQ==", + "dev": true, + "requires": { + "@webassemblyjs/helper-module-context": "1.8.5", + "@webassemblyjs/helper-wasm-bytecode": "1.8.5", + "@webassemblyjs/wast-parser": "1.8.5" + } + }, + "@webassemblyjs/floating-point-hex-parser": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.8.5.tgz", + "integrity": "sha512-9p+79WHru1oqBh9ewP9zW95E3XAo+90oth7S5Re3eQnECGq59ly1Ri5tsIipKGpiStHsUYmY3zMLqtk3gTcOtQ==", + "dev": true + }, + "@webassemblyjs/helper-api-error": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.8.5.tgz", + "integrity": "sha512-Za/tnzsvnqdaSPOUXHyKJ2XI7PDX64kWtURyGiJJZKVEdFOsdKUCPTNEVFZq3zJ2R0G5wc2PZ5gvdTRFgm81zA==", + "dev": true + }, + "@webassemblyjs/helper-buffer": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.8.5.tgz", + "integrity": "sha512-Ri2R8nOS0U6G49Q86goFIPNgjyl6+oE1abW1pS84BuhP1Qcr5JqMwRFT3Ah3ADDDYGEgGs1iyb1DGX+kAi/c/Q==", + "dev": true + }, + "@webassemblyjs/helper-code-frame": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.8.5.tgz", + "integrity": "sha512-VQAadSubZIhNpH46IR3yWO4kZZjMxN1opDrzePLdVKAZ+DFjkGD/rf4v1jap744uPVU6yjL/smZbRIIJTOUnKQ==", + "dev": true, + "requires": { + "@webassemblyjs/wast-printer": "1.8.5" + } + }, + "@webassemblyjs/helper-fsm": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-fsm/-/helper-fsm-1.8.5.tgz", + "integrity": "sha512-kRuX/saORcg8se/ft6Q2UbRpZwP4y7YrWsLXPbbmtepKr22i8Z4O3V5QE9DbZK908dh5Xya4Un57SDIKwB9eow==", + "dev": true + }, + "@webassemblyjs/helper-module-context": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-module-context/-/helper-module-context-1.8.5.tgz", + "integrity": "sha512-/O1B236mN7UNEU4t9X7Pj38i4VoU8CcMHyy3l2cV/kIF4U5KoHXDVqcDuOs1ltkac90IM4vZdHc52t1x8Yfs3g==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.8.5", + "mamacro": "^0.0.3" + } + }, + "@webassemblyjs/helper-wasm-bytecode": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.8.5.tgz", + "integrity": "sha512-Cu4YMYG3Ddl72CbmpjU/wbP6SACcOPVbHN1dI4VJNJVgFwaKf1ppeFJrwydOG3NDHxVGuCfPlLZNyEdIYlQ6QQ==", + "dev": true + }, + "@webassemblyjs/helper-wasm-section": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.8.5.tgz", + "integrity": "sha512-VV083zwR+VTrIWWtgIUpqfvVdK4ff38loRmrdDBgBT8ADXYsEZ5mPQ4Nde90N3UYatHdYoDIFb7oHzMncI02tA==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.8.5", + "@webassemblyjs/helper-buffer": "1.8.5", + "@webassemblyjs/helper-wasm-bytecode": "1.8.5", + "@webassemblyjs/wasm-gen": "1.8.5" + } + }, + "@webassemblyjs/ieee754": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.8.5.tgz", + "integrity": "sha512-aaCvQYrvKbY/n6wKHb/ylAJr27GglahUO89CcGXMItrOBqRarUMxWLJgxm9PJNuKULwN5n1csT9bYoMeZOGF3g==", + "dev": true, + "requires": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "@webassemblyjs/leb128": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.8.5.tgz", + "integrity": "sha512-plYUuUwleLIziknvlP8VpTgO4kqNaH57Y3JnNa6DLpu/sGcP6hbVdfdX5aHAV716pQBKrfuU26BJK29qY37J7A==", + "dev": true, + "requires": { + "@xtuc/long": "4.2.2" + } + }, + "@webassemblyjs/utf8": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.8.5.tgz", + "integrity": "sha512-U7zgftmQriw37tfD934UNInokz6yTmn29inT2cAetAsaU9YeVCveWEwhKL1Mg4yS7q//NGdzy79nlXh3bT8Kjw==", + "dev": true + }, + "@webassemblyjs/wasm-edit": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.8.5.tgz", + "integrity": "sha512-A41EMy8MWw5yvqj7MQzkDjU29K7UJq1VrX2vWLzfpRHt3ISftOXqrtojn7nlPsZ9Ijhp5NwuODuycSvfAO/26Q==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.8.5", + "@webassemblyjs/helper-buffer": "1.8.5", + "@webassemblyjs/helper-wasm-bytecode": "1.8.5", + "@webassemblyjs/helper-wasm-section": "1.8.5", + "@webassemblyjs/wasm-gen": "1.8.5", + "@webassemblyjs/wasm-opt": "1.8.5", + "@webassemblyjs/wasm-parser": "1.8.5", + "@webassemblyjs/wast-printer": "1.8.5" + } + }, + "@webassemblyjs/wasm-gen": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.8.5.tgz", + "integrity": "sha512-BCZBT0LURC0CXDzj5FXSc2FPTsxwp3nWcqXQdOZE4U7h7i8FqtFK5Egia6f9raQLpEKT1VL7zr4r3+QX6zArWg==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.8.5", + "@webassemblyjs/helper-wasm-bytecode": "1.8.5", + "@webassemblyjs/ieee754": "1.8.5", + "@webassemblyjs/leb128": "1.8.5", + "@webassemblyjs/utf8": "1.8.5" + } + }, + "@webassemblyjs/wasm-opt": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.8.5.tgz", + "integrity": "sha512-HKo2mO/Uh9A6ojzu7cjslGaHaUU14LdLbGEKqTR7PBKwT6LdPtLLh9fPY33rmr5wcOMrsWDbbdCHq4hQUdd37Q==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.8.5", + "@webassemblyjs/helper-buffer": "1.8.5", + "@webassemblyjs/wasm-gen": "1.8.5", + "@webassemblyjs/wasm-parser": "1.8.5" + } + }, + "@webassemblyjs/wasm-parser": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.8.5.tgz", + "integrity": "sha512-pi0SYE9T6tfcMkthwcgCpL0cM9nRYr6/6fjgDtL6q/ZqKHdMWvxitRi5JcZ7RI4SNJJYnYNaWy5UUrHQy998lw==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.8.5", + "@webassemblyjs/helper-api-error": "1.8.5", + "@webassemblyjs/helper-wasm-bytecode": "1.8.5", + "@webassemblyjs/ieee754": "1.8.5", + "@webassemblyjs/leb128": "1.8.5", + "@webassemblyjs/utf8": "1.8.5" + } + }, + "@webassemblyjs/wast-parser": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-parser/-/wast-parser-1.8.5.tgz", + "integrity": "sha512-daXC1FyKWHF1i11obK086QRlsMsY4+tIOKgBqI1lxAnkp9xe9YMcgOxm9kLe+ttjs5aWV2KKE1TWJCN57/Btsg==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.8.5", + "@webassemblyjs/floating-point-hex-parser": "1.8.5", + "@webassemblyjs/helper-api-error": "1.8.5", + "@webassemblyjs/helper-code-frame": "1.8.5", + "@webassemblyjs/helper-fsm": "1.8.5", + "@xtuc/long": "4.2.2" + } + }, + "@webassemblyjs/wast-printer": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.8.5.tgz", + "integrity": "sha512-w0U0pD4EhlnvRyeJzBqaVSJAo9w/ce7/WPogeXLzGkO6hzhr4GnQIZ4W4uUt5b9ooAaXPtnXlj0gzsXEOUNYMg==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.8.5", + "@webassemblyjs/wast-parser": "1.8.5", + "@xtuc/long": "4.2.2" + } + }, + "@webpack-contrib/config-loader": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@webpack-contrib/config-loader/-/config-loader-1.2.1.tgz", + "integrity": "sha512-C7XsS6bXft0aRlyt7YCLg+fm97Mb3tWd+i5fVVlEl0NW5HKy8LoXVKj3mB7ECcEHNEEdHhgzg8gxP+Or8cMj8Q==", + "dev": true, + "requires": { + "@webpack-contrib/schema-utils": "^1.0.0-beta.0", + "chalk": "^2.1.0", + "cosmiconfig": "^5.0.2", + "is-plain-obj": "^1.1.0", + "loud-rejection": "^1.6.0", + "merge-options": "^1.0.1", + "minimist": "^1.2.0", + "resolve": "^1.6.0", + "webpack-log": "^1.1.2" + } + }, + "@webpack-contrib/schema-utils": { + "version": "1.0.0-beta.0", + "resolved": "https://registry.npmjs.org/@webpack-contrib/schema-utils/-/schema-utils-1.0.0-beta.0.tgz", + "integrity": "sha512-LonryJP+FxQQHsjGBi6W786TQB1Oym+agTpY0c+Kj8alnIw+DLUJb6SI8Y1GHGhLCH1yPRrucjObUmxNICQ1pg==", + "dev": true, + "requires": { + "ajv": "^6.1.0", + "ajv-keywords": "^3.1.0", + "chalk": "^2.3.2", + "strip-ansi": "^4.0.0", + "text-table": "^0.2.0", + "webpack-log": "^1.1.2" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true + }, + "@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true + }, + "abab": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.3.tgz", + "integrity": "sha512-tsFzPpcttalNjFBCFMqsKYQcWxxen1pgJR56by//QwvJc4/OUS3kPOOttx2tSIfjsylB0pYu7f5D3K1RCxUnUg==", + "dev": true + }, + "abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true + }, + "accepts": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", + "requires": { + "mime-types": "~2.1.24", + "negotiator": "0.6.2" + } + }, + "acorn": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz", + "integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo=" + }, + "acorn-globals": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-3.1.0.tgz", + "integrity": "sha1-/YJw9x+7SZawBPqIDuXUZXOnMb8=", + "requires": { + "acorn": "^4.0.4" + }, + "dependencies": { + "acorn": { + "version": "4.0.13", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-4.0.13.tgz", + "integrity": "sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c=" + } + } + }, + "acorn-jsx": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.1.0.tgz", + "integrity": "sha512-tMUqwBWfLFbJbizRmEcWSLw6HnFzfdJs2sOJEOwwtVPMoH/0Ay+E703oZz78VSXZiiDcZrQ5XKjPIUQixhmgVw==", + "dev": true + }, + "acorn-walk": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.2.0.tgz", + "integrity": "sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA==", + "dev": true + }, + "airbnb-prop-types": { + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/airbnb-prop-types/-/airbnb-prop-types-2.15.0.tgz", + "integrity": "sha512-jUh2/hfKsRjNFC4XONQrxo/n/3GG4Tn6Hl0WlFQN5PY9OMC9loSCoAYKnZsWaP8wEfd5xcrPloK0Zg6iS1xwVA==", + "dev": true, + "requires": { + "array.prototype.find": "^2.1.0", + "function.prototype.name": "^1.1.1", + "has": "^1.0.3", + "is-regex": "^1.0.4", + "object-is": "^1.0.1", + "object.assign": "^4.1.0", + "object.entries": "^1.1.0", + "prop-types": "^15.7.2", + "prop-types-exact": "^1.2.0", + "react-is": "^16.9.0" + } + }, + "ajv": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.11.0.tgz", + "integrity": "sha512-nCprB/0syFYy9fVYU1ox1l2KN8S9I+tziH8D4zdZuLT3N6RMlGSGt5FSTpAiHB/Whv8Qs1cWHma1aMKZyaHRKA==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ajv-errors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.1.tgz", + "integrity": "sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==", + "dev": true + }, + "ajv-keywords": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.4.1.tgz", + "integrity": "sha512-RO1ibKvd27e6FEShVFfPALuHI3WjSVNeK5FIsmme/LYRNxjKuNj+Dt7bucLa6NdSv3JcVTyMlm9kGR84z1XpaQ==", + "dev": true + }, + "align-text": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", + "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", + "requires": { + "kind-of": "^3.0.2", + "longest": "^1.0.1", + "repeat-string": "^1.5.2" + } + }, + "ansi-align": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-2.0.0.tgz", + "integrity": "sha1-w2rsy6VjuJzrVW82kPCx2eNUf38=", + "dev": true, + "requires": { + "string-width": "^2.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "ansi-escapes": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", + "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==", + "dev": true + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + }, + "ansi-styles": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-1.0.0.tgz", + "integrity": "sha1-yxAt8cVvUSPquLZ817mAJ6AnkXg=" + }, + "anymatch": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", + "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", + "dev": true, + "requires": { + "micromatch": "^3.1.4", + "normalize-path": "^2.1.1" + }, + "dependencies": { + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "dev": true, + "requires": { + "remove-trailing-separator": "^1.0.1" + } + } + } + }, + "append-transform": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-1.0.0.tgz", + "integrity": "sha512-P009oYkeHyU742iSZJzZZywj4QRJdnTWffaKuJQLablCZ1uz6/cW4yaRgcDaoQ+uwOxxnt0gRUcwfsNP2ri0gw==", + "dev": true, + "requires": { + "default-require-extensions": "^2.0.0" + } + }, + "aproba": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", + "dev": true + }, + "archy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", + "integrity": "sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=", + "dev": true + }, + "are-we-there-yet": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", + "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", + "dev": true, + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + } + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "aria-query": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-3.0.0.tgz", + "integrity": "sha1-ZbP8wcoRVajJrmTW7uKX8V1RM8w=", + "dev": true, + "requires": { + "ast-types-flow": "0.0.7", + "commander": "^2.11.0" + }, + "dependencies": { + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + } + } + }, + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", + "dev": true + }, + "arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", + "dev": true + }, + "arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", + "dev": true + }, + "array-differ": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-differ/-/array-differ-2.1.0.tgz", + "integrity": "sha512-KbUpJgx909ZscOc/7CLATBFam7P1Z1QRQInvgT0UztM9Q72aGKCunKASAl7WNW0tnPmPyEMeMhdsfWhfmW037w==", + "dev": true + }, + "array-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-equal/-/array-equal-1.0.0.tgz", + "integrity": "sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM=", + "dev": true + }, + "array-filter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-filter/-/array-filter-1.0.0.tgz", + "integrity": "sha1-uveeYubvTCpMC4MSMtr/7CUfnYM=", + "dev": true + }, + "array-find-index": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", + "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=", + "dev": true + }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" + }, + "array-includes": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.1.tgz", + "integrity": "sha512-c2VXaCHl7zPsvpkFsw4nxvFie4fh1ur9bpcgsVkIjqn0H/Xwdg+7fv3n2r/isyS8EBj5b06M9kHyZuIr4El6WQ==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0", + "is-string": "^1.0.5" + } + }, + "array-union": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", + "dev": true, + "requires": { + "array-uniq": "^1.0.1" + } + }, + "array-uniq": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", + "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=", + "dev": true + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", + "dev": true + }, + "array.prototype.find": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array.prototype.find/-/array.prototype.find-2.1.0.tgz", + "integrity": "sha512-Wn41+K1yuO5p7wRZDl7890c3xvv5UBrfVXTVIe28rSQb6LS0fZMDrQB6PAcxQFRFy6vJTLDc3A2+3CjQdzVKRg==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.13.0" + } + }, + "array.prototype.flat": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.2.3.tgz", + "integrity": "sha512-gBlRZV0VSmfPIeWfuuy56XZMvbVfbEUnOXUvt3F/eUUUSyzlgLxhEX4YAEpxNAogRGehPSnfXyPtYyKAhkzQhQ==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1" + } + }, + "arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", + "dev": true + }, + "asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=" + }, + "ascli": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ascli/-/ascli-1.0.1.tgz", + "integrity": "sha1-vPpZdKYvGOgcq660lzKrSoj5Brw=", + "requires": { + "colour": "~0.7.1", + "optjs": "~3.2.2" + } + }, + "asn1": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "dev": true, + "requires": { + "safer-buffer": "~2.1.0" + } + }, + "asn1.js": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", + "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==", + "dev": true, + "requires": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, + "assert": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/assert/-/assert-1.5.0.tgz", + "integrity": "sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA==", + "dev": true, + "requires": { + "object-assign": "^4.1.1", + "util": "0.10.3" + }, + "dependencies": { + "inherits": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", + "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=", + "dev": true + }, + "util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", + "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", + "dev": true, + "requires": { + "inherits": "2.0.1" + } + } + } + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true + }, + "assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true + }, + "assets-webpack-plugin": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/assets-webpack-plugin/-/assets-webpack-plugin-4.0.0.tgz", + "integrity": "sha512-0Mhe40xK7MkbQGp3D3zrRXNB27Y4MTYlkJyXlPwN8vFgUawtuLS/2Yip7un0V+4yxPh9RsKKbkkAmatoep0qZw==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "escape-string-regexp": "^1.0.3", + "lodash.assign": "^4.2.0", + "lodash.merge": "^4.6.1", + "mkdirp": "^0.5.1" + }, + "dependencies": { + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + } + } + }, + "assign-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", + "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", + "dev": true + }, + "ast-types-flow": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz", + "integrity": "sha1-9wtzXGvKGlycItmCw+Oef+ujva0=", + "dev": true + }, + "astral-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", + "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", + "dev": true + }, + "async": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", + "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==", + "requires": { + "lodash": "^4.17.14" + } + }, + "async-each": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz", + "integrity": "sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==", + "dev": true + }, + "async-limiter": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", + "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==", + "dev": true + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "dev": true + }, + "atob": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", + "dev": true + }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", + "dev": true + }, + "aws4": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.9.1.tgz", + "integrity": "sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug==", + "dev": true + }, + "axios": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.18.1.tgz", + "integrity": "sha512-0BfJq4NSfQXd+SkFdrvFbG7addhYSBA2mQwISr46pD6E5iqkWg02RAs8vyTT/j0RTnoYmeXauBuSv1qKwR179g==", + "requires": { + "follow-redirects": "1.5.10", + "is-buffer": "^2.0.2" + } + }, + "axios-mock-adapter": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/axios-mock-adapter/-/axios-mock-adapter-1.17.0.tgz", + "integrity": "sha512-q3efmwJUOO4g+wsLNSk9Ps1UlJoF3fQ3FSEe4uEEhkRtu7SoiAVPj8R3Hc/WP55MBTVFzaDP9QkdJhdVhP8A1Q==", + "dev": true, + "requires": { + "deep-equal": "^1.0.1" + } + }, + "axobject-query": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.1.1.tgz", + "integrity": "sha512-lF98xa/yvy6j3fBHAgQXIYl+J4eZadOSqsPojemUqClzNbBV38wWGpUbQbVEyf4eUF5yF7eHmGgGA2JiHyjeqw==", + "dev": true, + "requires": { + "@babel/runtime": "^7.7.4", + "@babel/runtime-corejs3": "^7.7.4" + } + }, + "babel-eslint": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-9.0.0.tgz", + "integrity": "sha512-itv1MwE3TMbY0QtNfeL7wzak1mV47Uy+n6HtSOO4Xd7rvmO+tsGQSgyOEEgo6Y2vHZKZphaoelNeSVj4vkLA1g==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "@babel/parser": "^7.0.0", + "@babel/traverse": "^7.0.0", + "@babel/types": "^7.0.0", + "eslint-scope": "3.7.1", + "eslint-visitor-keys": "^1.0.0" + } + }, + "babel-loader": { + "version": "8.0.6", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.0.6.tgz", + "integrity": "sha512-4BmWKtBOBm13uoUwd08UwjZlaw3O9GWf456R9j+5YykFZ6LUIjIKLc0zEZf+hauxPOJs96C8k6FvYD09vWzhYw==", + "dev": true, + "requires": { + "find-cache-dir": "^2.0.0", + "loader-utils": "^1.0.2", + "mkdirp": "^0.5.1", + "pify": "^4.0.1" + } + }, + "babel-plugin-dynamic-import-node": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-1.2.0.tgz", + "integrity": "sha512-yeDwKaLgGdTpXL7RgGt5r6T4LmnTza/hUn5Ul8uZSGGMtEjYo13Nxai7SQaGCTEzUtg9Zq9qJn0EjEr7SeSlTQ==", + "dev": true, + "requires": { + "babel-plugin-syntax-dynamic-import": "^6.18.0" + } + }, + "babel-plugin-lodash": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/babel-plugin-lodash/-/babel-plugin-lodash-3.3.4.tgz", + "integrity": "sha512-yDZLjK7TCkWl1gpBeBGmuaDIFhZKmkoL+Cu2MUUjv5VxUZx/z7tBGBCBcQs5RI1Bkz5LLmNdjx7paOyQtMovyg==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.0.0-beta.49", + "@babel/types": "^7.0.0-beta.49", + "glob": "^7.1.1", + "lodash": "^4.17.10", + "require-package-name": "^2.0.1" + } + }, + "babel-plugin-syntax-dynamic-import": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-dynamic-import/-/babel-plugin-syntax-dynamic-import-6.18.0.tgz", + "integrity": "sha1-jWomIpyDdFqZgqRBBRVyyqF5sdo=", + "dev": true + }, + "babel-runtime": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", + "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", + "requires": { + "core-js": "^2.4.0", + "regenerator-runtime": "^0.11.0" + } + }, + "babel-types": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", + "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", + "requires": { + "babel-runtime": "^6.26.0", + "esutils": "^2.0.2", + "lodash": "^4.17.4", + "to-fast-properties": "^1.0.3" + } + }, + "babylon": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz", + "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==" + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + }, + "base": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", + "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", + "dev": true, + "requires": { + "cache-base": "^1.0.1", + "class-utils": "^0.3.5", + "component-emitter": "^1.2.1", + "define-property": "^1.0.0", + "isobject": "^3.0.1", + "mixin-deep": "^1.2.0", + "pascalcase": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + }, + "kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true + } + } + }, + "base64-js": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", + "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==", + "dev": true + }, + "bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "dev": true, + "requires": { + "tweetnacl": "^0.14.3" + } + }, + "bfj": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/bfj/-/bfj-6.1.2.tgz", + "integrity": "sha512-BmBJa4Lip6BPRINSZ0BPEIfB1wUY/9rwbwvIHQA1KjX9om29B6id0wnWXq7m3bn5JrUVjeOTnVuhPT1FiHwPGw==", + "dev": true, + "requires": { + "bluebird": "^3.5.5", + "check-types": "^8.0.3", + "hoopy": "^0.1.4", + "tryer": "^1.0.1" + } + }, + "big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true + }, + "binary-extensions": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", + "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==", + "dev": true + }, + "bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dev": true, + "optional": true, + "requires": { + "file-uri-to-path": "1.0.0" + } + }, + "bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true + }, + "bn.js": { + "version": "4.11.8", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", + "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==", + "dev": true + }, + "body-parser": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", + "requires": { + "bytes": "3.1.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "on-finished": "~2.3.0", + "qs": "6.7.0", + "raw-body": "2.4.0", + "type-is": "~1.6.17" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" + } + } + }, + "boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=", + "dev": true + }, + "boxen": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-1.3.0.tgz", + "integrity": "sha512-TNPjfTr432qx7yOjQyaXm3dSR0MH9vXp7eT1BFSl/C51g+EFnOR9hTg1IreahGBmDNCehscshe45f+C1TBZbLw==", + "dev": true, + "requires": { + "ansi-align": "^2.0.0", + "camelcase": "^4.0.0", + "chalk": "^2.0.1", + "cli-boxes": "^1.0.0", + "string-width": "^2.0.0", + "term-size": "^1.2.0", + "widest-line": "^2.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "camelcase": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=", + "dev": true + }, + "browser-process-hrtime": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-0.1.3.tgz", + "integrity": "sha512-bRFnI4NnjO6cnyLmOV/7PVoDEMJChlcfN0z4s1YMBY989/SvlfMI1lgCnkFUs53e9gQF+w7qu7XdllSTiSl8Aw==", + "dev": true + }, + "browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, + "browserify-aes": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", + "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", + "dev": true, + "requires": { + "buffer-xor": "^1.0.3", + "cipher-base": "^1.0.0", + "create-hash": "^1.1.0", + "evp_bytestokey": "^1.0.3", + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "browserify-cipher": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", + "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", + "dev": true, + "requires": { + "browserify-aes": "^1.0.4", + "browserify-des": "^1.0.0", + "evp_bytestokey": "^1.0.0" + } + }, + "browserify-des": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz", + "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", + "dev": true, + "requires": { + "cipher-base": "^1.0.1", + "des.js": "^1.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "browserify-rsa": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", + "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=", + "dev": true, + "requires": { + "bn.js": "^4.1.0", + "randombytes": "^2.0.1" + } + }, + "browserify-sign": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.0.4.tgz", + "integrity": "sha1-qk62jl17ZYuqa/alfmMMvXqT0pg=", + "dev": true, + "requires": { + "bn.js": "^4.1.1", + "browserify-rsa": "^4.0.0", + "create-hash": "^1.1.0", + "create-hmac": "^1.1.2", + "elliptic": "^6.0.0", + "inherits": "^2.0.1", + "parse-asn1": "^5.0.0" + } + }, + "browserify-zlib": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", + "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", + "dev": true, + "requires": { + "pako": "~1.0.5" + } + }, + "browserslist": { + "version": "4.8.6", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.8.6.tgz", + "integrity": "sha512-ZHao85gf0eZ0ESxLfCp73GG9O/VTytYDIkIiZDlURppLTI9wErSM/5yAKEq6rcUdxBLjMELmrYUJGg5sxGKMHg==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30001023", + "electron-to-chromium": "^1.3.341", + "node-releases": "^1.1.47" + } + }, + "buffer": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", + "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", + "dev": true, + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + }, + "buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", + "dev": true + }, + "buffer-xor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", + "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=", + "dev": true + }, + "builtin-status-codes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", + "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=", + "dev": true + }, + "bytebuffer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/bytebuffer/-/bytebuffer-5.0.1.tgz", + "integrity": "sha1-WC7qSxqHO20CCkjVjfhfC7ps/d0=", + "requires": { + "long": "~3" + } + }, + "bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" + }, + "cacache": { + "version": "12.0.3", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-12.0.3.tgz", + "integrity": "sha512-kqdmfXEGFepesTuROHMs3MpFLWrPkSSpRqOw80RCflZXy/khxaArvFrQ7uJxSUduzAufc6G0g1VUCOZXxWavPw==", + "dev": true, + "requires": { + "bluebird": "^3.5.5", + "chownr": "^1.1.1", + "figgy-pudding": "^3.5.1", + "glob": "^7.1.4", + "graceful-fs": "^4.1.15", + "infer-owner": "^1.0.3", + "lru-cache": "^5.1.1", + "mississippi": "^3.0.0", + "mkdirp": "^0.5.1", + "move-concurrently": "^1.0.1", + "promise-inflight": "^1.0.1", + "rimraf": "^2.6.3", + "ssri": "^6.0.1", + "unique-filename": "^1.1.1", + "y18n": "^4.0.0" + }, + "dependencies": { + "lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "requires": { + "yallist": "^3.0.2" + } + }, + "y18n": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", + "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", + "dev": true + } + } + }, + "cache-base": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", + "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", + "dev": true, + "requires": { + "collection-visit": "^1.0.0", + "component-emitter": "^1.2.1", + "get-value": "^2.0.6", + "has-value": "^1.0.0", + "isobject": "^3.0.1", + "set-value": "^2.0.0", + "to-object-path": "^0.3.0", + "union-value": "^1.0.0", + "unset-value": "^1.0.0" + } + }, + "caching-transform": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/caching-transform/-/caching-transform-3.0.2.tgz", + "integrity": "sha512-Mtgcv3lh3U0zRii/6qVgQODdPA4G3zhG+jtbCWj39RXuUFTMzH0vcdMtaJS1jPowd+It2Pqr6y3NJMQqOqCE2w==", + "dev": true, + "requires": { + "hasha": "^3.0.0", + "make-dir": "^2.0.0", + "package-hash": "^3.0.0", + "write-file-atomic": "^2.4.2" + } + }, + "caller-callsite": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz", + "integrity": "sha1-hH4PzgoiN1CpoCfFSzNzGtMVQTQ=", + "dev": true, + "requires": { + "callsites": "^2.0.0" + }, + "dependencies": { + "callsites": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", + "integrity": "sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA=", + "dev": true + } + } + }, + "caller-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-2.0.0.tgz", + "integrity": "sha1-Ro+DBE42mrIBD6xfBs7uFbsssfQ=", + "dev": true, + "requires": { + "caller-callsite": "^2.0.0" + } + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, + "camelcase": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", + "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=" + }, + "camelcase-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-4.2.0.tgz", + "integrity": "sha1-oqpfsa9oh1glnDLBQUJteJI7m3c=", + "dev": true, + "requires": { + "camelcase": "^4.1.0", + "map-obj": "^2.0.0", + "quick-lru": "^1.0.0" + }, + "dependencies": { + "camelcase": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", + "dev": true + } + } + }, + "caniuse-lite": { + "version": "1.0.30001023", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001023.tgz", + "integrity": "sha512-C5TDMiYG11EOhVOA62W1p3UsJ2z4DsHtMBQtjzp3ZsUglcQn62WOUgW0y795c7A5uZ+GCEIvzkMatLIlAsbNTA==", + "dev": true + }, + "canvas": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/canvas/-/canvas-2.6.1.tgz", + "integrity": "sha512-S98rKsPcuhfTcYbtF53UIJhcbgIAK533d1kJKMwsMwAIFgfd58MOyxRud3kktlzWiEkFliaJtvyZCBtud/XVEA==", + "dev": true, + "requires": { + "nan": "^2.14.0", + "node-pre-gyp": "^0.11.0", + "simple-get": "^3.0.3" + } + }, + "capture-stack-trace": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/capture-stack-trace/-/capture-stack-trace-1.0.1.tgz", + "integrity": "sha512-mYQLZnx5Qt1JgB1WEiMCf2647plpGeQ2NMR/5L0HNZzGQo4fuSPnK+wjfPnKZV0aiJDgzmWqqkV/g7JD+DW0qw==", + "dev": true + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", + "dev": true + }, + "center-align": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", + "integrity": "sha1-qg0yYptu6XIgBBHL1EYckHvCt60=", + "requires": { + "align-text": "^0.1.3", + "lazy-cache": "^1.0.3" + } + }, + "chai": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz", + "integrity": "sha512-XQU3bhBukrOsQCuwZndwGcCVQHyZi53fQ6Ys1Fym7E4olpIqqZZhhoFJoaKVvV17lWQoXYwgWN2nF5crA8J2jw==", + "dev": true, + "requires": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^3.0.1", + "get-func-name": "^2.0.0", + "pathval": "^1.1.0", + "type-detect": "^4.0.5" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + } + } + }, + "character-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/character-parser/-/character-parser-2.2.0.tgz", + "integrity": "sha1-x84o821LzZdE5f/CxfzeHHMmH8A=", + "requires": { + "is-regex": "^1.0.3" + } + }, + "chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true + }, + "chart.js": { + "version": "2.9.3", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-2.9.3.tgz", + "integrity": "sha512-+2jlOobSk52c1VU6fzkh3UwqHMdSlgH1xFv9FKMqHiNCpXsGPQa/+81AFa+i3jZ253Mq9aAycPwDjnn1XbRNNw==", + "dev": true, + "requires": { + "chartjs-color": "^2.1.0", + "moment": "^2.10.2" + } + }, + "chartjs-color": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/chartjs-color/-/chartjs-color-2.4.1.tgz", + "integrity": "sha512-haqOg1+Yebys/Ts/9bLo/BqUcONQOdr/hoEr2LLTRl6C5LXctUdHxsCYfvQVg5JIxITrfCNUDr4ntqmQk9+/0w==", + "dev": true, + "requires": { + "chartjs-color-string": "^0.6.0", + "color-convert": "^1.9.3" + } + }, + "chartjs-color-string": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/chartjs-color-string/-/chartjs-color-string-0.6.0.tgz", + "integrity": "sha512-TIB5OKn1hPJvO7JcteW4WY/63v6KwEdt6udfnDE9iCAZgy+V4SrbSxoIbTw/xkUIapjEI4ExGtD0+6D3KyFd7A==", + "dev": true, + "requires": { + "color-name": "^1.0.0" + } + }, + "chartjs-plugin-zoom": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/chartjs-plugin-zoom/-/chartjs-plugin-zoom-0.7.5.tgz", + "integrity": "sha512-OGVQXlw5meOD7ac+CBNO7yKg4Tk06eBb5LUIgpK/qgv7SjVB/89pWMQY3pxWnzCMI8FsoV3iTKQ2ZCOvh4+q6w==", + "dev": true, + "requires": { + "hammerjs": "^2.0.8" + } + }, + "check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", + "dev": true + }, + "check-types": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/check-types/-/check-types-8.0.3.tgz", + "integrity": "sha512-YpeKZngUmG65rLudJ4taU7VLkOCTMhNl/u4ctNC56LQS/zJTyNH0Lrtwm1tfTsbLlwvlfsA2d1c8vCf/Kh2KwQ==", + "dev": true + }, + "cheerio": { + "version": "1.0.0-rc.3", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.3.tgz", + "integrity": "sha512-0td5ijfUPuubwLUu0OBoe98gZj8C/AA+RW3v67GPlGOrvxWjZmBXiBCRU+I8VEiNyJzjth40POfHiz2RB3gImA==", + "dev": true, + "requires": { + "css-select": "~1.2.0", + "dom-serializer": "~0.1.1", + "entities": "~1.1.1", + "htmlparser2": "^3.9.1", + "lodash": "^4.15.0", + "parse5": "^3.0.1" + } + }, + "chokidar": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", + "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", + "dev": true, + "requires": { + "anymatch": "^2.0.0", + "async-each": "^1.0.1", + "braces": "^2.3.2", + "fsevents": "^1.2.7", + "glob-parent": "^3.1.0", + "inherits": "^2.0.3", + "is-binary-path": "^1.0.0", + "is-glob": "^4.0.0", + "normalize-path": "^3.0.0", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.2.1", + "upath": "^1.1.1" + } + }, + "chownr": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.3.tgz", + "integrity": "sha512-i70fVHhmV3DtTl6nqvZOnIjbY0Pe4kAUjwHj8z0zAdgBtYrJyYwLKCCuRBQ5ppkyL0AkN7HKRnETdmdp1zqNXw==", + "dev": true + }, + "chroma-js": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/chroma-js/-/chroma-js-1.4.1.tgz", + "integrity": "sha512-jTwQiT859RTFN/vIf7s+Vl/Z2LcMrvMv3WUFmd/4u76AdlFC0NTNgqEEFPcRiHmAswPsMiQEDZLM8vX8qXpZNQ==", + "dev": true + }, + "chrome-trace-event": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz", + "integrity": "sha512-9e/zx1jw7B4CO+c/RXoCsfg/x1AfUBioy4owYH0bJprEYAx5hRFLRhWBqHAG57D0ZM4H7vxbP7bPe0VwhQRYDQ==", + "dev": true, + "requires": { + "tslib": "^1.9.0" + } + }, + "ci-info": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", + "dev": true + }, + "cipher-base": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", + "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "class-utils": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", + "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", + "dev": true, + "requires": { + "arr-union": "^3.1.0", + "define-property": "^0.2.5", + "isobject": "^3.0.0", + "static-extend": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, + "classnames": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz", + "integrity": "sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q==", + "dev": true + }, + "clean-css": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.3.tgz", + "integrity": "sha512-VcMWDN54ZN/DS+g58HYL5/n4Zrqe8vHJpGA8KdgUXFU4fuP/aHNw8eld9SyEIyabIMJX/0RaY/fplOo5hYLSFA==", + "requires": { + "source-map": "~0.6.0" + } + }, + "cli-boxes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-1.0.0.tgz", + "integrity": "sha1-T6kXw+WclKAEzWH47lCdplFocUM=", + "dev": true + }, + "cli-cursor": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", + "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", + "dev": true, + "requires": { + "restore-cursor": "^2.0.0" + } + }, + "cli-spinners": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-1.3.1.tgz", + "integrity": "sha512-1QL4544moEsDVH9T/l6Cemov/37iv1RtoKf7NJ04A60+4MREXNfx/QvavbH6QoGdsD4N4Mwy49cmaINR/o2mdg==", + "dev": true + }, + "cli-width": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", + "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=", + "dev": true + }, + "cliui": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", + "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", + "requires": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wrap-ansi": "^2.0.0" + }, + "dependencies": { + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "requires": { + "ansi-regex": "^2.0.0" + } + } + } + }, + "clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=", + "dev": true + }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" + }, + "collection-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", + "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", + "dev": true, + "requires": { + "map-visit": "^1.0.0", + "object-visit": "^1.0.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-hash": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/color-hash/-/color-hash-1.0.3.tgz", + "integrity": "sha1-wOeVLwbQIuVI5l2iOVEr1n04Ce4=", + "dev": true + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "colors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", + "integrity": "sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=" + }, + "colour": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/colour/-/colour-0.7.1.tgz", + "integrity": "sha1-nLFpkX7F0SwHNtPoaFdG3xyt93g=" + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true + }, + "commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", + "dev": true + }, + "component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", + "dev": true + }, + "compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "requires": { + "mime-db": ">= 1.43.0 < 2" + } + }, + "compression": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", + "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "requires": { + "accepts": "~1.3.5", + "bytes": "3.0.0", + "compressible": "~2.0.16", + "debug": "2.6.9", + "on-headers": "~1.0.2", + "safe-buffer": "5.1.2", + "vary": "~1.1.2" + }, + "dependencies": { + "bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + } + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "concurrently": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-3.6.1.tgz", + "integrity": "sha512-/+ugz+gwFSEfTGUxn0KHkY+19XPRTXR8+7oUK/HxgiN1n7FjeJmkrbSiXAJfyQ0zORgJYPaenmymwon51YXH9Q==", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "commander": "2.6.0", + "date-fns": "^1.23.0", + "lodash": "^4.5.1", + "read-pkg": "^3.0.0", + "rx": "2.3.24", + "spawn-command": "^0.0.2-1", + "supports-color": "^3.2.3", + "tree-kill": "^1.1.0" + }, + "dependencies": { + "commander": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.6.0.tgz", + "integrity": "sha1-nfflL7Kgyw+4kFjugMMQQiXzfh0=", + "dev": true + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=", + "dev": true + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "dev": true, + "requires": { + "has-flag": "^1.0.0" + } + } + } + }, + "configstore": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/configstore/-/configstore-3.1.2.tgz", + "integrity": "sha512-vtv5HtGjcYUgFrXc6Kx747B83MRRVS5R1VTEQoXvuP+kMI+if6uywV0nDGoiydJRy4yk7h9od5Og0kxx4zUXmw==", + "dev": true, + "requires": { + "dot-prop": "^4.1.0", + "graceful-fs": "^4.1.2", + "make-dir": "^1.0.0", + "unique-string": "^1.0.0", + "write-file-atomic": "^2.0.0", + "xdg-basedir": "^3.0.0" + }, + "dependencies": { + "make-dir": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", + "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", + "dev": true, + "requires": { + "pify": "^3.0.0" + } + }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + } + } + }, + "confusing-browser-globals": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.9.tgz", + "integrity": "sha512-KbS1Y0jMtyPgIxjO7ZzMAuUpAKMt1SzCL9fsrKsX6b0zJPTaT0SiSPmewwVZg9UAO83HVIlEhZF84LIjZ0lmAw==", + "dev": true + }, + "console-browserify": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz", + "integrity": "sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==", + "dev": true + }, + "console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", + "dev": true + }, + "constantinople": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/constantinople/-/constantinople-3.1.2.tgz", + "integrity": "sha512-yePcBqEFhLOqSBtwYOGGS1exHo/s1xjekXiinh4itpNQGCu4KA1euPh1fg07N2wMITZXQkBz75Ntdt1ctGZouw==", + "requires": { + "@types/babel-types": "^7.0.0", + "@types/babylon": "^6.16.2", + "babel-types": "^6.26.0", + "babylon": "^6.18.0" + } + }, + "constants-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", + "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=", + "dev": true + }, + "contains-path": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/contains-path/-/contains-path-0.1.0.tgz", + "integrity": "sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo=", + "dev": true + }, + "content-disposition": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", + "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", + "requires": { + "safe-buffer": "5.1.2" + } + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" + }, + "convert-source-map": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", + "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.1" + } + }, + "cookie": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" + }, + "cookie-session": { + "version": "2.0.0-rc.1", + "resolved": "https://registry.npmjs.org/cookie-session/-/cookie-session-2.0.0-rc.1.tgz", + "integrity": "sha512-zg80EsLe7S1J4y0XxV7SZ8Fbi90ZZoampuX2bfYDOvJfc//98sSlZC41YDzTTjtVbeU1VlVdBbldXOOyi5xzEw==", + "requires": { + "cookies": "0.8.0", + "debug": "3.2.6", + "on-headers": "~1.0.2", + "safe-buffer": "5.2.0" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "safe-buffer": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz", + "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==" + } + } + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" + }, + "cookiejar": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.2.tgz", + "integrity": "sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA==", + "dev": true + }, + "cookies": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/cookies/-/cookies-0.8.0.tgz", + "integrity": "sha512-8aPsApQfebXnuI+537McwYsDtjVxGm8gTIzQI3FDW6t5t/DAhERxtnbEPN/8RX+uZthoz4eCOgloXaE5cYyNow==", + "requires": { + "depd": "~2.0.0", + "keygrip": "~1.1.0" + }, + "dependencies": { + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" + } + } + }, + "copy-concurrently": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/copy-concurrently/-/copy-concurrently-1.0.5.tgz", + "integrity": "sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A==", + "dev": true, + "requires": { + "aproba": "^1.1.1", + "fs-write-stream-atomic": "^1.0.8", + "iferr": "^0.1.5", + "mkdirp": "^0.5.1", + "rimraf": "^2.5.4", + "run-queue": "^1.0.0" + } + }, + "copy-descriptor": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", + "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", + "dev": true + }, + "copy-to-clipboard": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.2.0.tgz", + "integrity": "sha512-eOZERzvCmxS8HWzugj4Uxl8OJxa7T2k1Gi0X5qavwydHIfuSHq2dTD09LOg/XyGq4Zpb5IsR/2OJ5lbOegz78w==", + "dev": true, + "requires": { + "toggle-selection": "^1.0.6" + } + }, + "core-js": { + "version": "2.6.11", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.11.tgz", + "integrity": "sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg==" + }, + "core-js-compat": { + "version": "3.6.4", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.6.4.tgz", + "integrity": "sha512-zAa3IZPvsJ0slViBQ2z+vgyyTuhd3MFn1rBQjZSKVEgB0UMYhUkCj9jJUVPgGTGqWvsBVmfnruXgTcNyTlEiSA==", + "dev": true, + "requires": { + "browserslist": "^4.8.3", + "semver": "7.0.0" + }, + "dependencies": { + "semver": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz", + "integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==", + "dev": true + } + } + }, + "core-js-pure": { + "version": "3.6.4", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.6.4.tgz", + "integrity": "sha512-epIhRLkXdgv32xIUFaaAry2wdxZYBi6bgM7cB136dzzXXa+dFyRLTZeLUJxnd8ShrmyVXBub63n2NHo2JAt8Cw==", + "dev": true + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "dev": true + }, + "cosmiconfig": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.1.tgz", + "integrity": "sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==", + "dev": true, + "requires": { + "import-fresh": "^2.0.0", + "is-directory": "^0.3.1", + "js-yaml": "^3.13.1", + "parse-json": "^4.0.0" + }, + "dependencies": { + "import-fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz", + "integrity": "sha1-2BNVwVYS04bGH53dOSLUMEgipUY=", + "dev": true, + "requires": { + "caller-path": "^2.0.0", + "resolve-from": "^3.0.0" + } + }, + "resolve-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", + "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=", + "dev": true + } + } + }, + "coveralls": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/coveralls/-/coveralls-3.0.9.tgz", + "integrity": "sha512-nNBg3B1+4iDox5A5zqHKzUTiwl2ey4k2o0NEcVZYvl+GOSJdKBj4AJGKLv6h3SvWch7tABHePAQOSZWM9E2hMg==", + "dev": true, + "requires": { + "js-yaml": "^3.13.1", + "lcov-parse": "^1.0.0", + "log-driver": "^1.2.7", + "minimist": "^1.2.0", + "request": "^2.88.0" + } + }, + "cp-file": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/cp-file/-/cp-file-6.2.0.tgz", + "integrity": "sha512-fmvV4caBnofhPe8kOcitBwSn2f39QLjnAnGq3gO9dfd75mUytzKNZB1hde6QHunW2Rt+OwuBOMc3i1tNElbszA==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "make-dir": "^2.0.0", + "nested-error-stacks": "^2.0.0", + "pify": "^4.0.1", + "safe-buffer": "^5.0.1" + } + }, + "create-ecdh": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.3.tgz", + "integrity": "sha512-GbEHQPMOswGpKXM9kCWVrremUcBmjteUaQ01T9rkKCPDXfUHX0IoP9LpHYo2NPFampa4e+/pFDc3jQdxrxQLaw==", + "dev": true, + "requires": { + "bn.js": "^4.1.0", + "elliptic": "^6.0.0" + } + }, + "create-error-class": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/create-error-class/-/create-error-class-3.0.2.tgz", + "integrity": "sha1-Br56vvlHo/FKMP1hBnHUAbyot7Y=", + "dev": true, + "requires": { + "capture-stack-trace": "^1.0.0" + } + }, + "create-hash": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", + "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", + "dev": true, + "requires": { + "cipher-base": "^1.0.1", + "inherits": "^2.0.1", + "md5.js": "^1.3.4", + "ripemd160": "^2.0.1", + "sha.js": "^2.4.0" + } + }, + "create-hmac": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", + "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", + "dev": true, + "requires": { + "cipher-base": "^1.0.3", + "create-hash": "^1.1.0", + "inherits": "^2.0.1", + "ripemd160": "^2.0.0", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "create-react-class": { + "version": "15.6.3", + "resolved": "https://registry.npmjs.org/create-react-class/-/create-react-class-15.6.3.tgz", + "integrity": "sha512-M+/3Q6E6DLO6Yx3OwrWjwHBnvfXXYA7W+dFjt/ZDBemHO1DDZhsalX/NUtnTYclN6GfnBDRh4qRHjcDHmlJBJg==", + "dev": true, + "requires": { + "fbjs": "^0.8.9", + "loose-envify": "^1.3.1", + "object-assign": "^4.1.1" + } + }, + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "crypto-browserify": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", + "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==", + "dev": true, + "requires": { + "browserify-cipher": "^1.0.0", + "browserify-sign": "^4.0.0", + "create-ecdh": "^4.0.0", + "create-hash": "^1.1.0", + "create-hmac": "^1.1.0", + "diffie-hellman": "^5.0.0", + "inherits": "^2.0.1", + "pbkdf2": "^3.0.3", + "public-encrypt": "^4.0.0", + "randombytes": "^2.0.0", + "randomfill": "^1.0.3" + } + }, + "crypto-random-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-1.0.0.tgz", + "integrity": "sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4=", + "dev": true + }, + "css-loader": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-3.4.2.tgz", + "integrity": "sha512-jYq4zdZT0oS0Iykt+fqnzVLRIeiPWhka+7BqPn+oSIpWJAHak5tmB/WZrJ2a21JhCeFyNnnlroSl8c+MtVndzA==", + "dev": true, + "requires": { + "camelcase": "^5.3.1", + "cssesc": "^3.0.0", + "icss-utils": "^4.1.1", + "loader-utils": "^1.2.3", + "normalize-path": "^3.0.0", + "postcss": "^7.0.23", + "postcss-modules-extract-imports": "^2.0.0", + "postcss-modules-local-by-default": "^3.0.2", + "postcss-modules-scope": "^2.1.1", + "postcss-modules-values": "^3.0.0", + "postcss-value-parser": "^4.0.2", + "schema-utils": "^2.6.0" + }, + "dependencies": { + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + } + } + }, + "css-select": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz", + "integrity": "sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=", + "dev": true, + "requires": { + "boolbase": "~1.0.0", + "css-what": "2.1", + "domutils": "1.5.1", + "nth-check": "~1.0.1" + } + }, + "css-what": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.3.tgz", + "integrity": "sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==", + "dev": true + }, + "cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true + }, + "cssom": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", + "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", + "dev": true + }, + "cssstyle": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-1.4.0.tgz", + "integrity": "sha512-GBrLZYZ4X4x6/QEoBnIrqb8B/f5l4+8me2dkom/j1Gtbxy0kBv6OGzKuAsGM75bkGwGAFkt56Iwg28S3XTZgSA==", + "dev": true, + "requires": { + "cssom": "0.3.x" + } + }, + "currently-unhandled": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", + "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=", + "dev": true, + "requires": { + "array-find-index": "^1.0.1" + } + }, + "cycle": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/cycle/-/cycle-1.0.3.tgz", + "integrity": "sha1-IegLK+hYD5i0aPN5QwZisEbDStI=" + }, + "cyclist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-1.0.1.tgz", + "integrity": "sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=", + "dev": true + }, + "d": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz", + "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==", + "dev": true, + "requires": { + "es5-ext": "^0.10.50", + "type": "^1.0.1" + } + }, + "d3-array": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-1.2.4.tgz", + "integrity": "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==", + "dev": true + }, + "d3-collection": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/d3-collection/-/d3-collection-1.0.7.tgz", + "integrity": "sha512-ii0/r5f4sjKNTfh84Di+DpztYwqKhEyUlKoPrzUFfeSkWxjW49xU2QzO9qrPrNkpdI0XJkfzvmTu8V2Zylln6A==", + "dev": true + }, + "d3-color": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-1.4.0.tgz", + "integrity": "sha512-TzNPeJy2+iEepfiL92LAAB7fvnp/dV2YwANPVHdDWmYMm23qIJBYww3qT8I8C1wXrmrg4UWs7BKc2tKIgyjzHg==", + "dev": true + }, + "d3-contour": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-1.3.2.tgz", + "integrity": "sha512-hoPp4K/rJCu0ladiH6zmJUEz6+u3lgR+GSm/QdM2BBvDraU39Vr7YdDCicJcxP1z8i9B/2dJLgDC1NcvlF8WCg==", + "dev": true, + "requires": { + "d3-array": "^1.1.1" + } + }, + "d3-format": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-1.4.3.tgz", + "integrity": "sha512-mm/nE2Y9HgGyjP+rKIekeITVgBtX97o1nrvHCWX8F/yBYyevUTvu9vb5pUnKwrcSw7o7GuwMOWjS9gFDs4O+uQ==", + "dev": true + }, + "d3-geo": { + "version": "1.11.9", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-1.11.9.tgz", + "integrity": "sha512-9edcH6J3s/Aa3KJITWqFJbyB/8q3mMlA9Fi7z6yy+FAYMnRaxmC7jBhUnsINxVWD14GmqX3DK8uk7nV6/Ekt4A==", + "dev": true, + "requires": { + "d3-array": "1" + } + }, + "d3-hexbin": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/d3-hexbin/-/d3-hexbin-0.2.2.tgz", + "integrity": "sha1-nFg32s/UcasFM3qeke8Qv8T5iDE=", + "dev": true + }, + "d3-hierarchy": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-1.1.9.tgz", + "integrity": "sha512-j8tPxlqh1srJHAtxfvOUwKNYJkQuBFdM1+JAUfq6xqH5eAqf93L7oG1NVqDa4CpFZNvnNKtCYEUC8KY9yEn9lQ==", + "dev": true + }, + "d3-interpolate": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-1.4.0.tgz", + "integrity": "sha512-V9znK0zc3jOPV4VD2zZn0sDhZU3WAE2bmlxdIwwQPPzPjvyLkd8B3JUVdS1IDUFDkWZ72c9qnv1GK2ZagTZ8EA==", + "dev": true, + "requires": { + "d3-color": "1" + } + }, + "d3-path": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", + "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==", + "dev": true + }, + "d3-sankey": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/d3-sankey/-/d3-sankey-0.7.1.tgz", + "integrity": "sha1-0imDImj8aaf+yEgD6WwiVqYUxSE=", + "dev": true, + "requires": { + "d3-array": "1", + "d3-collection": "1", + "d3-shape": "^1.2.0" + } + }, + "d3-scale": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-1.0.7.tgz", + "integrity": "sha512-KvU92czp2/qse5tUfGms6Kjig0AhHOwkzXG0+PqIJB3ke0WUv088AHMZI0OssO9NCkXt4RP8yju9rpH8aGB7Lw==", + "dev": true, + "requires": { + "d3-array": "^1.2.0", + "d3-collection": "1", + "d3-color": "1", + "d3-format": "1", + "d3-interpolate": "1", + "d3-time": "1", + "d3-time-format": "2" + } + }, + "d3-shape": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", + "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", + "dev": true, + "requires": { + "d3-path": "1" + } + }, + "d3-time": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-1.1.0.tgz", + "integrity": "sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA==", + "dev": true + }, + "d3-time-format": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-2.2.3.tgz", + "integrity": "sha512-RAHNnD8+XvC4Zc4d2A56Uw0yJoM7bsvOlJR33bclxq399Rak/b9bhvu/InjxdWhPtkgU53JJcleJTGkNRnN6IA==", + "dev": true, + "requires": { + "d3-time": "1" + } + }, + "d3-voronoi": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/d3-voronoi/-/d3-voronoi-1.1.4.tgz", + "integrity": "sha512-dArJ32hchFsrQ8uMiTBLq256MpnZjeuBtdHpaDlYuQyjU0CVzCJl/BVW+SkszaAeH95D/8gxqAhgx0ouAWAfRg==", + "dev": true + }, + "dagre": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/dagre/-/dagre-0.8.5.tgz", + "integrity": "sha512-/aTqmnRta7x7MCCpExk7HQL2O4owCT2h8NT//9I1OQ9vt29Pa0BzSAkR5lwFUcQ7491yVi/3CXU9jQ5o0Mn2Sw==", + "requires": { + "graphlib": "^2.1.8", + "lodash": "^4.17.15" + } + }, + "damerau-levenshtein": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.6.tgz", + "integrity": "sha512-JVrozIeElnj3QzfUIt8tB8YMluBJom4Vw9qTPpjGYQ9fYlB3D/rb6OordUxf3xeFB35LKWs0xqcO5U6ySvBtug==", + "dev": true + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0" + } + }, + "data-urls": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-1.1.0.tgz", + "integrity": "sha512-YTWYI9se1P55u58gL5GkQHW4P6VJBJ5iBT+B5a7i2Tjadhv52paJG0qHX4A0OR6/t52odI64KP2YvFpkDOi3eQ==", + "dev": true, + "requires": { + "abab": "^2.0.0", + "whatwg-mimetype": "^2.2.0", + "whatwg-url": "^7.0.0" + } + }, + "date-fns": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-1.30.1.tgz", + "integrity": "sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw==", + "dev": true + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" + }, + "decamelize-keys": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.0.tgz", + "integrity": "sha1-0XGoeTMlKAfrPLYdwcFEXQeN8tk=", + "dev": true, + "requires": { + "decamelize": "^1.1.0", + "map-obj": "^1.0.0" + }, + "dependencies": { + "map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=", + "dev": true + } + } + }, + "decode-uri-component": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", + "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", + "dev": true + }, + "decompress-response": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", + "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", + "dev": true, + "requires": { + "mimic-response": "^2.0.0" + } + }, + "deep-eql": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", + "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "dev": true, + "requires": { + "type-detect": "^4.0.0" + } + }, + "deep-equal": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.1.tgz", + "integrity": "sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g==", + "dev": true, + "requires": { + "is-arguments": "^1.0.4", + "is-date-object": "^1.0.1", + "is-regex": "^1.0.4", + "object-is": "^1.0.1", + "object-keys": "^1.1.1", + "regexp.prototype.flags": "^1.2.0" + } + }, + "deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true + }, + "deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", + "dev": true + }, + "deepmerge": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", + "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==" + }, + "default-require-extensions": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-2.0.0.tgz", + "integrity": "sha1-9fj7sYp9bVCyH2QfZJ67Uiz+JPc=", + "dev": true, + "requires": { + "strip-bom": "^3.0.0" + } + }, + "defaults": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz", + "integrity": "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=", + "dev": true, + "requires": { + "clone": "^1.0.2" + }, + "dependencies": { + "clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=", + "dev": true + } + } + }, + "define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "dev": true, + "requires": { + "object-keys": "^1.0.12" + } + }, + "define-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", + "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "dev": true, + "requires": { + "is-descriptor": "^1.0.2", + "isobject": "^3.0.1" + }, + "dependencies": { + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + }, + "kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true + } + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "dev": true + }, + "delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", + "dev": true + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" + }, + "des.js": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.1.tgz", + "integrity": "sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA==", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" + }, + "detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", + "dev": true + }, + "diff": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", + "dev": true + }, + "diffie-hellman": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", + "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", + "dev": true, + "requires": { + "bn.js": "^4.1.0", + "miller-rabin": "^4.0.0", + "randombytes": "^2.0.0" + } + }, + "discontinuous-range": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/discontinuous-range/-/discontinuous-range-1.0.0.tgz", + "integrity": "sha1-44Mx8IRLukm5qctxx3FYWqsbxlo=", + "dev": true + }, + "doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "doctypes": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/doctypes/-/doctypes-1.1.0.tgz", + "integrity": "sha1-6oCxBqh1OHdOijpKWv4pPeSJ4Kk=" + }, + "dom-serializer": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.1.tgz", + "integrity": "sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA==", + "dev": true, + "requires": { + "domelementtype": "^1.3.0", + "entities": "^1.1.1" + } + }, + "dom-walk": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.1.tgz", + "integrity": "sha1-ZyIm3HTI95mtNTB9+TaroRrNYBg=", + "dev": true + }, + "domain-browser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz", + "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==", + "dev": true + }, + "domelementtype": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", + "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==", + "dev": true + }, + "domexception": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-1.0.1.tgz", + "integrity": "sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug==", + "dev": true, + "requires": { + "webidl-conversions": "^4.0.2" + } + }, + "domhandler": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz", + "integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==", + "dev": true, + "requires": { + "domelementtype": "1" + } + }, + "domutils": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", + "integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=", + "dev": true, + "requires": { + "dom-serializer": "0", + "domelementtype": "1" + } + }, + "dot-prop": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.0.tgz", + "integrity": "sha512-tUMXrxlExSW6U2EXiiKGSBVdYgtV8qlHL+C10TsW4PURY/ic+eaysnSkwB4kA/mBlCyy/IKDJ+Lc3wbWeaXtuQ==", + "dev": true, + "requires": { + "is-obj": "^1.0.0" + } + }, + "duplexer": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", + "integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=", + "dev": true + }, + "duplexer3": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", + "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=", + "dev": true + }, + "duplexify": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", + "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", + "dev": true, + "requires": { + "end-of-stream": "^1.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.0.0", + "stream-shift": "^1.0.0" + } + }, + "ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "dev": true, + "requires": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, + "ejs": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-2.7.4.tgz", + "integrity": "sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA==" + }, + "electron-to-chromium": { + "version": "1.3.344", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.344.tgz", + "integrity": "sha512-tvbx2Wl8WBR+ym3u492D0L6/jH+8NoQXqe46+QhbWH3voVPauGuZYeb1QAXYoOAWuiP2dbSvlBx0kQ1F3hu/Mw==", + "dev": true + }, + "elliptic": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.2.tgz", + "integrity": "sha512-f4x70okzZbIQl/NSRLkI/+tteV/9WqL98zx+SQ69KbXxmVrmjwsNUPn/gYJJ0sHvEak24cZgHIPegRePAtA/xw==", + "dev": true, + "requires": { + "bn.js": "^4.4.0", + "brorand": "^1.0.1", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.0" + } + }, + "emitter-component": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/emitter-component/-/emitter-component-1.1.1.tgz", + "integrity": "sha1-Bl4tvtaVm/RwZ57avq95gdEAOrY=", + "dev": true + }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "emojis-list": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz", + "integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k=", + "dev": true + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" + }, + "encoding": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz", + "integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=", + "dev": true, + "requires": { + "iconv-lite": "~0.4.13" + } + }, + "end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "requires": { + "once": "^1.4.0" + } + }, + "enhanced-resolve": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.1.1.tgz", + "integrity": "sha512-98p2zE+rL7/g/DzMHMTF4zZlCgeVdJ7yr6xzEpJRYwFYrGi9ANdn5DnJURg6RpBkyk60XYDnWIv51VfIhfNGuA==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "memory-fs": "^0.5.0", + "tapable": "^1.0.0" + }, + "dependencies": { + "memory-fs": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.5.0.tgz", + "integrity": "sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA==", + "dev": true, + "requires": { + "errno": "^0.1.3", + "readable-stream": "^2.0.1" + } + } + } + }, + "entities": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", + "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==", + "dev": true + }, + "enzyme": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/enzyme/-/enzyme-3.11.0.tgz", + "integrity": "sha512-Dw8/Gs4vRjxY6/6i9wU0V+utmQO9kvh9XLnz3LIudviOnVYDEe2ec+0k+NQoMamn1VrjKgCUOWj5jG/5M5M0Qw==", + "dev": true, + "requires": { + "array.prototype.flat": "^1.2.3", + "cheerio": "^1.0.0-rc.3", + "enzyme-shallow-equal": "^1.0.1", + "function.prototype.name": "^1.1.2", + "has": "^1.0.3", + "html-element-map": "^1.2.0", + "is-boolean-object": "^1.0.1", + "is-callable": "^1.1.5", + "is-number-object": "^1.0.4", + "is-regex": "^1.0.5", + "is-string": "^1.0.5", + "is-subset": "^0.1.1", + "lodash.escape": "^4.0.1", + "lodash.isequal": "^4.5.0", + "object-inspect": "^1.7.0", + "object-is": "^1.0.2", + "object.assign": "^4.1.0", + "object.entries": "^1.1.1", + "object.values": "^1.1.1", + "raf": "^3.4.1", + "rst-selector-parser": "^2.2.3", + "string.prototype.trim": "^1.2.1" + } + }, + "enzyme-adapter-react-16": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.15.2.tgz", + "integrity": "sha512-SkvDrb8xU3lSxID8Qic9rB8pvevDbLybxPK6D/vW7PrT0s2Cl/zJYuXvsd1EBTz0q4o3iqG3FJhpYz3nUNpM2Q==", + "dev": true, + "requires": { + "enzyme-adapter-utils": "^1.13.0", + "enzyme-shallow-equal": "^1.0.1", + "has": "^1.0.3", + "object.assign": "^4.1.0", + "object.values": "^1.1.1", + "prop-types": "^15.7.2", + "react-is": "^16.12.0", + "react-test-renderer": "^16.0.0-0", + "semver": "^5.7.0" + } + }, + "enzyme-adapter-utils": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/enzyme-adapter-utils/-/enzyme-adapter-utils-1.13.0.tgz", + "integrity": "sha512-YuEtfQp76Lj5TG1NvtP2eGJnFKogk/zT70fyYHXK2j3v6CtuHqc8YmgH/vaiBfL8K1SgVVbQXtTcgQZFwzTVyQ==", + "dev": true, + "requires": { + "airbnb-prop-types": "^2.15.0", + "function.prototype.name": "^1.1.2", + "object.assign": "^4.1.0", + "object.fromentries": "^2.0.2", + "prop-types": "^15.7.2", + "semver": "^5.7.1" + } + }, + "enzyme-shallow-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/enzyme-shallow-equal/-/enzyme-shallow-equal-1.0.1.tgz", + "integrity": "sha512-hGA3i1so8OrYOZSM9whlkNmVHOicJpsjgTzC+wn2JMJXhq1oO4kA4bJ5MsfzSIcC71aLDKzJ6gZpIxrqt3QTAQ==", + "dev": true, + "requires": { + "has": "^1.0.3", + "object-is": "^1.0.2" + } + }, + "errno": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz", + "integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==", + "dev": true, + "requires": { + "prr": "~1.0.1" + } + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "es-abstract": { + "version": "1.17.4", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.4.tgz", + "integrity": "sha512-Ae3um/gb8F0mui/jPL+QiqmglkUsaQf7FwBEHYIFkztkneosu9imhqHpBzQ3h1vit8t5iQ74t6PEVvphBZiuiQ==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.1.5", + "is-regex": "^1.0.5", + "object-inspect": "^1.7.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimleft": "^2.1.1", + "string.prototype.trimright": "^2.1.1" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "es5-ext": { + "version": "0.10.53", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.53.tgz", + "integrity": "sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q==", + "dev": true, + "requires": { + "es6-iterator": "~2.0.3", + "es6-symbol": "~3.1.3", + "next-tick": "~1.0.0" + } + }, + "es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true + }, + "es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=", + "dev": true, + "requires": { + "d": "1", + "es5-ext": "^0.10.35", + "es6-symbol": "^3.1.1" + } + }, + "es6-symbol": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz", + "integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==", + "dev": true, + "requires": { + "d": "^1.0.1", + "ext": "^1.1.2" + } + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "escodegen": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.13.0.tgz", + "integrity": "sha512-eYk2dCkxR07DsHA/X2hRBj0CFAZeri/LyDMc0C8JT1Hqi6JnVpMhJ7XFITbb0+yZS3lVkaPL2oCkZ3AVmeVbMw==", + "dev": true, + "requires": { + "esprima": "^4.0.1", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1", + "source-map": "~0.6.1" + } + }, + "eslint": { + "version": "5.16.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-5.16.0.tgz", + "integrity": "sha512-S3Rz11i7c8AA5JPv7xAH+dOyq/Cu/VXHiHXBPOU1k/JAM5dXqQPt3qcrhpHSorXmrpu2g0gkIBVXAqCpzfoZIg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "ajv": "^6.9.1", + "chalk": "^2.1.0", + "cross-spawn": "^6.0.5", + "debug": "^4.0.1", + "doctrine": "^3.0.0", + "eslint-scope": "^4.0.3", + "eslint-utils": "^1.3.1", + "eslint-visitor-keys": "^1.0.0", + "espree": "^5.0.1", + "esquery": "^1.0.1", + "esutils": "^2.0.2", + "file-entry-cache": "^5.0.1", + "functional-red-black-tree": "^1.0.1", + "glob": "^7.1.2", + "globals": "^11.7.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "inquirer": "^6.2.2", + "js-yaml": "^3.13.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.3.0", + "lodash": "^4.17.11", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.1", + "natural-compare": "^1.4.0", + "optionator": "^0.8.2", + "path-is-inside": "^1.0.2", + "progress": "^2.0.0", + "regexpp": "^2.0.1", + "semver": "^5.5.1", + "strip-ansi": "^4.0.0", + "strip-json-comments": "^2.0.1", + "table": "^5.2.3", + "text-table": "^0.2.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "eslint-scope": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", + "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==", + "dev": true, + "requires": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "eslint-config-airbnb": { + "version": "17.1.1", + "resolved": "https://registry.npmjs.org/eslint-config-airbnb/-/eslint-config-airbnb-17.1.1.tgz", + "integrity": "sha512-xCu//8a/aWqagKljt+1/qAM62BYZeNq04HmdevG5yUGWpja0I/xhqd6GdLRch5oetEGFiJAnvtGuTEAese53Qg==", + "dev": true, + "requires": { + "eslint-config-airbnb-base": "^13.2.0", + "object.assign": "^4.1.0", + "object.entries": "^1.1.0" + } + }, + "eslint-config-airbnb-base": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-13.2.0.tgz", + "integrity": "sha512-1mg/7eoB4AUeB0X1c/ho4vb2gYkNH8Trr/EgCT/aGmKhhG+F6vF5s8+iRBlWAzFIAphxIdp3YfEKgEl0f9Xg+w==", + "dev": true, + "requires": { + "confusing-browser-globals": "^1.0.5", + "object.assign": "^4.1.0", + "object.entries": "^1.1.0" + } + }, + "eslint-import-resolver-node": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.3.tgz", + "integrity": "sha512-b8crLDo0M5RSe5YG8Pu2DYBj71tSB6OvXkfzwbJU2w7y8P4/yo0MyF8jU26IEuEuHF2K5/gcAJE3LhQGqBBbVg==", + "dev": true, + "requires": { + "debug": "^2.6.9", + "resolve": "^1.13.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + } + } + }, + "eslint-module-utils": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.5.2.tgz", + "integrity": "sha512-LGScZ/JSlqGKiT8OC+cYRxseMjyqt6QO54nl281CK93unD89ijSeRV6An8Ci/2nvWVKe8K/Tqdm75RQoIOCr+Q==", + "dev": true, + "requires": { + "debug": "^2.6.9", + "pkg-dir": "^2.0.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dev": true, + "requires": { + "locate-path": "^2.0.0" + } + }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "dev": true, + "requires": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + } + }, + "p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dev": true, + "requires": { + "p-try": "^1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "dev": true, + "requires": { + "p-limit": "^1.1.0" + } + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", + "dev": true + }, + "pkg-dir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz", + "integrity": "sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=", + "dev": true, + "requires": { + "find-up": "^2.1.0" + } + } + } + }, + "eslint-plugin-import": { + "version": "2.20.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.20.1.tgz", + "integrity": "sha512-qQHgFOTjguR+LnYRoToeZWT62XM55MBVXObHM6SKFd1VzDcX/vqT1kAz8ssqigh5eMj8qXcRoXXGZpPP6RfdCw==", + "dev": true, + "requires": { + "array-includes": "^3.0.3", + "array.prototype.flat": "^1.2.1", + "contains-path": "^0.1.0", + "debug": "^2.6.9", + "doctrine": "1.5.0", + "eslint-import-resolver-node": "^0.3.2", + "eslint-module-utils": "^2.4.1", + "has": "^1.0.3", + "minimatch": "^3.0.4", + "object.values": "^1.1.0", + "read-pkg-up": "^2.0.0", + "resolve": "^1.12.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "doctrine": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-1.5.0.tgz", + "integrity": "sha1-N53Ocw9hZvds76TmcHoVmwLFpvo=", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "isarray": "^1.0.0" + } + } + } + }, + "eslint-plugin-json": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-json/-/eslint-plugin-json-1.4.0.tgz", + "integrity": "sha512-CECvgRAWtUzuepdlPWd+VA7fhyF9HT183pZnl8wQw5x699Mk/MbME/q8xtULBfooi3LUbj6fToieNmsvUcDxWA==", + "dev": true, + "requires": { + "vscode-json-languageservice": "^3.2.1" + } + }, + "eslint-plugin-jsx-a11y": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.2.3.tgz", + "integrity": "sha512-CawzfGt9w83tyuVekn0GDPU9ytYtxyxyFZ3aSWROmnRRFQFT2BiPJd7jvRdzNDi6oLWaS2asMeYSNMjWTV4eNg==", + "dev": true, + "requires": { + "@babel/runtime": "^7.4.5", + "aria-query": "^3.0.0", + "array-includes": "^3.0.3", + "ast-types-flow": "^0.0.7", + "axobject-query": "^2.0.2", + "damerau-levenshtein": "^1.0.4", + "emoji-regex": "^7.0.2", + "has": "^1.0.3", + "jsx-ast-utils": "^2.2.1" + } + }, + "eslint-plugin-prettier": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.1.2.tgz", + "integrity": "sha512-GlolCC9y3XZfv3RQfwGew7NnuFDKsfI4lbvRK+PIIo23SFH+LemGs4cKwzAaRa+Mdb+lQO/STaIayno8T5sJJA==", + "dev": true, + "requires": { + "prettier-linter-helpers": "^1.0.0" + } + }, + "eslint-plugin-react": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.18.3.tgz", + "integrity": "sha512-Bt56LNHAQCoou88s8ViKRjMB2+36XRejCQ1VoLj716KI1MoE99HpTVvIThJ0rvFmG4E4Gsq+UgToEjn+j044Bg==", + "dev": true, + "requires": { + "array-includes": "^3.1.1", + "doctrine": "^2.1.0", + "has": "^1.0.3", + "jsx-ast-utils": "^2.2.3", + "object.entries": "^1.1.1", + "object.fromentries": "^2.0.2", + "object.values": "^1.1.1", + "prop-types": "^15.7.2", + "resolve": "^1.14.2", + "string.prototype.matchall": "^4.0.2" + }, + "dependencies": { + "doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + } + } + }, + "eslint-scope": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-3.7.1.tgz", + "integrity": "sha1-PWPD7f2gLgbgGkUq2IyqzHzctug=", + "dev": true, + "requires": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + } + }, + "eslint-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz", + "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" + } + }, + "eslint-visitor-keys": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz", + "integrity": "sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A==", + "dev": true + }, + "espree": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-5.0.1.tgz", + "integrity": "sha512-qWAZcWh4XE/RwzLJejfcofscgMc9CamR6Tn1+XRXNzrvUSSbiAjGOI/fggztjIi7y9VLPqnICMIPiGyr8JaZ0A==", + "dev": true, + "requires": { + "acorn": "^6.0.7", + "acorn-jsx": "^5.0.0", + "eslint-visitor-keys": "^1.0.0" + }, + "dependencies": { + "acorn": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.0.tgz", + "integrity": "sha512-gac8OEcQ2Li1dxIEWGZzsp2BitJxwkwcOm0zHAJLcPJaVvm58FRnk6RkuLRpU1EujipU2ZFODv2P9DLMfnV8mw==", + "dev": true + } + } + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + }, + "esquery": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.0.1.tgz", + "integrity": "sha512-SmiyZ5zIWH9VM+SRUReLS5Q8a7GxtRdxEBVZpm98rJM7Sb+A9DVCndXfkeFUd3byderg+EbDkfnevfCwynWaNA==", + "dev": true, + "requires": { + "estraverse": "^4.0.0" + } + }, + "esrecurse": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", + "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", + "dev": true, + "requires": { + "estraverse": "^4.1.0" + } + }, + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==" + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" + }, + "events": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.1.0.tgz", + "integrity": "sha512-Rv+u8MLHNOdMjTAFeT3nCjHn2aGlx435FP/sDHNaRhDEMwyI/aB22Kj2qIN8R0cw3z28psEQLYwxVKLsKrMgWg==", + "dev": true + }, + "evp_bytestokey": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", + "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", + "dev": true, + "requires": { + "md5.js": "^1.3.4", + "safe-buffer": "^5.1.1" + } + }, + "execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "dev": true, + "requires": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + } + }, + "exenv": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/exenv/-/exenv-1.2.2.tgz", + "integrity": "sha1-KueOhdmJQVhnCwPUe+wfA72Ru50=", + "dev": true + }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "dev": true, + "requires": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "express": { + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", + "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", + "requires": { + "accepts": "~1.3.7", + "array-flatten": "1.1.1", + "body-parser": "1.19.0", + "content-disposition": "0.5.3", + "content-type": "~1.0.4", + "cookie": "0.4.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.1.2", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.5", + "qs": "6.7.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.1.2", + "send": "0.17.1", + "serve-static": "1.14.1", + "setprototypeof": "1.1.1", + "statuses": "~1.5.0", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" + } + } + }, + "express-session": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.17.0.tgz", + "integrity": "sha512-t4oX2z7uoSqATbMfsxWMbNjAL0T5zpvcJCk3Z9wnPPN7ibddhnmDZXHfEcoBMG2ojKXZoCyPMc5FbtK+G7SoDg==", + "requires": { + "cookie": "0.4.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-headers": "~1.0.2", + "parseurl": "~1.3.3", + "safe-buffer": "5.2.0", + "uid-safe": "~2.1.5" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" + }, + "safe-buffer": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz", + "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==" + } + } + }, + "express-winston": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/express-winston/-/express-winston-2.6.0.tgz", + "integrity": "sha512-m4qvQrrIErAZFMQman8CKnQB8sgVG0dSp/wRFv1ZyoWPpP/6waDZywteAdjMF57uJ5+9O7tkwZb5k9w80ZyvAA==", + "requires": { + "chalk": "~0.4.0", + "lodash": "~4.17.5" + }, + "dependencies": { + "chalk": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-0.4.0.tgz", + "integrity": "sha1-UZmj3c0MHv4jvAjBsCewYXbgxk8=", + "requires": { + "ansi-styles": "~1.0.0", + "has-color": "~0.1.0", + "strip-ansi": "~0.1.0" + } + } + } + }, + "ext": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/ext/-/ext-1.4.0.tgz", + "integrity": "sha512-Key5NIsUxdqKg3vIsdw9dSuXpPCQ297y6wBjL30edxwPgt2E44WcWBZey/ZvUc6sERLTxKdyCu4gZFmUbk1Q7A==", + "dev": true, + "requires": { + "type": "^2.0.0" + }, + "dependencies": { + "type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/type/-/type-2.0.0.tgz", + "integrity": "sha512-KBt58xCHry4Cejnc2ISQAF7QY+ORngsWfxezO68+12hKV6lQY8P/psIkcbjeHWn7MqcgciWJyCCevFMJdIXpow==", + "dev": true + } + } + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true + }, + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "dev": true, + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dev": true, + "requires": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + } + }, + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "dev": true, + "requires": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + }, + "kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true + } + } + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", + "dev": true + }, + "eyes": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz", + "integrity": "sha1-Ys8SAjTGg3hdkCNIqADvPgzCC8A=" + }, + "fast-deep-equal": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz", + "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==", + "dev": true + }, + "fast-diff": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz", + "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==", + "dev": true + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "fbjs": { + "version": "0.8.17", + "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-0.8.17.tgz", + "integrity": "sha1-xNWY6taUkRJlPWWIsBpc3Nn5D90=", + "dev": true, + "requires": { + "core-js": "^1.0.0", + "isomorphic-fetch": "^2.1.1", + "loose-envify": "^1.0.0", + "object-assign": "^4.1.0", + "promise": "^7.1.1", + "setimmediate": "^1.0.5", + "ua-parser-js": "^0.7.18" + }, + "dependencies": { + "core-js": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz", + "integrity": "sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY=", + "dev": true + } + } + }, + "figgy-pudding": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.1.tgz", + "integrity": "sha512-vNKxJHTEKNThjfrdJwHc7brvM6eVevuO5nTj6ez8ZQ1qbXTvGthucRF7S4vf2cr71QVnT70V34v0S1DyQsti0w==", + "dev": true + }, + "figures": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", + "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.5" + } + }, + "file-entry-cache": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz", + "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==", + "dev": true, + "requires": { + "flat-cache": "^2.0.1" + } + }, + "file-loader": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-4.3.0.tgz", + "integrity": "sha512-aKrYPYjF1yG3oX0kWRrqrSMfgftm7oJW5M+m4owoldH5C51C0RkIwB++JbRvEW3IU6/ZG5n8UvEcdgwOt2UOWA==", + "dev": true, + "requires": { + "loader-utils": "^1.2.3", + "schema-utils": "^2.5.0" + } + }, + "file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "dev": true, + "optional": true + }, + "filesize": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/filesize/-/filesize-3.6.1.tgz", + "integrity": "sha512-7KjR1vv6qnicaPMi1iiTcI85CyYwRO/PSFCu6SvqL8jN2Wjt/NIYQTFtFs7fSDCYOstUkEWIQGFUg5YZQfjlcg==", + "dev": true + }, + "fill-keys": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/fill-keys/-/fill-keys-1.0.2.tgz", + "integrity": "sha1-mo+jb06K1jTjv2tPPIiCVRRS6yA=", + "dev": true, + "requires": { + "is-object": "~1.0.1", + "merge-descriptors": "~1.0.0" + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + } + } + }, + "find-cache-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", + "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", + "dev": true, + "requires": { + "commondir": "^1.0.1", + "make-dir": "^2.0.0", + "pkg-dir": "^3.0.0" + } + }, + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "finished": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/finished/-/finished-1.2.2.tgz", + "integrity": "sha1-QWCOr639ZWg7RqEiC8Sx7D2u3Ng=", + "requires": { + "ee-first": "1.0.3" + }, + "dependencies": { + "ee-first": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.0.3.tgz", + "integrity": "sha1-bJjECJq+y1p7hcGsRJqmA9Oz2r4=" + } + } + }, + "flat": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/flat/-/flat-4.1.0.tgz", + "integrity": "sha512-Px/TiLIznH7gEDlPXcUD4KnBusa6kR6ayRUVcnEAbreRIuhkqow/mun59BuRXwoYk7ZQOLW1ZM05ilIvK38hFw==", + "requires": { + "is-buffer": "~2.0.3" + } + }, + "flat-cache": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", + "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==", + "dev": true, + "requires": { + "flatted": "^2.0.0", + "rimraf": "2.6.3", + "write": "1.0.3" + }, + "dependencies": { + "rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } + } + }, + "flatted": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.1.tgz", + "integrity": "sha512-a1hQMktqW9Nmqr5aktAux3JMNqaucxGcjtjWnZLHX7yyPCmlSV3M54nGYbqT8K+0GhF3NBgmJCc3ma+WOgX8Jg==", + "dev": true + }, + "flush-write-stream": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz", + "integrity": "sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "readable-stream": "^2.3.6" + } + }, + "follow-redirects": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", + "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==", + "requires": { + "debug": "=3.1.0" + } + }, + "for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", + "dev": true + }, + "foreground-child": { + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-1.5.6.tgz", + "integrity": "sha1-T9ca0t/elnibmApcCilZN8svXOk=", + "dev": true, + "requires": { + "cross-spawn": "^4", + "signal-exit": "^3.0.0" + }, + "dependencies": { + "cross-spawn": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-4.0.2.tgz", + "integrity": "sha1-e5JHYhwjrf3ThWAEqCPL45dCTUE=", + "dev": true, + "requires": { + "lru-cache": "^4.0.1", + "which": "^1.2.9" + } + } + } + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", + "dev": true + }, + "form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, + "formatio": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/formatio/-/formatio-1.1.1.tgz", + "integrity": "sha1-XtPM1jZVEJc4NGXZlhmRAOhhYek=", + "dev": true, + "requires": { + "samsam": "~1.1" + } + }, + "formidable": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.1.tgz", + "integrity": "sha512-Fs9VRguL0gqGHkXS5GQiMCr1VhZBxz0JnJs4JmMp/2jL18Fmbzvv7vOFRU+U8TBkHEE/CX1qDXzJplVULgsLeg==", + "dev": true + }, + "forwarded": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", + "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" + }, + "fragment-cache": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", + "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", + "dev": true, + "requires": { + "map-cache": "^0.2.2" + } + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" + }, + "from2": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", + "integrity": "sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "readable-stream": "^2.0.0" + } + }, + "fs-minipass": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz", + "integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==", + "dev": true, + "requires": { + "minipass": "^2.6.0" + } + }, + "fs-readdir-recursive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz", + "integrity": "sha512-GNanXlVr2pf02+sPN40XN8HG+ePaNcvM0q5mZBd668Obwb0yD5GiUbZOFgwn8kGMY6I3mdyDJzieUy3PTYyTRA==", + "dev": true + }, + "fs-write-stream-atomic": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz", + "integrity": "sha1-tH31NJPvkR33VzHnCp3tAYnbQMk=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "iferr": "^0.1.5", + "imurmurhash": "^0.1.4", + "readable-stream": "1 || 2" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "fsevents": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.11.tgz", + "integrity": "sha512-+ux3lx6peh0BpvY0JebGyZoiR4D+oYzdPZMKJwkZ+sFkNJzpL7tXc/wehS49gUAxg3tmMHPHZkA8JU2rhhgDHw==", + "dev": true, + "optional": true, + "requires": { + "bindings": "^1.5.0", + "nan": "^2.12.1", + "node-pre-gyp": "*" + }, + "dependencies": { + "abbrev": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "ansi-regex": { + "version": "2.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "aproba": { + "version": "1.2.0", + "bundled": true, + "dev": true, + "optional": true + }, + "are-we-there-yet": { + "version": "1.1.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + } + }, + "balanced-match": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "brace-expansion": { + "version": "1.1.11", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "chownr": { + "version": "1.1.3", + "bundled": true, + "dev": true, + "optional": true + }, + "code-point-at": { + "version": "1.1.0", + "bundled": true, + "dev": true, + "optional": true + }, + "concat-map": { + "version": "0.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "console-control-strings": { + "version": "1.1.0", + "bundled": true, + "dev": true, + "optional": true + }, + "core-util-is": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "debug": { + "version": "3.2.6", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ms": "^2.1.1" + } + }, + "deep-extend": { + "version": "0.6.0", + "bundled": true, + "dev": true, + "optional": true + }, + "delegates": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "detect-libc": { + "version": "1.0.3", + "bundled": true, + "dev": true, + "optional": true + }, + "fs-minipass": { + "version": "1.2.7", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minipass": "^2.6.0" + } + }, + "fs.realpath": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "gauge": { + "version": "2.7.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + } + }, + "glob": { + "version": "7.1.6", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "has-unicode": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "iconv-lite": { + "version": "0.4.24", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ignore-walk": { + "version": "3.0.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minimatch": "^3.0.4" + } + }, + "inflight": { + "version": "1.0.6", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "bundled": true, + "dev": true, + "optional": true + }, + "ini": { + "version": "1.3.5", + "bundled": true, + "dev": true, + "optional": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "isarray": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "minimatch": { + "version": "3.0.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "bundled": true, + "dev": true, + "optional": true + }, + "minipass": { + "version": "2.9.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" + } + }, + "minizlib": { + "version": "1.3.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minipass": "^2.9.0" + } + }, + "mkdirp": { + "version": "0.5.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.1.2", + "bundled": true, + "dev": true, + "optional": true + }, + "needle": { + "version": "2.4.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "debug": "^3.2.6", + "iconv-lite": "^0.4.4", + "sax": "^1.2.4" + } + }, + "node-pre-gyp": { + "version": "0.14.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "detect-libc": "^1.0.2", + "mkdirp": "^0.5.1", + "needle": "^2.2.1", + "nopt": "^4.0.1", + "npm-packlist": "^1.1.6", + "npmlog": "^4.0.2", + "rc": "^1.2.7", + "rimraf": "^2.6.1", + "semver": "^5.3.0", + "tar": "^4.4.2" + } + }, + "nopt": { + "version": "4.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "abbrev": "1", + "osenv": "^0.1.4" + } + }, + "npm-bundled": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "npm-normalize-package-bin": "^1.0.1" + } + }, + "npm-normalize-package-bin": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "npm-packlist": { + "version": "1.4.7", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ignore-walk": "^3.0.1", + "npm-bundled": "^1.0.1" + } + }, + "npmlog": { + "version": "4.1.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "object-assign": { + "version": "4.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "once": { + "version": "1.4.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "wrappy": "1" + } + }, + "os-homedir": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "os-tmpdir": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "osenv": { + "version": "0.1.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "process-nextick-args": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "rc": { + "version": "1.2.8", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "readable-stream": { + "version": "2.3.6", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "rimraf": { + "version": "2.7.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "glob": "^7.1.3" + } + }, + "safe-buffer": { + "version": "5.1.2", + "bundled": true, + "dev": true, + "optional": true + }, + "safer-buffer": { + "version": "2.1.2", + "bundled": true, + "dev": true, + "optional": true + }, + "sax": { + "version": "1.2.4", + "bundled": true, + "dev": true, + "optional": true + }, + "semver": { + "version": "5.7.1", + "bundled": true, + "dev": true, + "optional": true + }, + "set-blocking": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "signal-exit": { + "version": "3.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "string-width": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "string_decoder": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "tar": { + "version": "4.4.13", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "chownr": "^1.1.1", + "fs-minipass": "^1.2.5", + "minipass": "^2.8.6", + "minizlib": "^1.2.1", + "mkdirp": "^0.5.0", + "safe-buffer": "^5.1.2", + "yallist": "^3.0.3" + } + }, + "util-deprecate": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "wide-align": { + "version": "1.1.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "string-width": "^1.0.2 || 2" + } + }, + "wrappy": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "yallist": { + "version": "3.1.1", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "function.prototype.name": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.2.tgz", + "integrity": "sha512-C8A+LlHBJjB2AdcRPorc5JvJ5VUoWlXdEHLOJdCI7kjHEtGTpHQUiqMvCIKUwIsGwZX2jZJy761AXsn356bJQg==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1", + "functions-have-names": "^1.2.0" + } + }, + "functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", + "dev": true + }, + "functions-have-names": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.1.tgz", + "integrity": "sha512-j48B/ZI7VKs3sgeI2cZp7WXWmZXu7Iq5pl5/vptV5N2mq+DGFuS/ulaDjtaoLpYzuD6u8UgrUKHfgo7fDTSiBA==", + "dev": true + }, + "gauge": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", + "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", + "dev": true, + "requires": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + }, + "dependencies": { + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + } + } + }, + "gensync": { + "version": "1.0.0-beta.1", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.1.tgz", + "integrity": "sha512-r8EC6NO1sngH/zdD9fiRDLdcgnbayXah+mLgManTaIZJqEC1MZstmnox8KpnI2/fxQwrp5OpCOYWLp4rBl4Jcg==", + "dev": true + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, + "get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", + "dev": true + }, + "get-stdin": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-7.0.0.tgz", + "integrity": "sha512-zRKcywvrXlXsA0v0i9Io4KDRaAw7+a1ZpjRwl9Wox8PFlVCCHra7E9c4kqXCoCM9nR5tBkaTTZRBoCm60bFqTQ==", + "dev": true + }, + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + }, + "get-value": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", + "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", + "dev": true + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0" + } + }, + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "dev": true, + "requires": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + }, + "dependencies": { + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "dev": true, + "requires": { + "is-extglob": "^2.1.0" + } + } + } + }, + "global": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/global/-/global-4.4.0.tgz", + "integrity": "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==", + "dev": true, + "requires": { + "min-document": "^2.19.0", + "process": "^0.11.10" + } + }, + "global-dirs": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-0.1.1.tgz", + "integrity": "sha1-sxnA3UYH81PzvpzKTHL8FIxJ9EU=", + "dev": true, + "requires": { + "ini": "^1.3.4" + } + }, + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true + }, + "google-protobuf": { + "version": "3.11.2", + "resolved": "https://registry.npmjs.org/google-protobuf/-/google-protobuf-3.11.2.tgz", + "integrity": "sha512-T4fin7lcYLUPj2ChUZ4DvfuuHtg3xi1621qeRZt2J7SvOQusOzq+sDT4vbotWTCjUXJoR36CA016LlhtPy80uQ==" + }, + "got": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/got/-/got-6.7.1.tgz", + "integrity": "sha1-JAzQV4WpoY5WHcG0S0HHY+8ejbA=", + "dev": true, + "requires": { + "create-error-class": "^3.0.0", + "duplexer3": "^0.1.4", + "get-stream": "^3.0.0", + "is-redirect": "^1.0.0", + "is-retry-allowed": "^1.0.0", + "is-stream": "^1.0.0", + "lowercase-keys": "^1.0.0", + "safe-buffer": "^5.0.1", + "timed-out": "^4.0.0", + "unzip-response": "^2.0.1", + "url-parse-lax": "^1.0.0" + }, + "dependencies": { + "get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", + "dev": true + } + } + }, + "graceful-fs": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz", + "integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==", + "dev": true + }, + "graphlib": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/graphlib/-/graphlib-2.1.8.tgz", + "integrity": "sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==", + "requires": { + "lodash": "^4.17.15" + } + }, + "growl": { + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", + "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", + "dev": true + }, + "grpc": { + "version": "1.24.2", + "resolved": "https://registry.npmjs.org/grpc/-/grpc-1.24.2.tgz", + "integrity": "sha512-EG3WH6AWMVvAiV15d+lr+K77HJ/KV/3FvMpjKjulXHbTwgDZkhkcWbwhxFAoTdxTkQvy0WFcO3Nog50QBbHZWw==", + "requires": { + "@types/bytebuffer": "^5.0.40", + "lodash.camelcase": "^4.3.0", + "lodash.clone": "^4.5.0", + "nan": "^2.13.2", + "node-pre-gyp": "^0.14.0", + "protobufjs": "^5.0.3" + }, + "dependencies": { + "abbrev": { + "version": "1.1.1", + "bundled": true + }, + "ansi-regex": { + "version": "2.1.1", + "bundled": true + }, + "aproba": { + "version": "1.2.0", + "bundled": true + }, + "are-we-there-yet": { + "version": "1.1.5", + "bundled": true, + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + } + }, + "balanced-match": { + "version": "1.0.0", + "bundled": true + }, + "brace-expansion": { + "version": "1.1.11", + "bundled": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "chownr": { + "version": "1.1.3", + "bundled": true + }, + "code-point-at": { + "version": "1.1.0", + "bundled": true + }, + "concat-map": { + "version": "0.0.1", + "bundled": true + }, + "console-control-strings": { + "version": "1.1.0", + "bundled": true + }, + "core-util-is": { + "version": "1.0.2", + "bundled": true + }, + "debug": { + "version": "3.2.6", + "bundled": true, + "requires": { + "ms": "^2.1.1" + } + }, + "deep-extend": { + "version": "0.6.0", + "bundled": true + }, + "delegates": { + "version": "1.0.0", + "bundled": true + }, + "detect-libc": { + "version": "1.0.3", + "bundled": true + }, + "fs-minipass": { + "version": "1.2.7", + "bundled": true, + "requires": { + "minipass": "^2.6.0" + } + }, + "fs.realpath": { + "version": "1.0.0", + "bundled": true + }, + "gauge": { + "version": "2.7.4", + "bundled": true, + "requires": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + } + }, + "glob": { + "version": "7.1.4", + "bundled": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "has-unicode": { + "version": "2.0.1", + "bundled": true + }, + "iconv-lite": { + "version": "0.4.24", + "bundled": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ignore-walk": { + "version": "3.0.3", + "bundled": true, + "requires": { + "minimatch": "^3.0.4" + } + }, + "inflight": { + "version": "1.0.6", + "bundled": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "bundled": true + }, + "ini": { + "version": "1.3.5", + "bundled": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "bundled": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "isarray": { + "version": "1.0.0", + "bundled": true + }, + "minimatch": { + "version": "3.0.4", + "bundled": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.0", + "bundled": true + }, + "minipass": { + "version": "2.9.0", + "bundled": true, + "requires": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" + } + }, + "minizlib": { + "version": "1.3.3", + "bundled": true, + "requires": { + "minipass": "^2.9.0" + } + }, + "mkdirp": { + "version": "0.5.1", + "bundled": true, + "requires": { + "minimist": "0.0.8" + }, + "dependencies": { + "minimist": { + "version": "0.0.8", + "bundled": true + } + } + }, + "ms": { + "version": "2.1.2", + "bundled": true + }, + "needle": { + "version": "2.4.0", + "bundled": true, + "requires": { + "debug": "^3.2.6", + "iconv-lite": "^0.4.4", + "sax": "^1.2.4" + } + }, + "node-pre-gyp": { + "version": "0.14.0", + "bundled": true, + "requires": { + "detect-libc": "^1.0.2", + "mkdirp": "^0.5.1", + "needle": "^2.2.1", + "nopt": "^4.0.1", + "npm-packlist": "^1.1.6", + "npmlog": "^4.0.2", + "rc": "^1.2.7", + "rimraf": "^2.6.1", + "semver": "^5.3.0", + "tar": "^4.4.2" + } + }, + "nopt": { + "version": "4.0.1", + "bundled": true, + "requires": { + "abbrev": "1", + "osenv": "^0.1.4" + } + }, + "npm-bundled": { + "version": "1.0.6", + "bundled": true + }, + "npm-packlist": { + "version": "1.4.6", + "bundled": true, + "requires": { + "ignore-walk": "^3.0.1", + "npm-bundled": "^1.0.1" + } + }, + "npmlog": { + "version": "4.1.2", + "bundled": true, + "requires": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "bundled": true + }, + "object-assign": { + "version": "4.1.1", + "bundled": true + }, + "once": { + "version": "1.4.0", + "bundled": true, + "requires": { + "wrappy": "1" + } + }, + "os-homedir": { + "version": "1.0.2", + "bundled": true + }, + "os-tmpdir": { + "version": "1.0.2", + "bundled": true + }, + "osenv": { + "version": "0.1.5", + "bundled": true, + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "bundled": true + }, + "process-nextick-args": { + "version": "2.0.1", + "bundled": true + }, + "rc": { + "version": "1.2.8", + "bundled": true, + "requires": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + } + }, + "readable-stream": { + "version": "2.3.6", + "bundled": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "rimraf": { + "version": "2.7.1", + "bundled": true, + "requires": { + "glob": "^7.1.3" + } + }, + "safe-buffer": { + "version": "5.1.2", + "bundled": true + }, + "safer-buffer": { + "version": "2.1.2", + "bundled": true + }, + "sax": { + "version": "1.2.4", + "bundled": true + }, + "semver": { + "version": "5.7.1", + "bundled": true + }, + "set-blocking": { + "version": "2.0.0", + "bundled": true + }, + "signal-exit": { + "version": "3.0.2", + "bundled": true + }, + "string-width": { + "version": "1.0.2", + "bundled": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "string_decoder": { + "version": "1.1.1", + "bundled": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "bundled": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "bundled": true + }, + "tar": { + "version": "4.4.13", + "bundled": true, + "requires": { + "chownr": "^1.1.1", + "fs-minipass": "^1.2.5", + "minipass": "^2.8.6", + "minizlib": "^1.2.1", + "mkdirp": "^0.5.0", + "safe-buffer": "^5.1.2", + "yallist": "^3.0.3" + } + }, + "util-deprecate": { + "version": "1.0.2", + "bundled": true + }, + "wide-align": { + "version": "1.1.3", + "bundled": true, + "requires": { + "string-width": "^1.0.2 || 2" + } + }, + "wrappy": { + "version": "1.0.2", + "bundled": true + }, + "yallist": { + "version": "3.1.1", + "bundled": true + } + } + }, + "grpc-tools": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/grpc-tools/-/grpc-tools-1.8.1.tgz", + "integrity": "sha512-CvZLshEDbum8ZtB8r3bn6JsrHs3L7S1jf7PTa02nZSLmcLTKbiXH5UYrte06Kh7SdzFmkxPMaOsys2rCs+HRjA==", + "dev": true, + "requires": { + "node-pre-gyp": "^0.12.0" + }, + "dependencies": { + "abbrev": { + "version": "1.1.1", + "bundled": true, + "dev": true + }, + "ansi-regex": { + "version": "2.1.1", + "bundled": true, + "dev": true + }, + "aproba": { + "version": "1.2.0", + "bundled": true, + "dev": true + }, + "are-we-there-yet": { + "version": "1.1.5", + "bundled": true, + "dev": true, + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + } + }, + "balanced-match": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "bundled": true, + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "chownr": { + "version": "1.1.1", + "bundled": true, + "dev": true + }, + "code-point-at": { + "version": "1.1.0", + "bundled": true, + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "bundled": true, + "dev": true + }, + "console-control-strings": { + "version": "1.1.0", + "bundled": true, + "dev": true + }, + "core-util-is": { + "version": "1.0.2", + "bundled": true, + "dev": true + }, + "debug": { + "version": "2.6.9", + "bundled": true, + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "deep-extend": { + "version": "0.6.0", + "bundled": true, + "dev": true + }, + "delegates": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "detect-libc": { + "version": "1.0.3", + "bundled": true, + "dev": true + }, + "fs-minipass": { + "version": "1.2.5", + "bundled": true, + "dev": true, + "requires": { + "minipass": "^2.2.1" + } + }, + "fs.realpath": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "gauge": { + "version": "2.7.4", + "bundled": true, + "dev": true, + "requires": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + } + }, + "glob": { + "version": "7.1.3", + "bundled": true, + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "has-unicode": { + "version": "2.0.1", + "bundled": true, + "dev": true + }, + "iconv-lite": { + "version": "0.4.24", + "bundled": true, + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ignore-walk": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "requires": { + "minimatch": "^3.0.4" + } + }, + "inflight": { + "version": "1.0.6", + "bundled": true, + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "bundled": true, + "dev": true + }, + "ini": { + "version": "1.3.5", + "bundled": true, + "dev": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "isarray": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "bundled": true, + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "bundled": true, + "dev": true + }, + "minipass": { + "version": "2.3.5", + "bundled": true, + "dev": true, + "requires": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" + } + }, + "minizlib": { + "version": "1.2.1", + "bundled": true, + "dev": true, + "requires": { + "minipass": "^2.2.1" + } + }, + "mkdirp": { + "version": "0.5.1", + "bundled": true, + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.0.0", + "bundled": true, + "dev": true + }, + "needle": { + "version": "2.2.4", + "bundled": true, + "dev": true, + "requires": { + "debug": "^2.1.2", + "iconv-lite": "^0.4.4", + "sax": "^1.2.4" + } + }, + "node-pre-gyp": { + "version": "0.12.0", + "bundled": true, + "dev": true, + "requires": { + "detect-libc": "^1.0.2", + "mkdirp": "^0.5.1", + "needle": "^2.2.1", + "nopt": "^4.0.1", + "npm-packlist": "^1.1.6", + "npmlog": "^4.0.2", + "rc": "^1.2.7", + "rimraf": "^2.6.1", + "semver": "^5.3.0", + "tar": "^4" + } + }, + "nopt": { + "version": "4.0.1", + "bundled": true, + "dev": true, + "requires": { + "abbrev": "1", + "osenv": "^0.1.4" + } + }, + "npm-bundled": { + "version": "1.0.6", + "bundled": true, + "dev": true + }, + "npm-packlist": { + "version": "1.4.1", + "bundled": true, + "dev": true, + "requires": { + "ignore-walk": "^3.0.1", + "npm-bundled": "^1.0.1" + } + }, + "npmlog": { + "version": "4.1.2", + "bundled": true, + "dev": true, + "requires": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "bundled": true, + "dev": true + }, + "object-assign": { + "version": "4.1.1", + "bundled": true, + "dev": true + }, + "once": { + "version": "1.4.0", + "bundled": true, + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "os-homedir": { + "version": "1.0.2", + "bundled": true, + "dev": true + }, + "os-tmpdir": { + "version": "1.0.2", + "bundled": true, + "dev": true + }, + "osenv": { + "version": "0.1.5", + "bundled": true, + "dev": true, + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "bundled": true, + "dev": true + }, + "process-nextick-args": { + "version": "2.0.0", + "bundled": true, + "dev": true + }, + "rc": { + "version": "1.2.8", + "bundled": true, + "dev": true, + "requires": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "bundled": true, + "dev": true + } + } + }, + "readable-stream": { + "version": "2.3.6", + "bundled": true, + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "rimraf": { + "version": "2.6.3", + "bundled": true, + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "safe-buffer": { + "version": "5.1.2", + "bundled": true, + "dev": true + }, + "safer-buffer": { + "version": "2.1.2", + "bundled": true, + "dev": true + }, + "sax": { + "version": "1.2.4", + "bundled": true, + "dev": true + }, + "semver": { + "version": "5.6.0", + "bundled": true, + "dev": true + }, + "set-blocking": { + "version": "2.0.0", + "bundled": true, + "dev": true + }, + "signal-exit": { + "version": "3.0.2", + "bundled": true, + "dev": true + }, + "string-width": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "string_decoder": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "bundled": true, + "dev": true + }, + "tar": { + "version": "4.4.8", + "bundled": true, + "dev": true, + "requires": { + "chownr": "^1.1.1", + "fs-minipass": "^1.2.5", + "minipass": "^2.3.4", + "minizlib": "^1.1.1", + "mkdirp": "^0.5.0", + "safe-buffer": "^5.1.2", + "yallist": "^3.0.2" + } + }, + "util-deprecate": { + "version": "1.0.2", + "bundled": true, + "dev": true + }, + "wide-align": { + "version": "1.1.3", + "bundled": true, + "dev": true, + "requires": { + "string-width": "^1.0.2 || 2" + } + }, + "wrappy": { + "version": "1.0.2", + "bundled": true, + "dev": true + }, + "yallist": { + "version": "3.0.3", + "bundled": true, + "dev": true + } + } + }, + "gzip-size": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-5.1.1.tgz", + "integrity": "sha512-FNHi6mmoHvs1mxZAds4PpdCS6QG8B4C1krxJsMutgxl5t3+GlRTzzI3NEkifXx2pVsOvJdOGSmIgDhQ55FwdPA==", + "dev": true, + "requires": { + "duplexer": "^0.1.1", + "pify": "^4.0.1" + } + }, + "hammerjs": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/hammerjs/-/hammerjs-2.0.8.tgz", + "integrity": "sha1-BO93hiz/K7edMPdpIJWTAiK/YPE=", + "dev": true + }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", + "dev": true + }, + "har-validator": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", + "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", + "dev": true, + "requires": { + "ajv": "^6.5.5", + "har-schema": "^2.0.0" + } + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "has-color": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/has-color/-/has-color-0.1.7.tgz", + "integrity": "sha1-ZxRKUmDDT8PMpnfQQdr1L+e3iy8=" + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "has-symbols": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", + "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", + "dev": true + }, + "has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", + "dev": true + }, + "has-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", + "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", + "dev": true, + "requires": { + "get-value": "^2.0.6", + "has-values": "^1.0.0", + "isobject": "^3.0.0" + } + }, + "has-values": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", + "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "kind-of": "^4.0.0" + }, + "dependencies": { + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true + }, + "kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "hash-base": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz", + "integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, + "hasha": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hasha/-/hasha-3.0.0.tgz", + "integrity": "sha1-UqMvq4Vp1BymmmH/GiFPjrfIvTk=", + "dev": true, + "requires": { + "is-stream": "^1.0.1" + } + }, + "he": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", + "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", + "dev": true + }, + "history": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz", + "integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==", + "dev": true, + "requires": { + "@babel/runtime": "^7.1.2", + "loose-envify": "^1.2.0", + "resolve-pathname": "^3.0.0", + "tiny-invariant": "^1.0.2", + "tiny-warning": "^1.0.0", + "value-equal": "^1.0.1" + } + }, + "hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", + "dev": true, + "requires": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "hoek": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.1.tgz", + "integrity": "sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA==", + "dev": true + }, + "hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "dev": true, + "requires": { + "react-is": "^16.7.0" + } + }, + "hoopy": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz", + "integrity": "sha512-HRcs+2mr52W0K+x8RzcLzuPPmVIKMSv97RGHy0Ea9y/mpcaK+xTrjICA04KAHi4GRzxliNqNJEFYWHghy3rSfQ==", + "dev": true + }, + "hosted-git-info": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.5.tgz", + "integrity": "sha512-kssjab8CvdXfcXMXVcvsXum4Hwdq9XGtRD3TteMEvEbq0LXyiNQr6AprqKqfeaDXze7SxWvRxdpwE6ku7ikLkg==", + "dev": true + }, + "html-element-map": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/html-element-map/-/html-element-map-1.2.0.tgz", + "integrity": "sha512-0uXq8HsuG1v2TmQ8QkIhzbrqeskE4kn52Q18QJ9iAA/SnHoEKXWiUxHQtclRsCFWEUD2So34X+0+pZZu862nnw==", + "dev": true, + "requires": { + "array-filter": "^1.0.0" + } + }, + "html-encoding-sniffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz", + "integrity": "sha512-71lZziiDnsuabfdYiUeWdCVyKuqwWi23L8YeIgV9jSSZHCtb6wB1BKWooH7L3tn4/FuZJMVWyNaIDr4RGmaSYw==", + "dev": true, + "requires": { + "whatwg-encoding": "^1.0.1" + } + }, + "html-escaper": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.0.tgz", + "integrity": "sha512-a4u9BeERWGu/S8JiWEAQcdrg9v4QArtP9keViQjGMdff20fBdd8waotXaNmODqBe6uZ3Nafi7K/ho4gCQHV3Ig==", + "dev": true + }, + "htmlparser2": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz", + "integrity": "sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==", + "dev": true, + "requires": { + "domelementtype": "^1.3.1", + "domhandler": "^2.3.0", + "domutils": "^1.5.1", + "entities": "^1.1.1", + "inherits": "^2.0.1", + "readable-stream": "^3.1.1" + }, + "dependencies": { + "readable-stream": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.5.0.tgz", + "integrity": "sha512-gSz026xs2LfxBPudDuI41V1lka8cxg64E66SGe78zJlsUofOg/yqwezdIcdfwik6B4h8LFmWPA9ef9X3FiNFLA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } + } + }, + "http-errors": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", + "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + } + }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, + "https": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/https/-/https-1.0.0.tgz", + "integrity": "sha1-PDfHrhqO65ZpBKKtHpdaGUt+06Q=" + }, + "https-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", + "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=", + "dev": true + }, + "husky": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/husky/-/husky-2.7.0.tgz", + "integrity": "sha512-LIi8zzT6PyFpcYKdvWRCn/8X+6SuG2TgYYMrM6ckEYhlp44UcEduVymZGIZNLiwOUjrEud+78w/AsAiqJA/kRg==", + "dev": true, + "requires": { + "cosmiconfig": "^5.2.0", + "execa": "^1.0.0", + "find-up": "^3.0.0", + "get-stdin": "^7.0.0", + "is-ci": "^2.0.0", + "pkg-dir": "^4.1.0", + "please-upgrade-node": "^3.1.1", + "read-pkg": "^5.1.1", + "run-node": "^1.0.0", + "slash": "^3.0.0" + }, + "dependencies": { + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "parse-json": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.0.0.tgz", + "integrity": "sha512-OOY5b7PAEFV0E2Fir1KOkxchnZNCdowAJgQ5NuxjpBKTRP3pQhwkrkxqQjeoKJ+fO7bCpmIZaogI4eZGDMEGOw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1", + "lines-and-columns": "^1.1.6" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "requires": { + "find-up": "^4.0.0" + }, + "dependencies": { + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + } + } + }, + "read-pkg": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", + "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", + "dev": true, + "requires": { + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^2.5.0", + "parse-json": "^5.0.0", + "type-fest": "^0.6.0" + } + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + } + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "icss-utils": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-4.1.1.tgz", + "integrity": "sha512-4aFq7wvWyMHKgxsH8QQtGpvbASCf+eM3wPRLI6R+MgAnTCZ6STYsRvttLvRWK0Nfif5piF394St3HeJDaljGPA==", + "dev": true, + "requires": { + "postcss": "^7.0.14" + } + }, + "ieee754": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==", + "dev": true + }, + "iferr": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/iferr/-/iferr-0.1.5.tgz", + "integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=", + "dev": true + }, + "ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true + }, + "ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha1-SMptcvbGo68Aqa1K5odr44ieKwk=", + "dev": true + }, + "ignore-styles": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ignore-styles/-/ignore-styles-5.0.1.tgz", + "integrity": "sha1-tJ7yJ0va/NikiAqWa/440aC/RnE=", + "dev": true + }, + "ignore-walk": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.3.tgz", + "integrity": "sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw==", + "dev": true, + "requires": { + "minimatch": "^3.0.4" + } + }, + "image-size": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", + "integrity": "sha1-Cd/Uq50g4p6xw+gLiZA3jfnjy5w=", + "dev": true, + "optional": true + }, + "import-fresh": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz", + "integrity": "sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, + "import-lazy": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz", + "integrity": "sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM=", + "dev": true + }, + "import-local": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-1.0.0.tgz", + "integrity": "sha512-vAaZHieK9qjGo58agRBg+bhHX3hoTZU/Oa3GESWLz7t1U62fk63aHuDJJEteXoDeTCcPmUT+z38gkHPZkkmpmQ==", + "dev": true, + "requires": { + "pkg-dir": "^2.0.0", + "resolve-cwd": "^2.0.0" + }, + "dependencies": { + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dev": true, + "requires": { + "locate-path": "^2.0.0" + } + }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "dev": true, + "requires": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + } + }, + "p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dev": true, + "requires": { + "p-try": "^1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "dev": true, + "requires": { + "p-limit": "^1.1.0" + } + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", + "dev": true + }, + "pkg-dir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz", + "integrity": "sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=", + "dev": true, + "requires": { + "find-up": "^2.1.0" + } + } + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true + }, + "indent-string": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-3.2.0.tgz", + "integrity": "sha1-Sl/W0nzDMvN+VBmlBNu4NxBckok=", + "dev": true + }, + "indexes-of": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/indexes-of/-/indexes-of-1.0.1.tgz", + "integrity": "sha1-8w9xbI4r00bHtn0985FVZqfAVgc=", + "dev": true + }, + "infer-owner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "ini": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", + "dev": true + }, + "inquirer": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.5.2.tgz", + "integrity": "sha512-cntlB5ghuB0iuO65Ovoi8ogLHiWGs/5yNrtUcKjFhSSiVeAIVpD7koaSU9RM8mpXw5YDi9RdYXGQMaOURB7ycQ==", + "dev": true, + "requires": { + "ansi-escapes": "^3.2.0", + "chalk": "^2.4.2", + "cli-cursor": "^2.1.0", + "cli-width": "^2.0.0", + "external-editor": "^3.0.3", + "figures": "^2.0.0", + "lodash": "^4.17.12", + "mute-stream": "0.0.7", + "run-async": "^2.2.0", + "rxjs": "^6.4.0", + "string-width": "^2.1.0", + "strip-ansi": "^5.1.0", + "through": "^2.3.6" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + }, + "dependencies": { + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + } + } + } + } + }, + "internal-slot": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.2.tgz", + "integrity": "sha512-2cQNfwhAfJIkU4KZPkDI+Gj5yNNnbqi40W9Gge6dfnk4TocEVm00B3bdiL+JINrbGJil2TeHvM4rETGzk/f/0g==", + "dev": true, + "requires": { + "es-abstract": "^1.17.0-next.1", + "has": "^1.0.3", + "side-channel": "^1.0.2" + } + }, + "invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "dev": true, + "requires": { + "loose-envify": "^1.0.0" + } + }, + "invert-kv": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", + "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=" + }, + "ipaddr.js": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.0.tgz", + "integrity": "sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA==" + }, + "irregular-plurals": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/irregular-plurals/-/irregular-plurals-2.0.0.tgz", + "integrity": "sha512-Y75zBYLkh0lJ9qxeHlMjQ7bSbyiSqNW/UOPWDmzC7cXskL1hekSITh1Oc6JV0XCWWZ9DE8VYSB71xocLk3gmGw==", + "dev": true + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + } + }, + "is-arguments": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.0.4.tgz", + "integrity": "sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA==", + "dev": true + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "dev": true + }, + "is-binary-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", + "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", + "dev": true, + "requires": { + "binary-extensions": "^1.0.0" + } + }, + "is-boolean-object": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.0.1.tgz", + "integrity": "sha512-TqZuVwa/sppcrhUCAYkGBk7w0yxfQQnxq28fjkO53tnK9FQXmdwz2JS5+GjsWQ6RByES1K40nI+yDic5c9/aAQ==", + "dev": true + }, + "is-buffer": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.4.tgz", + "integrity": "sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A==" + }, + "is-callable": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.5.tgz", + "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==", + "dev": true + }, + "is-ci": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", + "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", + "dev": true, + "requires": { + "ci-info": "^2.0.0" + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + } + }, + "is-date-object": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz", + "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==", + "dev": true + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + }, + "dependencies": { + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "is-directory": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz", + "integrity": "sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=", + "dev": true + }, + "is-expression": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-expression/-/is-expression-3.0.0.tgz", + "integrity": "sha1-Oayqa+f9HzRx3ELHQW5hwkMXrJ8=", + "requires": { + "acorn": "~4.0.2", + "object-assign": "^4.0.1" + }, + "dependencies": { + "acorn": { + "version": "4.0.13", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-4.0.13.tgz", + "integrity": "sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c=" + } + } + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", + "dev": true + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "is-generator-function": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.7.tgz", + "integrity": "sha512-YZc5EwyO4f2kWCax7oegfuSr9mFz1ZvieNYBEjmukLxgXfBUbxAWGVF7GZf0zidYtoBl3WvC07YK0wT76a+Rtw==", + "dev": true + }, + "is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-installed-globally": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.1.0.tgz", + "integrity": "sha1-Df2Y9akRFxbdU13aZJL2e/PSWoA=", + "dev": true, + "requires": { + "global-dirs": "^0.1.0", + "is-path-inside": "^1.0.0" + } + }, + "is-npm": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-1.0.0.tgz", + "integrity": "sha1-8vtjpl5JBbQGyGBydloaTceTufQ=", + "dev": true + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + } + }, + "is-number-object": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.4.tgz", + "integrity": "sha512-zohwelOAur+5uXtk8O3GPQ1eAcu4ZX3UwxQhUlfFFMNpUd83gXgjbhJh6HmB6LUNV/ieOLQuDwJO3dWJosUeMw==", + "dev": true + }, + "is-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=", + "dev": true + }, + "is-object": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-object/-/is-object-1.0.1.tgz", + "integrity": "sha1-iVJojF7C/9awPsyF52ngKQMINHA=", + "dev": true + }, + "is-path-inside": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz", + "integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=", + "dev": true, + "requires": { + "path-is-inside": "^1.0.1" + } + }, + "is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=", + "dev": true + }, + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "requires": { + "isobject": "^3.0.1" + } + }, + "is-promise": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", + "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=" + }, + "is-redirect": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-redirect/-/is-redirect-1.0.0.tgz", + "integrity": "sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ=", + "dev": true + }, + "is-regex": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz", + "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==", + "requires": { + "has": "^1.0.3" + } + }, + "is-retry-allowed": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz", + "integrity": "sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg==", + "dev": true + }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", + "dev": true + }, + "is-string": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.5.tgz", + "integrity": "sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ==", + "dev": true + }, + "is-subset": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-subset/-/is-subset-0.1.1.tgz", + "integrity": "sha1-ilkRfZMt4d4A8kX83TnOQ/HpOaY=", + "dev": true + }, + "is-symbol": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", + "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==", + "dev": true, + "requires": { + "has-symbols": "^1.0.1" + } + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "dev": true + }, + "is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true + }, + "is-wsl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", + "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=", + "dev": true + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "isemail": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/isemail/-/isemail-2.2.1.tgz", + "integrity": "sha1-A1PT2aYpUQgMJiwqoKQrjqjp4qY=", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "isomorphic-fetch": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz", + "integrity": "sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=", + "dev": true, + "requires": { + "node-fetch": "^1.0.1", + "whatwg-fetch": ">=0.10.0" + } + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + }, + "istanbul-lib-coverage": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz", + "integrity": "sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA==", + "dev": true + }, + "istanbul-lib-hook": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-2.0.7.tgz", + "integrity": "sha512-vrRztU9VRRFDyC+aklfLoeXyNdTfga2EI3udDGn4cZ6fpSXpHLV9X6CHvfoMCPtggg8zvDDmC4b9xfu0z6/llA==", + "dev": true, + "requires": { + "append-transform": "^1.0.0" + } + }, + "istanbul-lib-instrument": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-3.3.0.tgz", + "integrity": "sha512-5nnIN4vo5xQZHdXno/YDXJ0G+I3dAm4XgzfSVTPLQpj/zAV2dV6Juy0yaf10/zrJOJeHoN3fraFe+XRq2bFVZA==", + "dev": true, + "requires": { + "@babel/generator": "^7.4.0", + "@babel/parser": "^7.4.3", + "@babel/template": "^7.4.0", + "@babel/traverse": "^7.4.3", + "@babel/types": "^7.4.0", + "istanbul-lib-coverage": "^2.0.5", + "semver": "^6.0.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "istanbul-lib-report": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-2.0.8.tgz", + "integrity": "sha512-fHBeG573EIihhAblwgxrSenp0Dby6tJMFR/HvlerBsrCTD5bkUuoNtn3gVh29ZCS824cGGBPn7Sg7cNk+2xUsQ==", + "dev": true, + "requires": { + "istanbul-lib-coverage": "^2.0.5", + "make-dir": "^2.1.0", + "supports-color": "^6.1.0" + }, + "dependencies": { + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "istanbul-lib-source-maps": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-3.0.6.tgz", + "integrity": "sha512-R47KzMtDJH6X4/YW9XTx+jrLnZnscW4VpNN+1PViSYTejLVPWv7oov+Duf8YQSPyVRUvueQqz1TcsC6mooZTXw==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^2.0.5", + "make-dir": "^2.1.0", + "rimraf": "^2.6.3", + "source-map": "^0.6.1" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "istanbul-reports": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-2.2.7.tgz", + "integrity": "sha512-uu1F/L1o5Y6LzPVSVZXNOoD/KXpJue9aeLRd0sM9uMXfZvzomB0WxVamWb5ue8kA2vVWEmW7EG+A5n3f1kqHKg==", + "dev": true, + "requires": { + "html-escaper": "^2.0.0" + } + }, + "items": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/items/-/items-2.1.2.tgz", + "integrity": "sha512-kezcEqgB97BGeZZYtX/MA8AG410ptURstvnz5RAgyFZ8wQFPMxHY8GpTq+/ZHKT3frSlIthUq7EvLt9xn3TvXg==", + "dev": true + }, + "joi": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/joi/-/joi-9.2.0.tgz", + "integrity": "sha1-M4WseQGSEwy+Iw6ALsAskhW7/to=", + "dev": true, + "requires": { + "hoek": "4.x.x", + "isemail": "2.x.x", + "items": "2.x.x", + "moment": "2.x.x", + "topo": "2.x.x" + } + }, + "js-cookie": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.1.tgz", + "integrity": "sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==", + "dev": true + }, + "js-stringify": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/js-stringify/-/js-stringify-1.0.2.tgz", + "integrity": "sha1-Fzb939lyTyijaCrcYjCufk6Weds=" + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "js-yaml": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", + "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "dev": true + }, + "jsdom": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-11.12.0.tgz", + "integrity": "sha512-y8Px43oyiBM13Zc1z780FrfNLJCXTL40EWlty/LXUtcjykRBNgLlCjWXpfSPBl2iv+N7koQN+dvqszHZgT/Fjw==", + "dev": true, + "requires": { + "abab": "^2.0.0", + "acorn": "^5.5.3", + "acorn-globals": "^4.1.0", + "array-equal": "^1.0.0", + "cssom": ">= 0.3.2 < 0.4.0", + "cssstyle": "^1.0.0", + "data-urls": "^1.0.0", + "domexception": "^1.0.1", + "escodegen": "^1.9.1", + "html-encoding-sniffer": "^1.0.2", + "left-pad": "^1.3.0", + "nwsapi": "^2.0.7", + "parse5": "4.0.0", + "pn": "^1.1.0", + "request": "^2.87.0", + "request-promise-native": "^1.0.5", + "sax": "^1.2.4", + "symbol-tree": "^3.2.2", + "tough-cookie": "^2.3.4", + "w3c-hr-time": "^1.0.1", + "webidl-conversions": "^4.0.2", + "whatwg-encoding": "^1.0.3", + "whatwg-mimetype": "^2.1.0", + "whatwg-url": "^6.4.1", + "ws": "^5.2.0", + "xml-name-validator": "^3.0.0" + }, + "dependencies": { + "acorn": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.3.tgz", + "integrity": "sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw==", + "dev": true + }, + "acorn-globals": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-4.3.4.tgz", + "integrity": "sha512-clfQEh21R+D0leSbUdWf3OcfqyaCSAQ8Ryq00bofSekfr9W8u1jyYZo6ir0xu9Gtcf7BjcHJpnbZH7JOCpP60A==", + "dev": true, + "requires": { + "acorn": "^6.0.1", + "acorn-walk": "^6.0.1" + }, + "dependencies": { + "acorn": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.0.tgz", + "integrity": "sha512-gac8OEcQ2Li1dxIEWGZzsp2BitJxwkwcOm0zHAJLcPJaVvm58FRnk6RkuLRpU1EujipU2ZFODv2P9DLMfnV8mw==", + "dev": true + } + } + }, + "parse5": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-4.0.0.tgz", + "integrity": "sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA==", + "dev": true + }, + "whatwg-url": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-6.5.0.tgz", + "integrity": "sha512-rhRZRqx/TLJQWUpQ6bmrt2UV4f0HCQ463yQuONJqC6fO2VoEb1pTYddbe59SkYq87aoM5A3bdhMZiUiVws+fzQ==", + "dev": true, + "requires": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } + } + } + }, + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true + }, + "json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "dev": true + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", + "dev": true + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", + "dev": true + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", + "dev": true + }, + "json5": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.1.tgz", + "integrity": "sha512-l+3HXD0GEI3huGq1njuqtzYK8OYJyXMkOLtQ53pjWh89tvWS2h6l+1zMkYWqlb57+SiQodKZyvMEFb2X+KrFhQ==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + }, + "jsonc-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-2.2.0.tgz", + "integrity": "sha512-4fLQxW1j/5fWj6p78vAlAafoCKtuBm6ghv+Ij5W2DrDx0qE+ZdEl2c6Ko1mgJNF5ftX1iEWQQ4Ap7+3GlhjkOA==", + "dev": true + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "dev": true, + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, + "jstransformer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/jstransformer/-/jstransformer-1.0.0.tgz", + "integrity": "sha1-7Yvwkh4vPx7U1cGkT2hwntJHIsM=", + "requires": { + "is-promise": "^2.0.0", + "promise": "^7.0.1" + } + }, + "jsx-ast-utils": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-2.2.3.tgz", + "integrity": "sha512-EdIHFMm+1BPynpKOpdPqiOsvnIrInRGJD7bzPZdPkjitQEqpdpUuFpq4T0npZFKTiB3RhWFdGN+oqOJIdhDhQA==", + "dev": true, + "requires": { + "array-includes": "^3.0.3", + "object.assign": "^4.1.0" + } + }, + "keycharm": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/keycharm/-/keycharm-0.2.0.tgz", + "integrity": "sha1-+m6i5DuQpoAohD0n8gddNajD5vk=", + "dev": true + }, + "keygrip": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz", + "integrity": "sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==", + "requires": { + "tsscmp": "1.0.6" + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + }, + "dependencies": { + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + } + } + }, + "latest-version": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-3.1.0.tgz", + "integrity": "sha1-ogU4P+oyKzO1rjsYq+4NwvNW7hU=", + "dev": true, + "requires": { + "package-json": "^4.0.0" + } + }, + "lazy-cache": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", + "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=" + }, + "lcid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", + "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", + "requires": { + "invert-kv": "^1.0.0" + } + }, + "lcov-parse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lcov-parse/-/lcov-parse-1.0.0.tgz", + "integrity": "sha1-6w1GtUER68VhrLTECO+TY73I9+A=", + "dev": true + }, + "left-pad": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz", + "integrity": "sha512-XI5MPzVNApjAyhQzphX8BkmKsKUxD4LdyK24iZeQGinBN9yTQT3bFlCBy/aVx2HrNcqQGsdot8ghrjyrvMCoEA==", + "dev": true + }, + "less": { + "version": "3.10.3", + "resolved": "https://registry.npmjs.org/less/-/less-3.10.3.tgz", + "integrity": "sha512-vz32vqfgmoxF1h3K4J+yKCtajH0PWmjkIFgbs5d78E/c/e+UQTnI+lWK+1eQRE95PXM2mC3rJlLSSP9VQHnaow==", + "dev": true, + "requires": { + "clone": "^2.1.2", + "errno": "^0.1.1", + "graceful-fs": "^4.1.2", + "image-size": "~0.5.0", + "mime": "^1.4.1", + "mkdirp": "^0.5.0", + "promise": "^7.1.1", + "request": "^2.83.0", + "source-map": "~0.6.0" + } + }, + "less-loader": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/less-loader/-/less-loader-4.1.0.tgz", + "integrity": "sha512-KNTsgCE9tMOM70+ddxp9yyt9iHqgmSs0yTZc5XH5Wo+g80RWRIYNqE58QJKm/yMud5wZEvz50ugRDuzVIkyahg==", + "dev": true, + "requires": { + "clone": "^2.1.1", + "loader-utils": "^1.1.0", + "pify": "^3.0.0" + }, + "dependencies": { + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + } + } + }, + "leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true + }, + "levenary": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/levenary/-/levenary-1.1.1.tgz", + "integrity": "sha512-mkAdOIt79FD6irqjYSs4rdbnlT5vRonMEvBVPVb3XmevfS8kgRXwfes0dhPdEtzTWD/1eNE/Bm/G1iRt6DcnQQ==", + "dev": true, + "requires": { + "leven": "^3.1.0" + } + }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } + }, + "lines-and-columns": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz", + "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=", + "dev": true + }, + "load-json-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0", + "strip-bom": "^3.0.0" + }, + "dependencies": { + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + } + } + }, + "loader-runner": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.4.0.tgz", + "integrity": "sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw==", + "dev": true + }, + "loader-utils": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.2.3.tgz", + "integrity": "sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==", + "dev": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^2.0.0", + "json5": "^1.0.1" + }, + "dependencies": { + "json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + } + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" + }, + "lodash-webpack-plugin": { + "version": "0.11.5", + "resolved": "https://registry.npmjs.org/lodash-webpack-plugin/-/lodash-webpack-plugin-0.11.5.tgz", + "integrity": "sha512-QWfEIYxpixOdbd6KBe5g6MDWcyTgP3trDXwKHFqTlXrWiLcs/67fGQ0IWeRyhWlTITQIgMpJAYd2oeIztuV5VA==", + "dev": true, + "requires": { + "lodash": "^4.17.4" + } + }, + "lodash.assign": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz", + "integrity": "sha1-DZnzzNem0mHRm9rrkkUAXShYCOc=", + "dev": true + }, + "lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=" + }, + "lodash.clone": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clone/-/lodash.clone-4.5.0.tgz", + "integrity": "sha1-GVhwRQ9aExkkeN9Lw9I9LeoZB7Y=" + }, + "lodash.escape": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.escape/-/lodash.escape-4.0.1.tgz", + "integrity": "sha1-yQRGkMIeBClL6qUXcS/e0fqI3pg=", + "dev": true + }, + "lodash.flattendeep": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", + "integrity": "sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=", + "dev": true + }, + "lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=", + "dev": true + }, + "lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=", + "dev": true + }, + "log-driver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/log-driver/-/log-driver-1.2.7.tgz", + "integrity": "sha512-U7KCmLdqsGHBLeWqYlFA0V0Sl6P08EE1ZrmA9cxjUE0WVqT9qnyVDPz1kzpFEP0jdJuFnasWIfSd7fsaNXkpbg==", + "dev": true + }, + "log-symbols": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", + "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", + "dev": true, + "requires": { + "chalk": "^2.0.1" + } + }, + "loglevelnext": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/loglevelnext/-/loglevelnext-1.0.5.tgz", + "integrity": "sha512-V/73qkPuJmx4BcBF19xPBr+0ZRVBhc4POxvZTZdMeXpJ4NItXSJ/MSwuFT0kQJlCbXvdlZoQQ/418bS1y9Jh6A==", + "dev": true, + "requires": { + "es6-symbol": "^3.1.1", + "object.assign": "^4.1.0" + } + }, + "lolex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/lolex/-/lolex-1.3.2.tgz", + "integrity": "sha1-fD2mL/yzDw9agKJWbKJORdigHzE=", + "dev": true + }, + "long": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/long/-/long-3.2.0.tgz", + "integrity": "sha1-2CG3E4yhy1gcFymQ7xTbIAtcR0s=" + }, + "longest": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", + "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=" + }, + "loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "requires": { + "js-tokens": "^3.0.0 || ^4.0.0" + } + }, + "loud-rejection": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", + "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=", + "dev": true, + "requires": { + "currently-unhandled": "^0.4.1", + "signal-exit": "^3.0.0" + } + }, + "lowercase-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", + "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==", + "dev": true + }, + "lru-cache": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", + "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "dev": true, + "requires": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + }, + "dependencies": { + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", + "dev": true + } + } + }, + "make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "requires": { + "pify": "^4.0.1", + "semver": "^5.6.0" + } + }, + "mamacro": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/mamacro/-/mamacro-0.0.3.tgz", + "integrity": "sha512-qMEwh+UujcQ+kbz3T6V+wAmO2U8veoq2w+3wY8MquqwVA3jChfwY+Tk52GZKDfACEPjuZ7r2oJLejwpt8jtwTA==", + "dev": true + }, + "map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", + "dev": true + }, + "map-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-2.0.0.tgz", + "integrity": "sha1-plzSkIepJZi4eRJXpSPgISIqwfk=", + "dev": true + }, + "map-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", + "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", + "dev": true, + "requires": { + "object-visit": "^1.0.0" + } + }, + "md5.js": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", + "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", + "dev": true, + "requires": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "meant": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/meant/-/meant-1.0.1.tgz", + "integrity": "sha512-UakVLFjKkbbUwNWJ2frVLnnAtbb7D7DsloxRd3s/gDpI8rdv8W5Hp3NaDb+POBI1fQdeussER6NB8vpcRURvlg==", + "dev": true + }, + "measured": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/measured/-/measured-1.1.0.tgz", + "integrity": "sha1-f2ozre53vGehZIloxXNgjCkbmsQ=", + "requires": { + "inherits": "^2.0" + } + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" + }, + "memory-fs": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", + "integrity": "sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=", + "dev": true, + "requires": { + "errno": "^0.1.3", + "readable-stream": "^2.0.1" + } + }, + "meow": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-5.0.0.tgz", + "integrity": "sha512-CbTqYU17ABaLefO8vCU153ZZlprKYWDljcndKKDCFcYQITzWCXZAVk4QMFZPgvzrnUQ3uItnIE/LoUOwrT15Ig==", + "dev": true, + "requires": { + "camelcase-keys": "^4.0.0", + "decamelize-keys": "^1.0.0", + "loud-rejection": "^1.0.0", + "minimist-options": "^3.0.1", + "normalize-package-data": "^2.3.4", + "read-pkg-up": "^3.0.0", + "redent": "^2.0.0", + "trim-newlines": "^2.0.0", + "yargs-parser": "^10.0.0" + }, + "dependencies": { + "camelcase": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", + "dev": true + }, + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dev": true, + "requires": { + "locate-path": "^2.0.0" + } + }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "dev": true, + "requires": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + } + }, + "p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dev": true, + "requires": { + "p-try": "^1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "dev": true, + "requires": { + "p-limit": "^1.1.0" + } + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", + "dev": true + }, + "read-pkg-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-3.0.0.tgz", + "integrity": "sha1-PtSWaF26D4/hGNBpHcUfSh/5bwc=", + "dev": true, + "requires": { + "find-up": "^2.0.0", + "read-pkg": "^3.0.0" + } + }, + "yargs-parser": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-10.1.0.tgz", + "integrity": "sha512-VCIyR1wJoEBZUqk5PA+oOBF6ypbwh5aNB3I50guxAL/quggdfs4TtNHQrSazFA3fYZ+tEqfs0zIGlv0c/rgjbQ==", + "dev": true, + "requires": { + "camelcase": "^4.1.0" + } + } + } + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + }, + "merge-options": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-options/-/merge-options-1.0.1.tgz", + "integrity": "sha512-iuPV41VWKWBIOpBsjoxjDZw8/GbSfZ2mk7N1453bwMrfzdrIk7EzBd+8UVR6rkw67th7xnk9Dytl3J+lHPdxvg==", + "dev": true, + "requires": { + "is-plain-obj": "^1.1" + } + }, + "merge-source-map": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/merge-source-map/-/merge-source-map-1.1.0.tgz", + "integrity": "sha512-Qkcp7P2ygktpMPh2mCQZaf3jhN6D3Z/qVZHSdWvQ+2Ef5HgRAPBO57A77+ENm0CPx2+1Ce/MYKi3ymqdfuqibw==", + "dev": true, + "requires": { + "source-map": "^0.6.1" + } + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true + } + } + }, + "miller-rabin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", + "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", + "dev": true, + "requires": { + "bn.js": "^4.0.0", + "brorand": "^1.0.1" + } + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" + }, + "mime-db": { + "version": "1.43.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.43.0.tgz", + "integrity": "sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ==" + }, + "mime-types": { + "version": "2.1.26", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.26.tgz", + "integrity": "sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ==", + "requires": { + "mime-db": "1.43.0" + } + }, + "mimic-fn": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", + "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", + "dev": true + }, + "mimic-response": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.0.0.tgz", + "integrity": "sha512-8ilDoEapqA4uQ3TwS0jakGONKXVJqpy+RpM+3b7pLdOjghCrEiGp9SRkFbUHAmZW9vdnrENWHjaweIoTIJExSQ==", + "dev": true + }, + "min-document": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz", + "integrity": "sha1-e9KC4/WELtKVu3SM3Z8f+iyCRoU=", + "dev": true, + "requires": { + "dom-walk": "^0.1.0" + } + }, + "mini-css-extract-plugin": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-0.4.5.tgz", + "integrity": "sha512-dqBanNfktnp2hwL2YguV9Jh91PFX7gu7nRLs4TGsbAfAG6WOtlynFRYzwDwmmeSb5uIwHo9nx1ta0f7vAZVp2w==", + "dev": true, + "requires": { + "loader-utils": "^1.1.0", + "schema-utils": "^1.0.0", + "webpack-sources": "^1.1.0" + }, + "dependencies": { + "schema-utils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", + "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", + "dev": true, + "requires": { + "ajv": "^6.1.0", + "ajv-errors": "^1.0.0", + "ajv-keywords": "^3.1.0" + } + } + } + }, + "minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true + }, + "minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + }, + "minimist-options": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-3.0.2.tgz", + "integrity": "sha512-FyBrT/d0d4+uiZRbqznPXqw3IpZZG3gl3wKWiX784FycUKVwBt0uLBFkQrtE4tZOrgo78nZp2jnKz3L65T5LdQ==", + "dev": true, + "requires": { + "arrify": "^1.0.1", + "is-plain-obj": "^1.1.0" + } + }, + "minipass": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz", + "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", + "dev": true, + "requires": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" + } + }, + "minizlib": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz", + "integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==", + "dev": true, + "requires": { + "minipass": "^2.9.0" + } + }, + "mississippi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mississippi/-/mississippi-3.0.0.tgz", + "integrity": "sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA==", + "dev": true, + "requires": { + "concat-stream": "^1.5.0", + "duplexify": "^3.4.2", + "end-of-stream": "^1.1.0", + "flush-write-stream": "^1.0.0", + "from2": "^2.1.0", + "parallel-transform": "^1.1.0", + "pump": "^3.0.0", + "pumpify": "^1.3.3", + "stream-each": "^1.1.0", + "through2": "^2.0.0" + } + }, + "mixin-deep": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", + "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", + "dev": true, + "requires": { + "for-in": "^1.0.2", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + }, + "dependencies": { + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + } + } + }, + "mobx": { + "version": "5.15.4", + "resolved": "https://registry.npmjs.org/mobx/-/mobx-5.15.4.tgz", + "integrity": "sha512-xRFJxSU2Im3nrGCdjSuOTFmxVDGeqOHL+TyADCGbT0k4HHqGmx5u2yaHNryvoORpI4DfbzjJ5jPmuv+d7sioFw==", + "dev": true + }, + "mobx-react": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/mobx-react/-/mobx-react-6.1.4.tgz", + "integrity": "sha512-wzrJF1RflhyLh8ne4FJfMbG8ZgRFmZ62b4nbyhJzwQpAmrkSnSsAWG9mIff4ffV/Q7OU+uOYf7rXvSmiuUe4cw==", + "dev": true, + "requires": { + "mobx-react-lite": "^1.4.2" + } + }, + "mobx-react-lite": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/mobx-react-lite/-/mobx-react-lite-1.5.2.tgz", + "integrity": "sha512-PyZmARqqWtpuQaAoHF5pKX7h6TKNLwq6vtovm4zZvG6sEbMRHHSqioGXSeQbpRmG8Kw8uln3q/W1yMO5IfL5Sg==", + "dev": true + }, + "mobx-utils": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/mobx-utils/-/mobx-utils-5.5.3.tgz", + "integrity": "sha512-tCj3WLHp3y2/OZADAg9KHGtJNNwwEa8ZY92E6dnVuDoV2OaTV+e2N4S23ogsoxJ72ZhFJhNPcy7ppPJRb1Emhg==", + "dev": true + }, + "mocha": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.2.0.tgz", + "integrity": "sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ==", + "dev": true, + "requires": { + "browser-stdout": "1.3.1", + "commander": "2.15.1", + "debug": "3.1.0", + "diff": "3.5.0", + "escape-string-regexp": "1.0.5", + "glob": "7.1.2", + "growl": "1.10.5", + "he": "1.1.1", + "minimatch": "3.0.4", + "mkdirp": "0.5.1", + "supports-color": "5.4.0" + }, + "dependencies": { + "commander": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", + "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", + "dev": true + }, + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "supports-color": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", + "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "mocha-lcov-reporter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/mocha-lcov-reporter/-/mocha-lcov-reporter-1.3.0.tgz", + "integrity": "sha1-Rpve9PivyaEWBW8HnfYYLQr7A4Q=", + "dev": true + }, + "module-not-found-error": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/module-not-found-error/-/module-not-found-error-1.0.1.tgz", + "integrity": "sha1-z4tP9PKWQGdNbN0CsOO8UjwrvcA=", + "dev": true + }, + "moment": { + "version": "2.24.0", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz", + "integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==" + }, + "moo": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/moo/-/moo-0.5.1.tgz", + "integrity": "sha512-I1mnb5xn4fO80BH9BLcF0yLypy2UKl+Cb01Fu0hJRkJjlCRtxZMWkTdAtDd5ZqCOxtCkhmRwyI57vWT+1iZ67w==", + "dev": true + }, + "move-concurrently": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", + "integrity": "sha1-viwAX9oy4LKa8fBdfEszIUxwH5I=", + "dev": true, + "requires": { + "aproba": "^1.1.1", + "copy-concurrently": "^1.0.0", + "fs-write-stream-atomic": "^1.0.8", + "mkdirp": "^0.5.1", + "rimraf": "^2.5.4", + "run-queue": "^1.0.3" + } + }, + "mri": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.1.4.tgz", + "integrity": "sha512-6y7IjGPm8AzlvoUrwAaw1tLnUBudaS3752vcd8JtrpGGQn+rXIe63LFVHm/YMwtqAuh+LJPCFdlLYPWM1nYn6w==", + "dev": true + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "multimatch": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/multimatch/-/multimatch-3.0.0.tgz", + "integrity": "sha512-22foS/gqQfANZ3o+W7ST2x25ueHDVNWl/b9OlGcLpy/iKxjCpvcNCM51YCenUi7Mt/jAjjqv8JwZRs8YP5sRjA==", + "dev": true, + "requires": { + "array-differ": "^2.0.3", + "array-union": "^1.0.2", + "arrify": "^1.0.1", + "minimatch": "^3.0.4" + } + }, + "mute-stream": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", + "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=", + "dev": true + }, + "nan": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", + "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==" + }, + "nanomatch": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", + "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "fragment-cache": "^0.2.1", + "is-windows": "^1.0.2", + "kind-of": "^6.0.2", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true + } + } + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "dev": true + }, + "nearley": { + "version": "2.19.1", + "resolved": "https://registry.npmjs.org/nearley/-/nearley-2.19.1.tgz", + "integrity": "sha512-xq47GIUGXxU9vQg7g/y1o1xuKnkO7ev4nRWqftmQrLkfnE/FjRqDaGOUakM8XHPn/6pW3bGjU2wgoJyId90rqg==", + "dev": true, + "requires": { + "commander": "^2.19.0", + "moo": "^0.5.0", + "railroad-diagrams": "^1.0.0", + "randexp": "0.4.6", + "semver": "^5.4.1" + }, + "dependencies": { + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + } + } + }, + "needle": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/needle/-/needle-2.4.0.tgz", + "integrity": "sha512-4Hnwzr3mi5L97hMYeNl8wRW/Onhy4nUKR/lVemJ8gJedxxUyBLm9kkrDColJvoSfwi0jCNhD+xCdOtiGDQiRZg==", + "dev": true, + "requires": { + "debug": "^3.2.6", + "iconv-lite": "^0.4.4", + "sax": "^1.2.4" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "negotiator": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", + "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" + }, + "neo-async": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.1.tgz", + "integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==", + "dev": true + }, + "nested-error-stacks": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/nested-error-stacks/-/nested-error-stacks-2.1.0.tgz", + "integrity": "sha512-AO81vsIO1k1sM4Zrd6Hu7regmJN1NSiAja10gc4bX3F0wd+9rQmcuHQaHVQCYIEC8iFXnE+mavh23GOt7wBgug==", + "dev": true + }, + "next-tick": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", + "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=", + "dev": true + }, + "nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true + }, + "node-fetch": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz", + "integrity": "sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==", + "dev": true, + "requires": { + "encoding": "^0.1.11", + "is-stream": "^1.0.1" + } + }, + "node-forge": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.7.6.tgz", + "integrity": "sha512-sol30LUpz1jQFBjOKwbjxijiE3b6pjd74YwfD0fJOKPjF+fONKb2Yg8rYgS6+bK6VDl+/wfr4IYpC7jDzLUIfw==" + }, + "node-libs-browser": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.1.tgz", + "integrity": "sha512-h/zcD8H9kaDZ9ALUWwlBUDo6TKF8a7qBSCSEGfjTVIYeqsioSKaAX+BN7NgiMGp6iSIXZ3PxgCu8KS3b71YK5Q==", + "dev": true, + "requires": { + "assert": "^1.1.1", + "browserify-zlib": "^0.2.0", + "buffer": "^4.3.0", + "console-browserify": "^1.1.0", + "constants-browserify": "^1.0.0", + "crypto-browserify": "^3.11.0", + "domain-browser": "^1.1.1", + "events": "^3.0.0", + "https-browserify": "^1.0.0", + "os-browserify": "^0.3.0", + "path-browserify": "0.0.1", + "process": "^0.11.10", + "punycode": "^1.2.4", + "querystring-es3": "^0.2.0", + "readable-stream": "^2.3.3", + "stream-browserify": "^2.0.1", + "stream-http": "^2.7.2", + "string_decoder": "^1.0.0", + "timers-browserify": "^2.0.4", + "tty-browserify": "0.0.0", + "url": "^0.11.0", + "util": "^0.11.0", + "vm-browserify": "^1.0.1" + }, + "dependencies": { + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", + "dev": true + }, + "util": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz", + "integrity": "sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ==", + "dev": true, + "requires": { + "inherits": "2.0.3" + } + } + } + }, + "node-modules-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz", + "integrity": "sha1-jZ2+KJZKSsVxLpExZCEHxx6Q7EA=", + "dev": true + }, + "node-pre-gyp": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.11.0.tgz", + "integrity": "sha512-TwWAOZb0j7e9eGaf9esRx3ZcLaE5tQ2lvYy1pb5IAaG1a2e2Kv5Lms1Y4hpj+ciXJRofIxxlt5haeQ/2ANeE0Q==", + "dev": true, + "requires": { + "detect-libc": "^1.0.2", + "mkdirp": "^0.5.1", + "needle": "^2.2.1", + "nopt": "^4.0.1", + "npm-packlist": "^1.1.6", + "npmlog": "^4.0.2", + "rc": "^1.2.7", + "rimraf": "^2.6.1", + "semver": "^5.3.0", + "tar": "^4" + } + }, + "node-releases": { + "version": "1.1.47", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.47.tgz", + "integrity": "sha512-k4xjVPx5FpwBUj0Gw7uvFOTF4Ep8Hok1I6qjwL3pLfwe7Y0REQSAqOwwv9TWBCUtMHxcXfY4PgRLRozcChvTcA==", + "dev": true, + "requires": { + "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "nodemon": { + "version": "1.19.4", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-1.19.4.tgz", + "integrity": "sha512-VGPaqQBNk193lrJFotBU8nvWZPqEZY2eIzymy2jjY0fJ9qIsxA0sxQ8ATPl0gZC645gijYEc1jtZvpS8QWzJGQ==", + "dev": true, + "requires": { + "chokidar": "^2.1.8", + "debug": "^3.2.6", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.0.4", + "pstree.remy": "^1.1.7", + "semver": "^5.7.1", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.2", + "update-notifier": "^2.5.0" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "nopt": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", + "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", + "dev": true, + "requires": { + "abbrev": "1", + "osenv": "^0.1.4" + } + }, + "normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "requires": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "npm-bundled": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.1.tgz", + "integrity": "sha512-gqkfgGePhTpAEgUsGEgcq1rqPXA+tv/aVBlgEzfXwA1yiUJF7xtEt3CtVwOjNYQOVknDk0F20w58Fnm3EtG0fA==", + "dev": true, + "requires": { + "npm-normalize-package-bin": "^1.0.1" + } + }, + "npm-normalize-package-bin": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz", + "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==", + "dev": true + }, + "npm-packlist": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.8.tgz", + "integrity": "sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A==", + "dev": true, + "requires": { + "ignore-walk": "^3.0.1", + "npm-bundled": "^1.0.1", + "npm-normalize-package-bin": "^1.0.1" + } + }, + "npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", + "dev": true, + "requires": { + "path-key": "^2.0.0" + } + }, + "npmlog": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", + "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", + "dev": true, + "requires": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "nth-check": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", + "integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==", + "dev": true, + "requires": { + "boolbase": "~1.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" + }, + "numeral": { + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/numeral/-/numeral-1.5.6.tgz", + "integrity": "sha1-ODHbloRRuc9q/5v5WSXx7443sz8=", + "dev": true + }, + "nwsapi": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz", + "integrity": "sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ==", + "dev": true + }, + "nyc": { + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/nyc/-/nyc-14.1.1.tgz", + "integrity": "sha512-OI0vm6ZGUnoGZv/tLdZ2esSVzDwUC88SNs+6JoSOMVxA+gKMB8Tk7jBwgemLx4O40lhhvZCVw1C+OYLOBOPXWw==", + "dev": true, + "requires": { + "archy": "^1.0.0", + "caching-transform": "^3.0.2", + "convert-source-map": "^1.6.0", + "cp-file": "^6.2.0", + "find-cache-dir": "^2.1.0", + "find-up": "^3.0.0", + "foreground-child": "^1.5.6", + "glob": "^7.1.3", + "istanbul-lib-coverage": "^2.0.5", + "istanbul-lib-hook": "^2.0.7", + "istanbul-lib-instrument": "^3.3.0", + "istanbul-lib-report": "^2.0.8", + "istanbul-lib-source-maps": "^3.0.6", + "istanbul-reports": "^2.2.4", + "js-yaml": "^3.13.1", + "make-dir": "^2.1.0", + "merge-source-map": "^1.1.0", + "resolve-from": "^4.0.0", + "rimraf": "^2.6.3", + "signal-exit": "^3.0.2", + "spawn-wrap": "^1.4.2", + "test-exclude": "^5.2.3", + "uuid": "^3.3.2", + "yargs": "^13.2.2", + "yargs-parser": "^13.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "cliui": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "dev": true, + "requires": { + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" + } + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + }, + "wrap-ansi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" + } + }, + "y18n": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", + "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", + "dev": true + }, + "yargs": { + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.0.tgz", + "integrity": "sha512-2eehun/8ALW8TLoIl7MVaRUrg+yCnenu8B4kBlRxj3GJGDKU1Og7sMXPNm1BYyM1DOJmTZ4YeN/Nwxv+8XJsUA==", + "dev": true, + "requires": { + "cliui": "^5.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.1.1" + } + } + } + }, + "oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "dev": true + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + }, + "object-copy": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", + "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", + "dev": true, + "requires": { + "copy-descriptor": "^0.1.0", + "define-property": "^0.2.5", + "kind-of": "^3.0.3" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, + "object-inspect": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.7.0.tgz", + "integrity": "sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==", + "dev": true + }, + "object-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.0.2.tgz", + "integrity": "sha512-Epah+btZd5wrrfjkJZq1AOB9O6OxUQto45hzFd7lXGrpHPGE0W1k+426yrZV+k6NJOzLNNW/nVsmZdIWsAqoOQ==", + "dev": true + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true + }, + "object-visit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", + "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", + "dev": true, + "requires": { + "isobject": "^3.0.0" + } + }, + "object.assign": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", + "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", + "dev": true, + "requires": { + "define-properties": "^1.1.2", + "function-bind": "^1.1.1", + "has-symbols": "^1.0.0", + "object-keys": "^1.0.11" + } + }, + "object.entries": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.1.tgz", + "integrity": "sha512-ilqR7BgdyZetJutmDPfXCDffGa0/Yzl2ivVNpbx/g4UeWrCdRnFDUBrKJGLhGieRHDATnyZXWBeCb29k9CJysQ==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1", + "function-bind": "^1.1.1", + "has": "^1.0.3" + } + }, + "object.fromentries": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.2.tgz", + "integrity": "sha512-r3ZiBH7MQppDJVLx6fhD618GKNG40CZYH9wgwdhKxBDDbQgjeWGGd4AtkZad84d291YxvWe7bJGuE65Anh0dxQ==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1", + "function-bind": "^1.1.1", + "has": "^1.0.3" + } + }, + "object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", + "dev": true, + "requires": { + "isobject": "^3.0.1" + } + }, + "object.values": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.1.tgz", + "integrity": "sha512-WTa54g2K8iu0kmS/us18jEmdv1a4Wi//BZ/DTVYEcH0XhLM5NYdpDHja3gt57VrZLcNAO2WGA+KpWsDBaHt6eA==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1", + "function-bind": "^1.1.1", + "has": "^1.0.3" + } + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "requires": { + "ee-first": "1.1.1" + } + }, + "on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==" + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1" + } + }, + "onetime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", + "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", + "dev": true, + "requires": { + "mimic-fn": "^1.0.0" + } + }, + "opener": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.1.tgz", + "integrity": "sha512-goYSy5c2UXE4Ra1xixabeVh1guIX/ZV/YokJksb6q2lubWu6UbvPQ20p542/sFIll1nl8JnCyK9oBaOcCWXwvA==", + "dev": true + }, + "opn": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/opn/-/opn-5.5.0.tgz", + "integrity": "sha512-PqHpggC9bLV0VeWcdKhkpxY+3JTzetLSqTCWL/z/tFIbI6G8JCjondXklT1JinczLz2Xib62sSp0T/gKT4KksA==", + "dev": true, + "requires": { + "is-wsl": "^1.1.0" + } + }, + "optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "dev": true, + "requires": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + } + }, + "optjs": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/optjs/-/optjs-3.2.2.tgz", + "integrity": "sha1-aabOicRCpEQDFBrS+bNwvVu29O4=" + }, + "ora": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-2.1.0.tgz", + "integrity": "sha512-hNNlAd3gfv/iPmsNxYoAPLvxg7HuPozww7fFonMZvL84tP6Ox5igfk5j/+a9rtJJwqMgKK+JgWsAQik5o0HTLA==", + "dev": true, + "requires": { + "chalk": "^2.3.1", + "cli-cursor": "^2.1.0", + "cli-spinners": "^1.1.0", + "log-symbols": "^2.2.0", + "strip-ansi": "^4.0.0", + "wcwidth": "^1.0.1" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "os-browserify": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", + "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=", + "dev": true + }, + "os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", + "dev": true + }, + "os-locale": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", + "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=", + "requires": { + "lcid": "^1.0.0" + } + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "dev": true + }, + "osenv": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", + "dev": true, + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", + "dev": true + }, + "p-limit": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.2.tgz", + "integrity": "sha512-WGR+xHecKTr7EbUEhyLSh5Dube9JtdiG78ufaeLxTgpudf/20KqyMioIUZJAezlTIi6evxuoUs9YXc11cU+yzQ==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "package-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/package-hash/-/package-hash-3.0.0.tgz", + "integrity": "sha512-lOtmukMDVvtkL84rJHI7dpTYq+0rli8N2wlnqUcBuDWCfVhRUfOmnR9SsoHFMLpACvEV60dX7rd0rFaYDZI+FA==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.15", + "hasha": "^3.0.0", + "lodash.flattendeep": "^4.4.0", + "release-zalgo": "^1.0.0" + } + }, + "package-json": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/package-json/-/package-json-4.0.1.tgz", + "integrity": "sha1-iGmgQBJTZhxMTKPabCEh7VVfXu0=", + "dev": true, + "requires": { + "got": "^6.7.1", + "registry-auth-token": "^3.0.1", + "registry-url": "^3.0.3", + "semver": "^5.1.0" + } + }, + "pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true + }, + "parallel-transform": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/parallel-transform/-/parallel-transform-1.2.0.tgz", + "integrity": "sha512-P2vSmIu38uIlvdcU7fDkyrxj33gTUy/ABO5ZUbGowxNCopBq/OoD42bP4UmMrJoPyk4Uqf0mu3mtWBhHCZD8yg==", + "dev": true, + "requires": { + "cyclist": "^1.0.1", + "inherits": "^2.0.3", + "readable-stream": "^2.1.5" + } + }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "requires": { + "callsites": "^3.0.0" + } + }, + "parse-asn1": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.5.tgz", + "integrity": "sha512-jkMYn1dcJqF6d5CpU689bq7w/b5ALS9ROVSpQDPrZsqqesUJii9qutvoT5ltGedNXMO2e16YUWIghG9KxaViTQ==", + "dev": true, + "requires": { + "asn1.js": "^4.0.0", + "browserify-aes": "^1.0.0", + "create-hash": "^1.1.0", + "evp_bytestokey": "^1.0.0", + "pbkdf2": "^3.0.3", + "safe-buffer": "^5.1.1" + } + }, + "parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "dev": true, + "requires": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + } + }, + "parse5": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-3.0.3.tgz", + "integrity": "sha512-rgO9Zg5LLLkfJF9E6CCmXlSE4UVceloys8JrFqCcHloC3usd/kJCyPDwH2SOlzix2j3xaP9sUX3e8+kvkuleAA==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + }, + "particles.js": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/particles.js/-/particles.js-2.0.0.tgz", + "integrity": "sha1-IThsQyjWx/lngKIB6W7t/AnHNvY=" + }, + "pascalcase": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", + "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", + "dev": true + }, + "passport": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.4.1.tgz", + "integrity": "sha512-IxXgZZs8d7uFSt3eqNjM9NQ3g3uQCW5avD8mRNoXV99Yig50vjuaez6dQK2qC0kVWPRTujxY0dWgGfT09adjYg==", + "requires": { + "passport-strategy": "1.x.x", + "pause": "0.0.1" + } + }, + "passport-saml": { + "version": "0.33.0", + "resolved": "https://registry.npmjs.org/passport-saml/-/passport-saml-0.33.0.tgz", + "integrity": "sha1-UbmfGdztVtJG7k4oh+MvBjIfvs8=", + "requires": { + "passport-strategy": "*", + "q": "^1.5.0", + "xml-crypto": "^0.10.1", + "xml-encryption": "^0.11.0", + "xml2js": "0.4.x", + "xmlbuilder": "^9.0.4", + "xmldom": "0.1.x" + } + }, + "passport-strategy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", + "integrity": "sha1-tVOaqPwiWj0a0XlHbd8ja0QPUuQ=" + }, + "path-browserify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.1.tgz", + "integrity": "sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ==", + "dev": true + }, + "path-dirname": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", + "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=", + "dev": true + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + }, + "path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", + "dev": true + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "dev": true + }, + "path-parse": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + }, + "path-type": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "dev": true, + "requires": { + "pify": "^3.0.0" + }, + "dependencies": { + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + } + } + }, + "pathval": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", + "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=", + "dev": true + }, + "pause": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", + "integrity": "sha1-HUCLP9t2kjuVQ9lvtMnf1TXZy10=" + }, + "pbkdf2": { + "version": "3.0.17", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.17.tgz", + "integrity": "sha512-U/il5MsrZp7mGg3mSQfn742na2T+1/vHDCG5/iTI3X9MKUuYUZVLQhyRsg06mCgDBTd57TxzgZt7P+fYfjRLtA==", + "dev": true, + "requires": { + "create-hash": "^1.1.2", + "create-hmac": "^1.1.4", + "ripemd160": "^2.0.1", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", + "dev": true + }, + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true + }, + "pirates": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.1.tgz", + "integrity": "sha512-WuNqLTbMI3tmfef2TKxlQmAiLHKtFhlsCZnPIpuv2Ow0RDVO8lfy1Opf4NUzlMXLjPl+Men7AuVdX6TA+s+uGA==", + "dev": true, + "requires": { + "node-modules-regexp": "^1.0.0" + } + }, + "pkg-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", + "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", + "dev": true, + "requires": { + "find-up": "^3.0.0" + } + }, + "please-upgrade-node": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz", + "integrity": "sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg==", + "dev": true, + "requires": { + "semver-compare": "^1.0.0" + } + }, + "plur": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/plur/-/plur-3.1.1.tgz", + "integrity": "sha512-t1Ax8KUvV3FFII8ltczPn2tJdjqbd1sIzu6t4JL7nQ3EyeL/lTrj5PWKb06ic5/6XYDr65rQ4uzQEGN70/6X5w==", + "dev": true, + "requires": { + "irregular-plurals": "^2.0.0" + } + }, + "pn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pn/-/pn-1.1.0.tgz", + "integrity": "sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA==", + "dev": true + }, + "posix-character-classes": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", + "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", + "dev": true + }, + "postcss": { + "version": "7.0.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.26.tgz", + "integrity": "sha512-IY4oRjpXWYshuTDFxMVkJDtWIk2LhsTlu8bZnbEJA4+bYT16Lvpo8Qv6EvDumhYRgzjZl489pmsY3qVgJQ08nA==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + }, + "dependencies": { + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss-modules-extract-imports": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-2.0.0.tgz", + "integrity": "sha512-LaYLDNS4SG8Q5WAWqIJgdHPJrDDr/Lv775rMBFUbgjTz6j34lUznACHcdRWroPvXANP2Vj7yNK57vp9eFqzLWQ==", + "dev": true, + "requires": { + "postcss": "^7.0.5" + } + }, + "postcss-modules-local-by-default": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-3.0.2.tgz", + "integrity": "sha512-jM/V8eqM4oJ/22j0gx4jrp63GSvDH6v86OqyTHHUvk4/k1vceipZsaymiZ5PvocqZOl5SFHiFJqjs3la0wnfIQ==", + "dev": true, + "requires": { + "icss-utils": "^4.1.1", + "postcss": "^7.0.16", + "postcss-selector-parser": "^6.0.2", + "postcss-value-parser": "^4.0.0" + } + }, + "postcss-modules-scope": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-2.1.1.tgz", + "integrity": "sha512-OXRUPecnHCg8b9xWvldG/jUpRIGPNRka0r4D4j0ESUU2/5IOnpsjfPPmDprM3Ih8CgZ8FXjWqaniK5v4rWt3oQ==", + "dev": true, + "requires": { + "postcss": "^7.0.6", + "postcss-selector-parser": "^6.0.0" + } + }, + "postcss-modules-values": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-3.0.0.tgz", + "integrity": "sha512-1//E5jCBrZ9DmRX+zCtmQtRSV6PV42Ix7Bzj9GbwJceduuf7IqP8MgeTXuRDHOWj2m0VzZD5+roFWDuU8RQjcg==", + "dev": true, + "requires": { + "icss-utils": "^4.0.0", + "postcss": "^7.0.6" + } + }, + "postcss-selector-parser": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.2.tgz", + "integrity": "sha512-36P2QR59jDTOAiIkqEprfJDsoNrvwFei3eCqKd1Y0tUsBimsq39BLp7RD+JWny3WgB1zGhJX8XVePwm9k4wdBg==", + "dev": true, + "requires": { + "cssesc": "^3.0.0", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1" + } + }, + "postcss-value-parser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.0.2.tgz", + "integrity": "sha512-LmeoohTpp/K4UiyQCwuGWlONxXamGzCMtFxLq4W1nZVGIQLYvMCJx3yAF9qyyuFpflABI9yVdtJAqbihOsCsJQ==", + "dev": true + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "dev": true + }, + "prepend-http": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", + "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=", + "dev": true + }, + "prettier": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.19.1.tgz", + "integrity": "sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==", + "dev": true + }, + "prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "requires": { + "fast-diff": "^1.1.2" + } + }, + "pretty-bytes": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.3.0.tgz", + "integrity": "sha512-hjGrh+P926p4R4WbaB6OckyRtO0F0/lQBiT+0gnxjV+5kjPBrfVBFCsCLbMqVQeydvIoouYTCmmEURiH3R1Bdg==", + "dev": true + }, + "pretty-quick": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/pretty-quick/-/pretty-quick-1.11.1.tgz", + "integrity": "sha512-kSXCkcETfak7EQXz6WOkCeCqpbC4GIzrN/vaneTGMP/fAtD8NerA9bPhCUqHAks1geo7biZNl5uEMPceeneLuA==", + "dev": true, + "requires": { + "chalk": "^2.3.0", + "execa": "^0.8.0", + "find-up": "^2.1.0", + "ignore": "^3.3.7", + "mri": "^1.1.0", + "multimatch": "^3.0.0" + }, + "dependencies": { + "cross-spawn": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", + "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", + "dev": true, + "requires": { + "lru-cache": "^4.0.1", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "execa": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-0.8.0.tgz", + "integrity": "sha1-2NdrvBtVIX7RkP1t1J08d07PyNo=", + "dev": true, + "requires": { + "cross-spawn": "^5.0.1", + "get-stream": "^3.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + } + }, + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dev": true, + "requires": { + "locate-path": "^2.0.0" + } + }, + "get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", + "dev": true + }, + "ignore": { + "version": "3.3.10", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.10.tgz", + "integrity": "sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==", + "dev": true + }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "dev": true, + "requires": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + } + }, + "p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dev": true, + "requires": { + "p-try": "^1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "dev": true, + "requires": { + "p-limit": "^1.1.0" + } + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", + "dev": true + } + } + }, + "private": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz", + "integrity": "sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==", + "dev": true + }, + "process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=", + "dev": true + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true + }, + "progress-bar-webpack-plugin": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/progress-bar-webpack-plugin/-/progress-bar-webpack-plugin-1.12.1.tgz", + "integrity": "sha512-tVbPB5xBbqNwdH3mwcxzjL1r1Vrm/xGu93OsqVSAbCaXGoKFvfWIh0gpMDpn2kYsPVRSAIK0pBkP9Vfs+JJibQ==", + "dev": true, + "requires": { + "chalk": "^1.1.1", + "object.assign": "^4.0.1", + "progress": "^1.1.8" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "progress": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/progress/-/progress-1.1.8.tgz", + "integrity": "sha1-4mDHj2Fhzdmw5WzD4Khd4Xx6V74=", + "dev": true + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + } + } + }, + "promise": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", + "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", + "requires": { + "asap": "~2.0.3" + } + }, + "promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=", + "dev": true + }, + "prop-types": { + "version": "15.7.2", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", + "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", + "dev": true, + "requires": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.8.1" + } + }, + "prop-types-exact": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/prop-types-exact/-/prop-types-exact-1.2.0.tgz", + "integrity": "sha512-K+Tk3Kd9V0odiXFP9fwDHUYRyvK3Nun3GVyPapSIs5OBkITAm15W0CPFD/YKTkMUAbc0b9CUwRQp2ybiBIq+eA==", + "dev": true, + "requires": { + "has": "^1.0.3", + "object.assign": "^4.1.0", + "reflect.ownkeys": "^0.2.0" + } + }, + "propagating-hammerjs": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/propagating-hammerjs/-/propagating-hammerjs-1.4.7.tgz", + "integrity": "sha512-oW9Wd+W2Tp5uOz6Fh4mEU7p+FoyU85smLH/mPga83Loh0pHa6AH4ZHGywvwMk3TWP31l7iUsvJyW265p4Ipwrg==", + "dev": true, + "requires": { + "hammerjs": "^2.0.8" + } + }, + "protobufjs": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-5.0.3.tgz", + "integrity": "sha512-55Kcx1MhPZX0zTbVosMQEO5R6/rikNXd9b6RQK4KSPcrSIIwoXTtebIczUrXlwaSrbz4x8XUVThGPob1n8I4QA==", + "requires": { + "ascli": "~1", + "bytebuffer": "~5", + "glob": "^7.0.5", + "yargs": "^3.10.0" + } + }, + "proxy-addr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.5.tgz", + "integrity": "sha512-t/7RxHXPH6cJtP0pRG6smSr9QJidhB+3kXu0KgXnbGYMgzEnUxRQ4/LDdfOwZEMyIh3/xHb8PX3t+lfL9z+YVQ==", + "requires": { + "forwarded": "~0.1.2", + "ipaddr.js": "1.9.0" + } + }, + "proxyquire": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/proxyquire/-/proxyquire-2.1.3.tgz", + "integrity": "sha512-BQWfCqYM+QINd+yawJz23tbBM40VIGXOdDw3X344KcclI/gtBbdWF6SlQ4nK/bYhF9d27KYug9WzljHC6B9Ysg==", + "dev": true, + "requires": { + "fill-keys": "^1.0.2", + "module-not-found-error": "^1.0.1", + "resolve": "^1.11.1" + } + }, + "prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=", + "dev": true + }, + "pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", + "dev": true + }, + "psl": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.7.0.tgz", + "integrity": "sha512-5NsSEDv8zY70ScRnOTn7bK7eanl2MvFrOrS/R6x+dBt5g1ghnj9Zv90kO8GwT8gxcu2ANyFprnFYB85IogIJOQ==", + "dev": true + }, + "pstree.remy": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.7.tgz", + "integrity": "sha512-xsMgrUwRpuGskEzBFkH8NmTimbZ5PcPup0LA8JJkHIm2IMUbQcpo3yeLNWVrufEYjh8YwtSVh0xz6UeWc5Oh5A==", + "dev": true + }, + "public-encrypt": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", + "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==", + "dev": true, + "requires": { + "bn.js": "^4.1.0", + "browserify-rsa": "^4.0.0", + "create-hash": "^1.1.0", + "parse-asn1": "^5.0.0", + "randombytes": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "pug": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pug/-/pug-2.0.4.tgz", + "integrity": "sha512-XhoaDlvi6NIzL49nu094R2NA6P37ijtgMDuWE+ofekDChvfKnzFal60bhSdiy8y2PBO6fmz3oMEIcfpBVRUdvw==", + "requires": { + "pug-code-gen": "^2.0.2", + "pug-filters": "^3.1.1", + "pug-lexer": "^4.1.0", + "pug-linker": "^3.0.6", + "pug-load": "^2.0.12", + "pug-parser": "^5.0.1", + "pug-runtime": "^2.0.5", + "pug-strip-comments": "^1.0.4" + } + }, + "pug-attrs": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pug-attrs/-/pug-attrs-2.0.4.tgz", + "integrity": "sha512-TaZ4Z2TWUPDJcV3wjU3RtUXMrd3kM4Wzjbe3EWnSsZPsJ3LDI0F3yCnf2/W7PPFF+edUFQ0HgDL1IoxSz5K8EQ==", + "requires": { + "constantinople": "^3.0.1", + "js-stringify": "^1.0.1", + "pug-runtime": "^2.0.5" + } + }, + "pug-code-gen": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/pug-code-gen/-/pug-code-gen-2.0.2.tgz", + "integrity": "sha512-kROFWv/AHx/9CRgoGJeRSm+4mLWchbgpRzTEn8XCiwwOy6Vh0gAClS8Vh5TEJ9DBjaP8wCjS3J6HKsEsYdvaCw==", + "requires": { + "constantinople": "^3.1.2", + "doctypes": "^1.1.0", + "js-stringify": "^1.0.1", + "pug-attrs": "^2.0.4", + "pug-error": "^1.3.3", + "pug-runtime": "^2.0.5", + "void-elements": "^2.0.1", + "with": "^5.0.0" + } + }, + "pug-error": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/pug-error/-/pug-error-1.3.3.tgz", + "integrity": "sha512-qE3YhESP2mRAWMFJgKdtT5D7ckThRScXRwkfo+Erqga7dyJdY3ZquspprMCj/9sJ2ijm5hXFWQE/A3l4poMWiQ==" + }, + "pug-filters": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/pug-filters/-/pug-filters-3.1.1.tgz", + "integrity": "sha512-lFfjNyGEyVWC4BwX0WyvkoWLapI5xHSM3xZJFUhx4JM4XyyRdO8Aucc6pCygnqV2uSgJFaJWW3Ft1wCWSoQkQg==", + "requires": { + "clean-css": "^4.1.11", + "constantinople": "^3.0.1", + "jstransformer": "1.0.0", + "pug-error": "^1.3.3", + "pug-walk": "^1.1.8", + "resolve": "^1.1.6", + "uglify-js": "^2.6.1" + } + }, + "pug-lexer": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/pug-lexer/-/pug-lexer-4.1.0.tgz", + "integrity": "sha512-i55yzEBtjm0mlplW4LoANq7k3S8gDdfC6+LThGEvsK4FuobcKfDAwt6V4jKPH9RtiE3a2Akfg5UpafZ1OksaPA==", + "requires": { + "character-parser": "^2.1.1", + "is-expression": "^3.0.0", + "pug-error": "^1.3.3" + } + }, + "pug-linker": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/pug-linker/-/pug-linker-3.0.6.tgz", + "integrity": "sha512-bagfuHttfQOpANGy1Y6NJ+0mNb7dD2MswFG2ZKj22s8g0wVsojpRlqveEQHmgXXcfROB2RT6oqbPYr9EN2ZWzg==", + "requires": { + "pug-error": "^1.3.3", + "pug-walk": "^1.1.8" + } + }, + "pug-load": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/pug-load/-/pug-load-2.0.12.tgz", + "integrity": "sha512-UqpgGpyyXRYgJs/X60sE6SIf8UBsmcHYKNaOccyVLEuT6OPBIMo6xMPhoJnqtB3Q3BbO4Z3Bjz5qDsUWh4rXsg==", + "requires": { + "object-assign": "^4.1.0", + "pug-walk": "^1.1.8" + } + }, + "pug-parser": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pug-parser/-/pug-parser-5.0.1.tgz", + "integrity": "sha512-nGHqK+w07p5/PsPIyzkTQfzlYfuqoiGjaoqHv1LjOv2ZLXmGX1O+4Vcvps+P4LhxZ3drYSljjq4b+Naid126wA==", + "requires": { + "pug-error": "^1.3.3", + "token-stream": "0.0.1" + } + }, + "pug-runtime": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/pug-runtime/-/pug-runtime-2.0.5.tgz", + "integrity": "sha512-P+rXKn9un4fQY77wtpcuFyvFaBww7/91f3jHa154qU26qFAnOe6SW1CbIDcxiG5lLK9HazYrMCCuDvNgDQNptw==" + }, + "pug-strip-comments": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/pug-strip-comments/-/pug-strip-comments-1.0.4.tgz", + "integrity": "sha512-i5j/9CS4yFhSxHp5iKPHwigaig/VV9g+FgReLJWWHEHbvKsbqL0oP/K5ubuLco6Wu3Kan5p7u7qk8A4oLLh6vw==", + "requires": { + "pug-error": "^1.3.3" + } + }, + "pug-walk": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pug-walk/-/pug-walk-1.1.8.tgz", + "integrity": "sha512-GMu3M5nUL3fju4/egXwZO0XLi6fW/K3T3VTgFQ14GxNi8btlxgT5qZL//JwZFm/2Fa64J/PNS8AZeys3wiMkVA==" + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "pumpify": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", + "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", + "dev": true, + "requires": { + "duplexify": "^3.6.0", + "inherits": "^2.0.3", + "pump": "^2.0.0" + }, + "dependencies": { + "pump": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", + "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + } + } + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true + }, + "q": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", + "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=" + }, + "qs": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.1.tgz", + "integrity": "sha512-Cxm7/SS/y/Z3MHWSxXb8lIFqgqBowP5JMlTUFyJN88y0SGQhVmZnqFK/PeuMX9LzUyWsqqhNxIyg0jlzq946yA==" + }, + "querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", + "dev": true + }, + "querystring-es3": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", + "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=", + "dev": true + }, + "quick-lru": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-1.1.0.tgz", + "integrity": "sha1-Q2CxfGETatOAeDl/8RQW4Ybc+7g=", + "dev": true + }, + "raf": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", + "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", + "dev": true, + "requires": { + "performance-now": "^2.1.0" + } + }, + "railroad-diagrams": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz", + "integrity": "sha1-635iZ1SN3t+4mcG5Dlc3RVnN234=", + "dev": true + }, + "randexp": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/randexp/-/randexp-0.4.6.tgz", + "integrity": "sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ==", + "dev": true, + "requires": { + "discontinuous-range": "1.0.0", + "ret": "~0.1.10" + } + }, + "random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha1-T2ih3Arli9P7lYSMMDJNt11kNgs=" + }, + "randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "requires": { + "safe-buffer": "^5.1.0" + } + }, + "randomfill": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", + "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", + "dev": true, + "requires": { + "randombytes": "^2.0.5", + "safe-buffer": "^5.1.0" + } + }, + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" + }, + "raw-body": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", + "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", + "requires": { + "bytes": "3.1.0", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + } + }, + "rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "requires": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + } + }, + "react": { + "version": "16.12.0", + "resolved": "https://registry.npmjs.org/react/-/react-16.12.0.tgz", + "integrity": "sha512-fglqy3k5E+81pA8s+7K0/T3DBCF0ZDOher1elBFzF7O6arXJgzyu/FW+COxFvAWXJoJN9KIZbT2LXlukwphYTA==", + "dev": true, + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "prop-types": "^15.6.2" + } + }, + "react-bootstrap-table": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/react-bootstrap-table/-/react-bootstrap-table-4.3.1.tgz", + "integrity": "sha1-9wS+Vbf2vwVX0vxb7G0l/TB9DN4=", + "dev": true, + "requires": { + "classnames": "^2.1.2", + "prop-types": "^15.5.10", + "react-modal": "^3.1.7", + "react-s-alert": "^1.3.2" + } + }, + "react-chartjs-2": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-2.9.0.tgz", + "integrity": "sha512-IYwqUUnQRAJ9SNA978vxulHJTcUFTJk2LDVfbAyk0TnJFZZG7+6U/2flsE4MCw6WCbBjTTypy8T82Ch7XrPtRw==", + "dev": true, + "requires": { + "lodash": "^4.17.4", + "prop-types": "^15.5.8" + } + }, + "react-circular-progressbar": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/react-circular-progressbar/-/react-circular-progressbar-0.8.1.tgz", + "integrity": "sha512-ys2+LcenXWfY5TejPtekl5CvOGb1dPoalyVW/08n8Wo6OPFax5kLrdBFLFi5F/bBYTpNiTp4xUAWKuyrWE2n/g==", + "dev": true, + "requires": { + "prop-types": "^15.5.10" + } + }, + "react-copy-to-clipboard": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/react-copy-to-clipboard/-/react-copy-to-clipboard-5.0.2.tgz", + "integrity": "sha512-/2t5mLMMPuN5GmdXo6TebFa8IoFxZ+KTDDqYhcDm0PhkgEzSxVvIX26G20s1EB02A4h2UZgwtfymZ3lGJm0OLg==", + "dev": true, + "requires": { + "copy-to-clipboard": "^3", + "prop-types": "^15.5.8" + } + }, + "react-datetime": { + "version": "2.16.3", + "resolved": "https://registry.npmjs.org/react-datetime/-/react-datetime-2.16.3.tgz", + "integrity": "sha512-amWfb5iGEiyqjLmqCLlPpu2oN415jK8wX1qoTq7qn6EYiU7qQgbNHglww014PT4O/3G5eo/3kbJu/M/IxxTyGw==", + "dev": true, + "requires": { + "create-react-class": "^15.5.2", + "object-assign": "^3.0.0", + "prop-types": "^15.5.7", + "react-onclickoutside": "^6.5.0" + }, + "dependencies": { + "object-assign": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-3.0.0.tgz", + "integrity": "sha1-m+3VygiXlJvKR+f/QIBi1Un1h/I=", + "dev": true + } + } + }, + "react-dom": { + "version": "16.12.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.12.0.tgz", + "integrity": "sha512-LMxFfAGrcS3kETtQaCkTKjMiifahaMySFDn71fZUNpPHZQEzmk/GiAeIT8JSOrHB23fnuCOMruL2a8NYlw+8Gw==", + "dev": true, + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "prop-types": "^15.6.2", + "scheduler": "^0.18.0" + } + }, + "react-ga": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/react-ga/-/react-ga-2.7.0.tgz", + "integrity": "sha512-AjC7UOZMvygrWTc2hKxTDvlMXEtbmA0IgJjmkhgmQQ3RkXrWR11xEagLGFGaNyaPnmg24oaIiaNPnEoftUhfXA==", + "dev": true + }, + "react-hot-loader": { + "version": "4.12.19", + "resolved": "https://registry.npmjs.org/react-hot-loader/-/react-hot-loader-4.12.19.tgz", + "integrity": "sha512-p8AnA4QE2GtrvkdmqnKrEiijtVlqdTIDCHZOwItkI9kW51bt5XnQ/4Anz8giiWf9kqBpEQwsmnChDCAFBRyR/Q==", + "dev": true, + "requires": { + "fast-levenshtein": "^2.0.6", + "global": "^4.3.0", + "hoist-non-react-statics": "^3.3.0", + "loader-utils": "^1.1.0", + "prop-types": "^15.6.1", + "react-lifecycles-compat": "^3.0.4", + "shallowequal": "^1.1.0", + "source-map": "^0.7.3" + }, + "dependencies": { + "source-map": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", + "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", + "dev": true + } + } + }, + "react-input-autosize": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/react-input-autosize/-/react-input-autosize-2.2.2.tgz", + "integrity": "sha512-jQJgYCA3S0j+cuOwzuCd1OjmBmnZLdqQdiLKRYrsMMzbjUrVDS5RvJUDwJqA7sKuksDuzFtm6hZGKFu7Mjk5aw==", + "dev": true, + "requires": { + "prop-types": "^15.5.8" + } + }, + "react-is": { + "version": "16.12.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.12.0.tgz", + "integrity": "sha512-rPCkf/mWBtKc97aLL9/txD8DZdemK0vkA3JMLShjlJB3Pj3s+lpf1KaBzMfQrAmhMQB0n1cU/SUGgKKBCe837Q==", + "dev": true + }, + "react-lifecycles-compat": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==", + "dev": true + }, + "react-modal": { + "version": "3.11.1", + "resolved": "https://registry.npmjs.org/react-modal/-/react-modal-3.11.1.tgz", + "integrity": "sha512-8uN744Yq0X2lbfSLxsEEc2UV3RjSRb4yDVxRQ1aGzPo86QjNOwhQSukDb8U8kR+636TRTvfMren10fgOjAy9eA==", + "dev": true, + "requires": { + "exenv": "^1.2.0", + "prop-types": "^15.5.10", + "react-lifecycles-compat": "^3.0.0", + "warning": "^4.0.3" + } + }, + "react-motion": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/react-motion/-/react-motion-0.5.2.tgz", + "integrity": "sha512-9q3YAvHoUiWlP3cK0v+w1N5Z23HXMj4IF4YuvjvWegWqNPfLXsOBE/V7UvQGpXxHFKRQQcNcVQE31g9SB/6qgQ==", + "dev": true, + "requires": { + "performance-now": "^0.2.0", + "prop-types": "^15.5.8", + "raf": "^3.1.0" + }, + "dependencies": { + "performance-now": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-0.2.0.tgz", + "integrity": "sha1-M+8wxcd9TqIcWlOGnZG1bY8lVeU=", + "dev": true + } + } + }, + "react-onclickoutside": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/react-onclickoutside/-/react-onclickoutside-6.9.0.tgz", + "integrity": "sha512-8ltIY3bC7oGhj2nPAvWOGi+xGFybPNhJM0V1H8hY/whNcXgmDeaeoCMPPd8VatrpTsUWjb/vGzrmu6SrXVty3A==", + "dev": true + }, + "react-router": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-4.3.1.tgz", + "integrity": "sha512-yrvL8AogDh2X42Dt9iknk4wF4V8bWREPirFfS9gLU1huk6qK41sg7Z/1S81jjTrGHxa3B8R3J6xIkDAA6CVarg==", + "dev": true, + "requires": { + "history": "^4.7.2", + "hoist-non-react-statics": "^2.5.0", + "invariant": "^2.2.4", + "loose-envify": "^1.3.1", + "path-to-regexp": "^1.7.0", + "prop-types": "^15.6.1", + "warning": "^4.0.1" + }, + "dependencies": { + "hoist-non-react-statics": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz", + "integrity": "sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw==", + "dev": true + }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "path-to-regexp": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "dev": true, + "requires": { + "isarray": "0.0.1" + } + } + } + }, + "react-router-dom": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-4.3.1.tgz", + "integrity": "sha512-c/MlywfxDdCp7EnB7YfPMOfMD3tOtIjrQlj/CKfNMBxdmpJP8xcz5P/UAFn3JbnQCNUxsHyVVqllF9LhgVyFCA==", + "dev": true, + "requires": { + "history": "^4.7.2", + "invariant": "^2.2.4", + "loose-envify": "^1.3.1", + "prop-types": "^15.6.1", + "react-router": "^4.3.1", + "warning": "^4.0.1" + } + }, + "react-s-alert": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/react-s-alert/-/react-s-alert-1.4.1.tgz", + "integrity": "sha512-+cSpVPe6YeGklhlo7zbVlB0Z6jdiU9HPmEVzp5nIhNm9lvdL7rVO2Jx09pCwT99GmODyoN0iNhbQku6r7six8A==", + "dev": true, + "requires": { + "babel-runtime": "^6.23.0" + } + }, + "react-select": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/react-select/-/react-select-1.3.0.tgz", + "integrity": "sha512-g/QAU1HZrzSfxkwMAo/wzi6/ezdWye302RGZevsATec07hI/iSxcpB1hejFIp7V63DJ8mwuign6KmB3VjdlinQ==", + "dev": true, + "requires": { + "classnames": "^2.2.4", + "prop-types": "^15.5.8", + "react-input-autosize": "^2.1.2" + } + }, + "react-sparklines": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/react-sparklines/-/react-sparklines-1.7.0.tgz", + "integrity": "sha512-bJFt9K4c5Z0k44G8KtxIhbG+iyxrKjBZhdW6afP+R7EnIq+iKjbWbEFISrf3WKNFsda+C46XAfnX0StS5fbDcg==", + "dev": true, + "requires": { + "prop-types": "^15.5.10" + } + }, + "react-test-renderer": { + "version": "16.12.0", + "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-16.12.0.tgz", + "integrity": "sha512-Vj/teSqt2oayaWxkbhQ6gKis+t5JrknXfPVo+aIJ8QwYAqMPH77uptOdrlphyxl8eQI/rtkOYg86i/UWkpFu0w==", + "dev": true, + "requires": { + "object-assign": "^4.1.1", + "prop-types": "^15.6.2", + "react-is": "^16.8.6", + "scheduler": "^0.18.0" + } + }, + "react-typist": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/react-typist/-/react-typist-2.0.5.tgz", + "integrity": "sha512-iZCkeqeegO0TlkTMiH2JD1tvMtY9RrXkRylnAI6m8aCVAUUwNzoWTVF7CKLij6THeOMcUDCznLDDvNp55s+YZA==", + "dev": true, + "requires": { + "prop-types": "^15.5.10" + } + }, + "react-vis": { + "version": "1.11.7", + "resolved": "https://registry.npmjs.org/react-vis/-/react-vis-1.11.7.tgz", + "integrity": "sha512-vJqS12l/6RHeSq8DVl4PzX0j8iPgbT8H8PtgTRsimKsBNcPjPseO4RICw1FUPrwj8MPrrna34LBtzyC4ATd5Ow==", + "dev": true, + "requires": { + "d3-array": "^1.2.0", + "d3-collection": "^1.0.3", + "d3-color": "^1.0.3", + "d3-contour": "^1.1.0", + "d3-format": "^1.2.0", + "d3-geo": "^1.6.4", + "d3-hexbin": "^0.2.2", + "d3-hierarchy": "^1.1.4", + "d3-interpolate": "^1.1.4", + "d3-sankey": "^0.7.1", + "d3-scale": "^1.0.5", + "d3-shape": "^1.1.0", + "d3-voronoi": "^1.1.2", + "deep-equal": "^1.0.1", + "global": "^4.3.1", + "hoek": "4.2.1", + "prop-types": "^15.5.8", + "react-motion": "^0.5.2" + } + }, + "read-pkg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", + "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", + "dev": true, + "requires": { + "load-json-file": "^4.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^3.0.0" + } + }, + "read-pkg-up": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz", + "integrity": "sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4=", + "dev": true, + "requires": { + "find-up": "^2.0.0", + "read-pkg": "^2.0.0" + }, + "dependencies": { + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dev": true, + "requires": { + "locate-path": "^2.0.0" + } + }, + "load-json-file": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", + "integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "strip-bom": "^3.0.0" + } + }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "dev": true, + "requires": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + } + }, + "p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dev": true, + "requires": { + "p-try": "^1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "dev": true, + "requires": { + "p-limit": "^1.1.0" + } + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", + "dev": true + }, + "parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", + "dev": true, + "requires": { + "error-ex": "^1.2.0" + } + }, + "path-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", + "integrity": "sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=", + "dev": true, + "requires": { + "pify": "^2.0.0" + } + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + }, + "read-pkg": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", + "integrity": "sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=", + "dev": true, + "requires": { + "load-json-file": "^2.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^2.0.0" + } + } + } + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "readdirp": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", + "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.11", + "micromatch": "^3.1.10", + "readable-stream": "^2.0.2" + } + }, + "redent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-2.0.0.tgz", + "integrity": "sha1-wbIAe0LVfrE4kHmzyDM2OdXhzKo=", + "dev": true, + "requires": { + "indent-string": "^3.0.0", + "strip-indent": "^2.0.0" + } + }, + "reflect.ownkeys": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/reflect.ownkeys/-/reflect.ownkeys-0.2.0.tgz", + "integrity": "sha1-dJrO7H8/34tj+SegSAnpDFwLNGA=", + "dev": true + }, + "regenerate": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz", + "integrity": "sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg==", + "dev": true + }, + "regenerate-unicode-properties": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-8.1.0.tgz", + "integrity": "sha512-LGZzkgtLY79GeXLm8Dp0BVLdQlWICzBnJz/ipWUgo59qBaZ+BHtq51P2q1uVZlppMuUAT37SDk39qUbjTWB7bA==", + "dev": true, + "requires": { + "regenerate": "^1.4.0" + } + }, + "regenerator-runtime": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", + "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==" + }, + "regenerator-transform": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.14.1.tgz", + "integrity": "sha512-flVuee02C3FKRISbxhXl9mGzdbWUVHubl1SMaknjxkFB1/iqpJhArQUvRxOOPEc/9tAiX0BaQ28FJH10E4isSQ==", + "dev": true, + "requires": { + "private": "^0.1.6" + } + }, + "regex-not": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", + "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", + "dev": true, + "requires": { + "extend-shallow": "^3.0.2", + "safe-regex": "^1.1.0" + } + }, + "regexp.prototype.flags": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.3.0.tgz", + "integrity": "sha512-2+Q0C5g951OlYlJz6yu5/M33IcsESLlLfsyIaLJaG4FA2r4yP8MvVMJUUP/fVBkSpbbbZlS5gynbEWLipiiXiQ==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1" + } + }, + "regexpp": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", + "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", + "dev": true + }, + "regexpu-core": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.6.0.tgz", + "integrity": "sha512-YlVaefl8P5BnFYOITTNzDvan1ulLOiXJzCNZxduTIosN17b87h3bvG9yHMoHaRuo88H4mQ06Aodj5VtYGGGiTg==", + "dev": true, + "requires": { + "regenerate": "^1.4.0", + "regenerate-unicode-properties": "^8.1.0", + "regjsgen": "^0.5.0", + "regjsparser": "^0.6.0", + "unicode-match-property-ecmascript": "^1.0.4", + "unicode-match-property-value-ecmascript": "^1.1.0" + } + }, + "registry-auth-token": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-3.4.0.tgz", + "integrity": "sha512-4LM6Fw8eBQdwMYcES4yTnn2TqIasbXuwDx3um+QRs7S55aMKCBKBxvPXl2RiUjHwuJLTyYfxSpmfSAjQpcuP+A==", + "dev": true, + "requires": { + "rc": "^1.1.6", + "safe-buffer": "^5.0.1" + } + }, + "registry-url": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-3.1.0.tgz", + "integrity": "sha1-PU74cPc93h138M+aOBQyRE4XSUI=", + "dev": true, + "requires": { + "rc": "^1.0.1" + } + }, + "regjsgen": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.5.1.tgz", + "integrity": "sha512-5qxzGZjDs9w4tzT3TPhCJqWdCc3RLYwy9J2NB0nm5Lz+S273lvWcpjaTGHsT1dc6Hhfq41uSEOw8wBmxrKOuyg==", + "dev": true + }, + "regjsparser": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.6.2.tgz", + "integrity": "sha512-E9ghzUtoLwDekPT0DYCp+c4h+bvuUpe6rRHCTYn6eGoqj1LgKXxT6I0Il4WbjhQkOghzi/V+y03bPKvbllL93Q==", + "dev": true, + "requires": { + "jsesc": "~0.5.0" + }, + "dependencies": { + "jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=", + "dev": true + } + } + }, + "release-zalgo": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/release-zalgo/-/release-zalgo-1.0.0.tgz", + "integrity": "sha1-CXALflB0Mpc5Mw5TXFqQ+2eFFzA=", + "dev": true, + "requires": { + "es6-error": "^4.0.1" + } + }, + "remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", + "dev": true + }, + "repeat-element": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz", + "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==", + "dev": true + }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=" + }, + "request": { + "version": "2.88.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", + "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", + "dev": true, + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.0", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.4.3", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + }, + "dependencies": { + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", + "dev": true + } + } + }, + "request-promise-core": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.3.tgz", + "integrity": "sha512-QIs2+ArIGQVp5ZYbWD5ZLCY29D5CfWizP8eWnm8FoGD1TX61veauETVQbrV60662V0oFBkrDOuaBI8XgtuyYAQ==", + "dev": true, + "requires": { + "lodash": "^4.17.15" + } + }, + "request-promise-native": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.8.tgz", + "integrity": "sha512-dapwLGqkHtwL5AEbfenuzjTYg35Jd6KPytsC2/TLkVMz8rm+tNt72MGUWT1RP/aYawMpN6HqbNGBQaRcBtjQMQ==", + "dev": true, + "requires": { + "request-promise-core": "1.1.3", + "stealthy-require": "^1.1.1", + "tough-cookie": "^2.3.3" + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "dev": true + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, + "require-package-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/require-package-name/-/require-package-name-2.0.1.tgz", + "integrity": "sha1-wR6XJ2tluOKSP3Xav1+y7ww4Qbk=", + "dev": true + }, + "resolve": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.15.0.tgz", + "integrity": "sha512-+hTmAldEGE80U2wJJDC1lebb5jWqvTYAfm3YZ1ckk1gBr0MnCqUKlwK1e+anaFljIl+F5tR5IoZcm4ZDA1zMQw==", + "requires": { + "path-parse": "^1.0.6" + } + }, + "resolve-cwd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-2.0.0.tgz", + "integrity": "sha1-AKn3OHVW4nA46uIyyqNypqWbZlo=", + "dev": true, + "requires": { + "resolve-from": "^3.0.0" + }, + "dependencies": { + "resolve-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", + "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=", + "dev": true + } + } + }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + }, + "resolve-pathname": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz", + "integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==", + "dev": true + }, + "resolve-url": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", + "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", + "dev": true + }, + "restore-cursor": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", + "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", + "dev": true, + "requires": { + "onetime": "^2.0.0", + "signal-exit": "^3.0.2" + } + }, + "ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", + "dev": true + }, + "right-align": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", + "integrity": "sha1-YTObci/mo1FWiSENJOFMlhSGE+8=", + "requires": { + "align-text": "^0.1.1" + } + }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "ripemd160": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", + "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", + "dev": true, + "requires": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1" + } + }, + "rst-selector-parser": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/rst-selector-parser/-/rst-selector-parser-2.2.3.tgz", + "integrity": "sha1-gbIw6i/MYGbInjRy3nlChdmwPZE=", + "dev": true, + "requires": { + "lodash.flattendeep": "^4.4.0", + "nearley": "^2.7.10" + } + }, + "run-async": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz", + "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=", + "dev": true, + "requires": { + "is-promise": "^2.1.0" + } + }, + "run-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/run-node/-/run-node-1.0.0.tgz", + "integrity": "sha512-kc120TBlQ3mih1LSzdAJXo4xn/GWS2ec0l3S+syHDXP9uRr0JAT8Qd3mdMuyjqCzeZktgP3try92cEgf9Nks8A==", + "dev": true + }, + "run-queue": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/run-queue/-/run-queue-1.0.3.tgz", + "integrity": "sha1-6Eg5bwV9Ij8kOGkkYY4laUFh7Ec=", + "dev": true, + "requires": { + "aproba": "^1.1.1" + } + }, + "rx": { + "version": "2.3.24", + "resolved": "https://registry.npmjs.org/rx/-/rx-2.3.24.tgz", + "integrity": "sha1-FPlQpCF9fjXapxu8vljv9o6ksrc=", + "dev": true + }, + "rxjs": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.4.tgz", + "integrity": "sha512-naMQXcgEo3csAEGvw/NydRA0fuS2nDZJiw1YUWFKU7aPPAPGZEsD4Iimit96qwCieH6y614MCLYwdkrWx7z/7Q==", + "dev": true, + "requires": { + "tslib": "^1.9.0" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "safe-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", + "dev": true, + "requires": { + "ret": "~0.1.10" + } + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "samsam": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/samsam/-/samsam-1.1.2.tgz", + "integrity": "sha1-vsEf3IOp/aBjQBIQ5AF2wwJNFWc=", + "dev": true + }, + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" + }, + "scheduler": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.18.0.tgz", + "integrity": "sha512-agTSHR1Nbfi6ulI0kYNK0203joW2Y5W4po4l+v03tOoiJKpTBbxpNhWDvqc/4IcOw+KLmSiQLTasZ4cab2/UWQ==", + "dev": true, + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" + } + }, + "schema-utils": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.6.4.tgz", + "integrity": "sha512-VNjcaUxVnEeun6B2fiiUDjXXBtD4ZSH7pdbfIu1pOFwgptDPLMo/z9jr4sUfsjFVPqDCEin/F7IYlq7/E6yDbQ==", + "dev": true, + "requires": { + "ajv": "^6.10.2", + "ajv-keywords": "^3.4.1" + } + }, + "seedrandom": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz", + "integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==" + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + }, + "semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha1-De4hahyUGrN+nvsXiPavxf9VN/w=", + "dev": true + }, + "semver-diff": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-2.1.0.tgz", + "integrity": "sha1-S7uEN8jTfksM8aaP1ybsbWRdbTY=", + "dev": true, + "requires": { + "semver": "^5.0.3" + } + }, + "send": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", + "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", + "requires": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "~1.7.2", + "mime": "1.6.0", + "ms": "2.1.1", + "on-finished": "~2.3.0", + "range-parser": "~1.2.1", + "statuses": "~1.5.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + }, + "dependencies": { + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + } + } + }, + "serialize-javascript": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-2.1.2.tgz", + "integrity": "sha512-rs9OggEUF0V4jUSecXazOYsLfu7OGK2qIn3c7IPBiffz32XniEp/TX9Xmc9LQfK2nQ2QKHvZ2oygKUGU0lG4jQ==", + "dev": true + }, + "serve-favicon": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/serve-favicon/-/serve-favicon-2.5.0.tgz", + "integrity": "sha1-k10kDN/g9YBTB/3+ln2IlCosvPA=", + "requires": { + "etag": "~1.8.1", + "fresh": "0.5.2", + "ms": "2.1.1", + "parseurl": "~1.3.2", + "safe-buffer": "5.1.1" + }, + "dependencies": { + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + }, + "safe-buffer": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", + "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" + } + } + }, + "serve-static": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", + "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.17.1" + } + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "dev": true + }, + "set-value": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", + "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.3", + "split-string": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=", + "dev": true + }, + "setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" + }, + "sha.js": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "shallowequal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==", + "dev": true + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "dev": true, + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "dev": true + }, + "side-channel": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.2.tgz", + "integrity": "sha512-7rL9YlPHg7Ancea1S96Pa8/QWb4BtXL/TZvS6B8XFetGBeuhAsfmUspK6DokBeZ64+Kj9TCNRD/30pVz1BvQNA==", + "dev": true, + "requires": { + "es-abstract": "^1.17.0-next.1", + "object-inspect": "^1.7.0" + } + }, + "signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", + "dev": true + }, + "simple-concat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.0.tgz", + "integrity": "sha1-c0TLuLbib7J9ZrL8hvn21Zl1IcY=", + "dev": true + }, + "simple-get": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.0.tgz", + "integrity": "sha512-bCR6cP+aTdScaQCnQKbPKtJOKDp/hj9EDLJo3Nw4y1QksqaovlW/bnptB6/c1e+qmNIDHRK+oXFDdEqBT8WzUA==", + "dev": true, + "requires": { + "decompress-response": "^4.2.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "sinon": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-1.17.7.tgz", + "integrity": "sha1-RUKk9JugxFwF6y6d2dID4rjv4L8=", + "dev": true, + "requires": { + "formatio": "1.1.1", + "lolex": "1.3.2", + "samsam": "1.1.2", + "util": ">=0.10.3 <1" + } + }, + "slash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", + "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", + "dev": true + }, + "slice-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", + "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.0", + "astral-regex": "^1.0.0", + "is-fullwidth-code-point": "^2.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + } + } + }, + "snapdragon": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", + "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", + "dev": true, + "requires": { + "base": "^0.11.1", + "debug": "^2.2.0", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "map-cache": "^0.2.2", + "source-map": "^0.5.6", + "source-map-resolve": "^0.5.0", + "use": "^3.1.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "snapdragon-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", + "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", + "dev": true, + "requires": { + "define-property": "^1.0.0", + "isobject": "^3.0.0", + "snapdragon-util": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + }, + "kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true + } + } + }, + "snapdragon-util": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", + "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", + "dev": true, + "requires": { + "kind-of": "^3.2.0" + } + }, + "source-list-map": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", + "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + }, + "source-map-resolve": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", + "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==", + "dev": true, + "requires": { + "atob": "^2.1.2", + "decode-uri-component": "^0.2.0", + "resolve-url": "^0.2.1", + "source-map-url": "^0.4.0", + "urix": "^0.1.0" + } + }, + "source-map-support": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.16.tgz", + "integrity": "sha512-efyLRJDr68D9hBBNIPWFjhpFzURh+KJykQwvMyW5UiZzYwoF6l4YMMDIJJEyFWxWCqfyxLzz6tSfUFR+kXXsVQ==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "source-map-url": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", + "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", + "dev": true + }, + "spawn-command": { + "version": "0.0.2-1", + "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2-1.tgz", + "integrity": "sha1-YvXpRmmBwbeW3Fkpk34RycaSG9A=", + "dev": true + }, + "spawn-wrap": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-1.4.3.tgz", + "integrity": "sha512-IgB8md0QW/+tWqcavuFgKYR/qIRvJkRLPJDFaoXtLLUaVcCDK0+HeFTkmQHj3eprcYhc+gOl0aEA1w7qZlYezw==", + "dev": true, + "requires": { + "foreground-child": "^1.5.6", + "mkdirp": "^0.5.0", + "os-homedir": "^1.0.1", + "rimraf": "^2.6.2", + "signal-exit": "^3.0.2", + "which": "^1.3.0" + } + }, + "spdx-correct": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz", + "integrity": "sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==", + "dev": true, + "requires": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz", + "integrity": "sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==", + "dev": true + }, + "spdx-expression-parse": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", + "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", + "dev": true, + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz", + "integrity": "sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==", + "dev": true + }, + "split-string": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", + "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", + "dev": true, + "requires": { + "extend-shallow": "^3.0.0" + } + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "sshpk": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", + "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", + "dev": true, + "requires": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + } + }, + "ssri": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz", + "integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==", + "dev": true, + "requires": { + "figgy-pudding": "^3.5.1" + } + }, + "stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=" + }, + "static-extend": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", + "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", + "dev": true, + "requires": { + "define-property": "^0.2.5", + "object-copy": "^0.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, + "statsd-client": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/statsd-client/-/statsd-client-0.4.4.tgz", + "integrity": "sha512-GjAReJDNZomTTTaIaDuDddWknHO2GXmXS/9JKy6iQFOHNSQ4yeaRGP18oNgahl+c3XTUfUWBYIUnipznNh5Vww==" + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" + }, + "stealthy-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", + "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=", + "dev": true + }, + "stream-browserify": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz", + "integrity": "sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg==", + "dev": true, + "requires": { + "inherits": "~2.0.1", + "readable-stream": "^2.0.2" + } + }, + "stream-each": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/stream-each/-/stream-each-1.2.3.tgz", + "integrity": "sha512-vlMC2f8I2u/bZGqkdfLQW/13Zihpej/7PmSiMQsbYddxuTsJp8vRe2x2FvVExZg7FaOds43ROAuFJwPR4MTZLw==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "stream-shift": "^1.0.0" + } + }, + "stream-http": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.8.3.tgz", + "integrity": "sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw==", + "dev": true, + "requires": { + "builtin-status-codes": "^3.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.3.6", + "to-arraybuffer": "^1.0.0", + "xtend": "^4.0.0" + } + }, + "stream-shift": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", + "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==", + "dev": true + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + }, + "dependencies": { + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "requires": { + "ansi-regex": "^2.0.0" + } + } + } + }, + "string.prototype.matchall": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.2.tgz", + "integrity": "sha512-N/jp6O5fMf9os0JU3E72Qhf590RSRZU/ungsL/qJUYVTNv7hTG0P/dbPjxINVN9jpscu3nzYwKESU3P3RY5tOg==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0", + "has-symbols": "^1.0.1", + "internal-slot": "^1.0.2", + "regexp.prototype.flags": "^1.3.0", + "side-channel": "^1.0.2" + } + }, + "string.prototype.trim": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.1.tgz", + "integrity": "sha512-MjGFEeqixw47dAMFMtgUro/I0+wNqZB5GKXGt1fFr24u3TzDXCPu7J9Buppzoe3r/LqkSDLDDJzE15RGWDGAVw==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1", + "function-bind": "^1.1.1" + } + }, + "string.prototype.trimleft": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.1.tgz", + "integrity": "sha512-iu2AGd3PuP5Rp7x2kEZCrB2Nf41ehzh+goo8TV7z8/XDBbsvc6HQIlUl9RjkZ4oyrW1XM5UwlGl1oVEaDjg6Ag==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "function-bind": "^1.1.1" + } + }, + "string.prototype.trimright": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.1.tgz", + "integrity": "sha512-qFvWL3/+QIgZXVmJBfpHmxLB7xsUXz6HsUmP8+5dRaC3Q7oKUv9Vo6aMCRZC1smrtyECFsIT30PqBJ1gTjAs+g==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "function-bind": "^1.1.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-ansi": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-0.1.1.tgz", + "integrity": "sha1-OeipjQRNFQZgq+SmgIrPcLt7yZE=" + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + }, + "strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", + "dev": true + }, + "strip-indent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-2.0.0.tgz", + "integrity": "sha1-XvjbKV0B5u1sv3qrlpmNeCJSe2g=", + "dev": true + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "dev": true + }, + "style-loader": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-0.21.0.tgz", + "integrity": "sha512-T+UNsAcl3Yg+BsPKs1vd22Fr8sVT+CJMtzqc6LEw9bbJZb43lm9GoeIfUcDEefBSWC0BhYbcdupV1GtI4DGzxg==", + "dev": true, + "requires": { + "loader-utils": "^1.1.0", + "schema-utils": "^0.4.5" + }, + "dependencies": { + "schema-utils": { + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.4.7.tgz", + "integrity": "sha512-v/iwU6wvwGK8HbU9yi3/nhGzP0yGSuhQMzL6ySiec1FSrZZDkhm4noOSWzrNFo/jEc+SJY6jRTwuwbSXJPDUnQ==", + "dev": true, + "requires": { + "ajv": "^6.1.0", + "ajv-keywords": "^3.1.0" + } + } + } + }, + "superagent": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-3.8.3.tgz", + "integrity": "sha512-GLQtLMCoEIK4eDv6OGtkOoSMt3D+oq0y3dsxMuYuDvaNUvuT8eFBuLmfR0iYYzHC1e8hpzC6ZsxbuP6DIalMFA==", + "dev": true, + "requires": { + "component-emitter": "^1.2.0", + "cookiejar": "^2.1.0", + "debug": "^3.1.0", + "extend": "^3.0.0", + "form-data": "^2.3.1", + "formidable": "^1.2.0", + "methods": "^1.1.1", + "mime": "^1.4.1", + "qs": "^6.5.1", + "readable-stream": "^2.3.5" + } + }, + "supertest": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-3.4.2.tgz", + "integrity": "sha512-WZWbwceHUo2P36RoEIdXvmqfs47idNNZjCuJOqDz6rvtkk8ym56aU5oglORCpPeXGxT7l9rkJ41+O1lffQXYSA==", + "dev": true, + "requires": { + "methods": "^1.1.2", + "superagent": "^3.8.3" + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "svg-url-loader": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/svg-url-loader/-/svg-url-loader-3.0.3.tgz", + "integrity": "sha512-MKGiRNDs8fnHcZcPkhGcw9+130IXyFM9H8m6T7u3ScUuZYEeVzX0vNMru30D4MCF6vMYas5iw/Ru9lwFKBjaGw==", + "dev": true, + "requires": { + "file-loader": "~4.3.0", + "loader-utils": "~1.2.3" + } + }, + "symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true + }, + "table": { + "version": "5.4.6", + "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz", + "integrity": "sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==", + "dev": true, + "requires": { + "ajv": "^6.10.2", + "lodash": "^4.17.14", + "slice-ansi": "^2.1.0", + "string-width": "^3.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "tapable": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", + "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", + "dev": true + }, + "tar": { + "version": "4.4.13", + "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.13.tgz", + "integrity": "sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==", + "dev": true, + "requires": { + "chownr": "^1.1.1", + "fs-minipass": "^1.2.5", + "minipass": "^2.8.6", + "minizlib": "^1.2.1", + "mkdirp": "^0.5.0", + "safe-buffer": "^5.1.2", + "yallist": "^3.0.3" + } + }, + "term-size": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/term-size/-/term-size-1.2.0.tgz", + "integrity": "sha1-RYuDiH8oj8Vtb/+/rSYuJmOO+mk=", + "dev": true, + "requires": { + "execa": "^0.7.0" + }, + "dependencies": { + "cross-spawn": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", + "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", + "dev": true, + "requires": { + "lru-cache": "^4.0.1", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "execa": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", + "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=", + "dev": true, + "requires": { + "cross-spawn": "^5.0.1", + "get-stream": "^3.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + } + }, + "get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", + "dev": true + } + } + }, + "terser": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/terser/-/terser-4.6.3.tgz", + "integrity": "sha512-Lw+ieAXmY69d09IIc/yqeBqXpEQIpDGZqT34ui1QWXIUpR2RjbqEkT8X7Lgex19hslSqcWM5iMN2kM11eMsESQ==", + "dev": true, + "requires": { + "commander": "^2.20.0", + "source-map": "~0.6.1", + "source-map-support": "~0.5.12" + }, + "dependencies": { + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + } + } + }, + "terser-webpack-plugin": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.3.tgz", + "integrity": "sha512-QMxecFz/gHQwteWwSo5nTc6UaICqN1bMedC5sMtUc7y3Ha3Q8y6ZO0iCR8pq4RJC8Hjf0FEPEHZqcMB/+DFCrA==", + "dev": true, + "requires": { + "cacache": "^12.0.2", + "find-cache-dir": "^2.1.0", + "is-wsl": "^1.1.0", + "schema-utils": "^1.0.0", + "serialize-javascript": "^2.1.2", + "source-map": "^0.6.1", + "terser": "^4.1.2", + "webpack-sources": "^1.4.0", + "worker-farm": "^1.7.0" + }, + "dependencies": { + "schema-utils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", + "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", + "dev": true, + "requires": { + "ajv": "^6.1.0", + "ajv-errors": "^1.0.0", + "ajv-keywords": "^3.1.0" + } + } + } + }, + "test-exclude": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-5.2.3.tgz", + "integrity": "sha512-M+oxtseCFO3EDtAaGH7iiej3CBkzXqFMbzqYAACdzKui4eZA+pq3tZEwChvOdNfa7xxy8BfbmgJSIr43cC/+2g==", + "dev": true, + "requires": { + "glob": "^7.1.3", + "minimatch": "^3.0.4", + "read-pkg-up": "^4.0.0", + "require-main-filename": "^2.0.0" + }, + "dependencies": { + "read-pkg-up": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-4.0.0.tgz", + "integrity": "sha512-6etQSH7nJGsK0RbG/2TeDzZFa8shjQ1um+SwQQ5cwKy0dhSXdOncEhb1CPpvQG4h7FyOV6EB6YlV0yJvZQNAkA==", + "dev": true, + "requires": { + "find-up": "^3.0.0", + "read-pkg": "^3.0.0" + } + } + } + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true + }, + "three": { + "version": "0.106.2", + "resolved": "https://registry.npmjs.org/three/-/three-0.106.2.tgz", + "integrity": "sha512-4Tlx43uoxnIaZFW2Bzkd1rXsatvVHEWAZJy8LuE+s6Q8c66ogNnhfq1bHiBKPAnXP230LD11H/ScIZc2LZMviA==", + "dev": true + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true + }, + "through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, + "timeago.js": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/timeago.js/-/timeago.js-3.0.2.tgz", + "integrity": "sha1-MqZ+fA2IfqQspYjTquJvd95edsw=", + "dev": true, + "requires": { + "@types/jquery": "^2.0.40" + } + }, + "timed-out": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz", + "integrity": "sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8=", + "dev": true + }, + "timers-browserify": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.11.tgz", + "integrity": "sha512-60aV6sgJ5YEbzUdn9c8kYGIqOubPoUdqQCul3SBAsRCZ40s6Y5cMcrW4dt3/k/EsbLVJNl9n6Vz3fTc+k2GeKQ==", + "dev": true, + "requires": { + "setimmediate": "^1.0.4" + } + }, + "tiny-invariant": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.1.0.tgz", + "integrity": "sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw==", + "dev": true + }, + "tiny-warning": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", + "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==", + "dev": true + }, + "titleize": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/titleize/-/titleize-1.0.1.tgz", + "integrity": "sha512-rUwGDruKq1gX+FFHbTl5qjI7teVO7eOe+C8IcQ7QT+1BK3eEUXJqbZcBOeaRP4FwSC/C1A5jDoIVta0nIQ9yew==", + "dev": true + }, + "tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "requires": { + "os-tmpdir": "~1.0.2" + } + }, + "to-arraybuffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", + "integrity": "sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=", + "dev": true + }, + "to-fast-properties": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", + "integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=" + }, + "to-object-path": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", + "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + } + }, + "to-regex": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", + "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", + "dev": true, + "requires": { + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "regex-not": "^1.0.2", + "safe-regex": "^1.1.0" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } + }, + "toggle-selection": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", + "integrity": "sha1-bkWxJj8gF/oKzH2J14sVuL932jI=", + "dev": true + }, + "toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" + }, + "token-stream": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/token-stream/-/token-stream-0.0.1.tgz", + "integrity": "sha1-zu78cXp2xDFvEm0LnbqlXX598Bo=" + }, + "topo": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/topo/-/topo-2.0.2.tgz", + "integrity": "sha1-zVYVdSU5BXwNwEkaYhw7xvvh0YI=", + "dev": true, + "requires": { + "hoek": "4.x.x" + } + }, + "touch": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", + "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==", + "dev": true, + "requires": { + "nopt": "~1.0.10" + }, + "dependencies": { + "nopt": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", + "integrity": "sha1-bd0hvSoxQXuScn3Vhfim83YI6+4=", + "dev": true, + "requires": { + "abbrev": "1" + } + } + } + }, + "tough-cookie": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", + "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", + "dev": true, + "requires": { + "psl": "^1.1.24", + "punycode": "^1.4.1" + }, + "dependencies": { + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", + "dev": true + } + } + }, + "tr46": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", + "integrity": "sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true + }, + "trim-newlines": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-2.0.0.tgz", + "integrity": "sha1-tAPQuRvlDDMd/EuC7s6yLD3hbSA=", + "dev": true + }, + "tryer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tryer/-/tryer-1.0.1.tgz", + "integrity": "sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==", + "dev": true + }, + "tslib": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", + "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==", + "dev": true + }, + "tsscmp": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", + "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==" + }, + "tty-browserify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", + "integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=", + "dev": true + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "dev": true, + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "dev": true + }, + "type": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz", + "integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==", + "dev": true + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2" + } + }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, + "type-fest": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", + "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", + "dev": true + }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", + "dev": true + }, + "ua-parser-js": { + "version": "0.7.21", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.21.tgz", + "integrity": "sha512-+O8/qh/Qj8CgC6eYBVBykMrNtp5Gebn4dlGD/kKXVkJNDwyrAwSIqwz8CDf+tsAIWVycKcku6gIXJ0qwx/ZXaQ==", + "dev": true + }, + "uglify-js": { + "version": "2.8.29", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", + "integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=", + "requires": { + "source-map": "~0.5.1", + "uglify-to-browserify": "~1.0.0", + "yargs": "~3.10.0" + }, + "dependencies": { + "camelcase": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", + "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=" + }, + "cliui": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", + "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=", + "requires": { + "center-align": "^0.1.1", + "right-align": "^0.1.1", + "wordwrap": "0.0.2" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + }, + "window-size": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", + "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=" + }, + "yargs": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", + "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=", + "requires": { + "camelcase": "^1.0.2", + "cliui": "^2.1.0", + "decamelize": "^1.0.0", + "window-size": "0.1.0" + } + } + } + }, + "uglify-to-browserify": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", + "integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc=", + "optional": true + }, + "uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "requires": { + "random-bytes": "~1.0.0" + } + }, + "undefsafe": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.2.tgz", + "integrity": "sha1-Il9rngM3Zj4Njnz9aG/Cg2zKznY=", + "dev": true, + "requires": { + "debug": "^2.2.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + } + } + }, + "unicode-canonical-property-names-ecmascript": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz", + "integrity": "sha512-jDrNnXWHd4oHiTZnx/ZG7gtUTVp+gCcTTKr8L0HjlwphROEW3+Him+IpvC+xcJEFegapiMZyZe02CyuOnRmbnQ==", + "dev": true + }, + "unicode-match-property-ecmascript": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-1.0.4.tgz", + "integrity": "sha512-L4Qoh15vTfntsn4P1zqnHulG0LdXgjSO035fEpdtp6YxXhMT51Q6vgM5lYdG/5X3MjS+k/Y9Xw4SFCY9IkR0rg==", + "dev": true, + "requires": { + "unicode-canonical-property-names-ecmascript": "^1.0.4", + "unicode-property-aliases-ecmascript": "^1.0.4" + } + }, + "unicode-match-property-value-ecmascript": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.1.0.tgz", + "integrity": "sha512-hDTHvaBk3RmFzvSl0UVrUmC3PuW9wKVnpoUDYH0JDkSIovzw+J5viQmeYHxVSBptubnr7PbH2e0fnpDRQnQl5g==", + "dev": true + }, + "unicode-property-aliases-ecmascript": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.0.5.tgz", + "integrity": "sha512-L5RAqCfXqAwR3RriF8pM0lU0w4Ryf/GgzONwi6KnL1taJQa7x1TCxdJnILX59WIGOwR57IVxn7Nej0fz1Ny6fw==", + "dev": true + }, + "union-value": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", + "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", + "dev": true, + "requires": { + "arr-union": "^3.1.0", + "get-value": "^2.0.6", + "is-extendable": "^0.1.1", + "set-value": "^2.0.1" + } + }, + "uniq": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz", + "integrity": "sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=", + "dev": true + }, + "unique-filename": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", + "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", + "dev": true, + "requires": { + "unique-slug": "^2.0.0" + } + }, + "unique-slug": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", + "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", + "dev": true, + "requires": { + "imurmurhash": "^0.1.4" + } + }, + "unique-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-1.0.0.tgz", + "integrity": "sha1-nhBXzKhRq7kzmPizOuGHuZyuwRo=", + "dev": true, + "requires": { + "crypto-random-string": "^1.0.0" + } + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" + }, + "unset-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", + "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", + "dev": true, + "requires": { + "has-value": "^0.3.1", + "isobject": "^3.0.0" + }, + "dependencies": { + "has-value": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", + "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", + "dev": true, + "requires": { + "get-value": "^2.0.3", + "has-values": "^0.1.4", + "isobject": "^2.0.0" + }, + "dependencies": { + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "dev": true, + "requires": { + "isarray": "1.0.0" + } + } + } + }, + "has-values": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", + "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=", + "dev": true + } + } + }, + "unzip-response": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unzip-response/-/unzip-response-2.0.1.tgz", + "integrity": "sha1-0vD3N9FrBhXnKmk17QQhRXLVb5c=", + "dev": true + }, + "upath": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", + "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", + "dev": true + }, + "update-notifier": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-2.5.0.tgz", + "integrity": "sha512-gwMdhgJHGuj/+wHJJs9e6PcCszpxR1b236igrOkUofGhqJuG+amlIKwApH1IW1WWl7ovZxsX49lMBWLxSdm5Dw==", + "dev": true, + "requires": { + "boxen": "^1.2.1", + "chalk": "^2.0.1", + "configstore": "^3.0.0", + "import-lazy": "^2.1.0", + "is-ci": "^1.0.10", + "is-installed-globally": "^0.1.0", + "is-npm": "^1.0.0", + "latest-version": "^3.0.0", + "semver-diff": "^2.0.0", + "xdg-basedir": "^3.0.0" + }, + "dependencies": { + "ci-info": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-1.6.0.tgz", + "integrity": "sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A==", + "dev": true + }, + "is-ci": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-1.2.1.tgz", + "integrity": "sha512-s6tfsaQaQi3JNciBH6shVqEDvhGut0SUXr31ag8Pd8BBbVVlcGfWhpPmEOoM6RJ5TFhbypvf5yyRw/VXW1IiWg==", + "dev": true, + "requires": { + "ci-info": "^1.5.0" + } + } + } + }, + "uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "urix": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", + "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", + "dev": true + }, + "url": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", + "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", + "dev": true, + "requires": { + "punycode": "1.3.2", + "querystring": "0.2.0" + }, + "dependencies": { + "punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=", + "dev": true + } + } + }, + "url-parse-lax": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-1.0.0.tgz", + "integrity": "sha1-evjzA2Rem9eaJy56FKxovAYJ2nM=", + "dev": true, + "requires": { + "prepend-http": "^1.0.1" + } + }, + "use": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", + "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", + "dev": true + }, + "util": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.1.tgz", + "integrity": "sha512-MREAtYOp+GTt9/+kwf00IYoHZyjM8VU4aVrkzUlejyqaIjd2GztVl5V9hGXKlvBKE3gENn/FMfHE5v6hElXGcQ==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "object.entries": "^1.1.0", + "safe-buffer": "^5.1.2" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" + }, + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "dev": true + }, + "v8-compile-cache": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.1.0.tgz", + "integrity": "sha512-usZBT3PW+LOjM25wbqIlZwPeJV+3OSz3M1k1Ws8snlW39dZyYL9lOGC5FgPVHfk0jKmjiDV8Z0mIbVQPiwFs7g==", + "dev": true + }, + "validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "requires": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "value-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz", + "integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==", + "dev": true + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "vis": { + "version": "4.21.0-EOL", + "resolved": "https://registry.npmjs.org/vis/-/vis-4.21.0-EOL.tgz", + "integrity": "sha512-JVS1mywKg5S88XbkDJPfCb3n+vlg5fMA8Ae2hzs3KHAwD4ryM5qwlbFZ6ReDfY8te7I4NLCpuCoywJQEehvJlQ==", + "dev": true, + "requires": { + "emitter-component": "^1.1.1", + "hammerjs": "^2.0.8", + "keycharm": "^0.2.0", + "moment": "^2.18.1", + "propagating-hammerjs": "^1.4.6" + } + }, + "vizceral": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/vizceral/-/vizceral-4.9.0.tgz", + "integrity": "sha512-K90lI+mLsEgqccgs77dtV8L6C7/ljxGnmGZivy7VMsi2/hbQKy9wih/1FEIAzzXUZ/830a8XPxsepMGNYcxQFg==", + "dev": true, + "requires": { + "@tweenjs/tween.js": "^16.8.0", + "chroma-js": "^1.1.1", + "hammerjs": "^2.0.8", + "lodash": "^4.17.14", + "numeral": "^1.5.3", + "three": "^0.106.2" + } + }, + "vizceral-react": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/vizceral-react/-/vizceral-react-4.8.0.tgz", + "integrity": "sha512-k9T/+wOfkoVy6Bw+7U1Jp/8KkebN2mtgPb9SJWQDK050u7u3SS944Q4AsSrRWK8oCqQRblzPYcy50DLlq1xfGw==", + "dev": true, + "requires": { + "lodash": "^4.17.14", + "prop-types": "^15.6.1", + "vizceral": "^4.9.0" + } + }, + "vm-browserify": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz", + "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==", + "dev": true + }, + "void-elements": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", + "integrity": "sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=" + }, + "vscode-json-languageservice": { + "version": "3.4.12", + "resolved": "https://registry.npmjs.org/vscode-json-languageservice/-/vscode-json-languageservice-3.4.12.tgz", + "integrity": "sha512-+tA0KPVM1pDfORZqsQen7bY5buBpQGDTVYEobm5MoGtXNeZY2Kn0iy5wIQqXveb28LRv/I5xKE87dmNJTEaijQ==", + "dev": true, + "requires": { + "jsonc-parser": "^2.2.0", + "vscode-languageserver-textdocument": "^1.0.1-next.1", + "vscode-languageserver-types": "^3.15.0", + "vscode-nls": "^4.1.1", + "vscode-uri": "^2.1.1" + } + }, + "vscode-languageserver-textdocument": { + "version": "1.0.1-next.1", + "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.1-next.1.tgz", + "integrity": "sha512-Cmt0KsNxouns+d7/Kw/jWtWU9Z3h56z1qAA8utjDOEqrDcrTs2rDXv3EJRa99nuKM3wVf6DbWym1VqL9q71XPA==", + "dev": true + }, + "vscode-languageserver-types": { + "version": "3.15.1", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.15.1.tgz", + "integrity": "sha512-+a9MPUQrNGRrGU630OGbYVQ+11iOIovjCkqxajPa9w57Sd5ruK8WQNsslzpa0x/QJqC8kRc2DUxWjIFwoNm4ZQ==", + "dev": true + }, + "vscode-nls": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/vscode-nls/-/vscode-nls-4.1.1.tgz", + "integrity": "sha512-4R+2UoUUU/LdnMnFjePxfLqNhBS8lrAFyX7pjb2ud/lqDkrUavFUTcG7wR0HBZFakae0Q6KLBFjMS6W93F403A==", + "dev": true + }, + "vscode-uri": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-2.1.1.tgz", + "integrity": "sha512-eY9jmGoEnVf8VE8xr5znSah7Qt1P/xsCdErz+g8HYZtJ7bZqKH5E3d+6oVNm1AC/c6IHUDokbmVXKOi4qPAC9A==", + "dev": true + }, + "w3c-hr-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.1.tgz", + "integrity": "sha1-gqwr/2PZUOqeMYmlimViX+3xkEU=", + "dev": true, + "requires": { + "browser-process-hrtime": "^0.1.2" + } + }, + "wait-on": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-2.1.2.tgz", + "integrity": "sha512-Jm6pzZkbswtcRUXohxY1Ek5MrL16AwHj83drgW2FTQuglHuhZhVMyBLPIYG0rL1wvr5rdC1uzRuU/7Bc+B9Pwg==", + "dev": true, + "requires": { + "core-js": "^2.4.1", + "joi": "^9.2.0", + "minimist": "^1.2.0", + "request": "^2.78.0", + "rx": "^4.1.0" + }, + "dependencies": { + "rx": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/rx/-/rx-4.1.0.tgz", + "integrity": "sha1-pfE/957zt0D+MKqAP7CfmIBdR4I=", + "dev": true + } + } + }, + "warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "dev": true, + "requires": { + "loose-envify": "^1.0.0" + } + }, + "watchpack": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.6.0.tgz", + "integrity": "sha512-i6dHe3EyLjMmDlU1/bGQpEw25XSjkJULPuAVKCbNRefQVq48yXKUpwg538F7AZTf9kyr57zj++pQFltUa5H7yA==", + "dev": true, + "requires": { + "chokidar": "^2.0.2", + "graceful-fs": "^4.1.2", + "neo-async": "^2.5.0" + } + }, + "wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=", + "dev": true, + "requires": { + "defaults": "^1.0.3" + } + }, + "webidl-conversions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", + "dev": true + }, + "webpack": { + "version": "4.41.5", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.41.5.tgz", + "integrity": "sha512-wp0Co4vpyumnp3KlkmpM5LWuzvZYayDwM2n17EHFr4qxBBbRokC7DJawPJC7TfSFZ9HZ6GsdH40EBj4UV0nmpw==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.8.5", + "@webassemblyjs/helper-module-context": "1.8.5", + "@webassemblyjs/wasm-edit": "1.8.5", + "@webassemblyjs/wasm-parser": "1.8.5", + "acorn": "^6.2.1", + "ajv": "^6.10.2", + "ajv-keywords": "^3.4.1", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^4.1.0", + "eslint-scope": "^4.0.3", + "json-parse-better-errors": "^1.0.2", + "loader-runner": "^2.4.0", + "loader-utils": "^1.2.3", + "memory-fs": "^0.4.1", + "micromatch": "^3.1.10", + "mkdirp": "^0.5.1", + "neo-async": "^2.6.1", + "node-libs-browser": "^2.2.1", + "schema-utils": "^1.0.0", + "tapable": "^1.1.3", + "terser-webpack-plugin": "^1.4.3", + "watchpack": "^1.6.0", + "webpack-sources": "^1.4.1" + }, + "dependencies": { + "acorn": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.0.tgz", + "integrity": "sha512-gac8OEcQ2Li1dxIEWGZzsp2BitJxwkwcOm0zHAJLcPJaVvm58FRnk6RkuLRpU1EujipU2ZFODv2P9DLMfnV8mw==", + "dev": true + }, + "eslint-scope": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", + "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==", + "dev": true, + "requires": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + } + }, + "schema-utils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", + "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", + "dev": true, + "requires": { + "ajv": "^6.1.0", + "ajv-errors": "^1.0.0", + "ajv-keywords": "^3.1.0" + } + } + } + }, + "webpack-bundle-analyzer": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-3.6.0.tgz", + "integrity": "sha512-orUfvVYEfBMDXgEKAKVvab5iQ2wXneIEorGNsyuOyVYpjYrI7CUOhhXNDd3huMwQ3vNNWWlGP+hzflMFYNzi2g==", + "dev": true, + "requires": { + "acorn": "^6.0.7", + "acorn-walk": "^6.1.1", + "bfj": "^6.1.1", + "chalk": "^2.4.1", + "commander": "^2.18.0", + "ejs": "^2.6.1", + "express": "^4.16.3", + "filesize": "^3.6.1", + "gzip-size": "^5.0.0", + "lodash": "^4.17.15", + "mkdirp": "^0.5.1", + "opener": "^1.5.1", + "ws": "^6.0.0" + }, + "dependencies": { + "acorn": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.0.tgz", + "integrity": "sha512-gac8OEcQ2Li1dxIEWGZzsp2BitJxwkwcOm0zHAJLcPJaVvm58FRnk6RkuLRpU1EujipU2ZFODv2P9DLMfnV8mw==", + "dev": true + }, + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "ws": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.1.tgz", + "integrity": "sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA==", + "dev": true, + "requires": { + "async-limiter": "~1.0.0" + } + } + } + }, + "webpack-command": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/webpack-command/-/webpack-command-0.4.2.tgz", + "integrity": "sha512-2JZRlV+eT2nsw0DGDS/F4ndv0e/QVkyYj4/1fagp9DbjRagQ02zuVzELp/QF5mrCESKKvnXiBQoaBJUOjAMp8w==", + "dev": true, + "requires": { + "@webpack-contrib/config-loader": "^1.2.0", + "@webpack-contrib/schema-utils": "^1.0.0-beta.0", + "camelcase": "^5.0.0", + "chalk": "^2.3.2", + "debug": "^3.1.0", + "decamelize": "^2.0.0", + "enhanced-resolve": "^4.0.0", + "import-local": "^1.0.0", + "isobject": "^3.0.1", + "loader-utils": "^1.1.0", + "log-symbols": "^2.2.0", + "loud-rejection": "^1.6.0", + "meant": "^1.0.1", + "meow": "^5.0.0", + "merge-options": "^1.0.0", + "object.values": "^1.0.4", + "opn": "^5.3.0", + "ora": "^2.1.0", + "plur": "^3.0.0", + "pretty-bytes": "^5.0.0", + "strip-ansi": "^4.0.0", + "text-table": "^0.2.0", + "titleize": "^1.0.1", + "update-notifier": "^2.3.0", + "v8-compile-cache": "^2.0.0", + "webpack-log": "^1.1.2", + "wordwrap": "^1.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, + "decamelize": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-2.0.0.tgz", + "integrity": "sha512-Ikpp5scV3MSYxY39ymh45ZLEecsTdv/Xj2CaQfI8RLMuwi7XvjX9H/fhraiSuU+C5w5NTDu4ZU72xNiZnurBPg==", + "dev": true, + "requires": { + "xregexp": "4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + }, + "wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", + "dev": true + } + } + }, + "webpack-log": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/webpack-log/-/webpack-log-1.2.0.tgz", + "integrity": "sha512-U9AnICnu50HXtiqiDxuli5gLB5PGBo7VvcHx36jRZHwK4vzOYLbImqT4lwWwoMHdQWwEKw736fCHEekokTEKHA==", + "dev": true, + "requires": { + "chalk": "^2.1.0", + "log-symbols": "^2.1.0", + "loglevelnext": "^1.0.1", + "uuid": "^3.1.0" + } + }, + "webpack-sources": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", + "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", + "dev": true, + "requires": { + "source-list-map": "^2.0.0", + "source-map": "~0.6.1" + } + }, + "whatwg-encoding": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz", + "integrity": "sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==", + "dev": true, + "requires": { + "iconv-lite": "0.4.24" + } + }, + "whatwg-fetch": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz", + "integrity": "sha512-9GSJUgz1D4MfyKU7KRqwOjXCXTqWdFNvEr7eUBYchQiVc744mqK/MzXPNR2WsPkmkOa4ywfg8C2n8h+13Bey1Q==", + "dev": true + }, + "whatwg-mimetype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz", + "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==", + "dev": true + }, + "whatwg-url": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", + "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", + "dev": true, + "requires": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", + "dev": true + }, + "wide-align": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", + "dev": true, + "requires": { + "string-width": "^1.0.2 || 2" + } + }, + "widest-line": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-2.0.1.tgz", + "integrity": "sha512-Ba5m9/Fa4Xt9eb2ELXt77JxVDV8w7qQrH0zS/TWSJdLyAwQjWoOzpzj5lwVftDz6n/EOu3tNACS84v509qwnJA==", + "dev": true, + "requires": { + "string-width": "^2.1.1" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "window-size": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.4.tgz", + "integrity": "sha1-+OGqHuWlPsW/FR/6CXQqatdpeHY=" + }, + "winston": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/winston/-/winston-2.4.4.tgz", + "integrity": "sha512-NBo2Pepn4hK4V01UfcWcDlmiVTs7VTB1h7bgnB0rgP146bYhMxX0ypCz3lBOfNxCO4Zuek7yeT+y/zM1OfMw4Q==", + "requires": { + "async": "~1.0.0", + "colors": "1.0.x", + "cycle": "1.0.x", + "eyes": "0.1.x", + "isstream": "0.1.x", + "stack-trace": "0.0.x" + }, + "dependencies": { + "async": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async/-/async-1.0.0.tgz", + "integrity": "sha1-+PwEyjoTeErenhZBr5hXjPvWR6k=" + } + } + }, + "with": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/with/-/with-5.1.1.tgz", + "integrity": "sha1-+k2qktrzLE6pTtRTyB8EaGtXXf4=", + "requires": { + "acorn": "^3.1.0", + "acorn-globals": "^3.0.0" + } + }, + "word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true + }, + "wordwrap": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", + "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=" + }, + "worker-farm": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/worker-farm/-/worker-farm-1.7.0.tgz", + "integrity": "sha512-rvw3QTZc8lAxyVrqcSGVm5yP/IJ2UcB3U0graE3LCFoZ0Yn2x4EoVSqJKdB/T5M+FLcRPjz4TDacRf3OCfNUzw==", + "dev": true, + "requires": { + "errno": "~0.1.7" + } + }, + "wrap-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", + "requires": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1" + }, + "dependencies": { + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "requires": { + "ansi-regex": "^2.0.0" + } + } + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "write": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/write/-/write-1.0.3.tgz", + "integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==", + "dev": true, + "requires": { + "mkdirp": "^0.5.1" + } + }, + "write-file-atomic": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.3.tgz", + "integrity": "sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.11", + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.2" + } + }, + "ws": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-5.2.2.tgz", + "integrity": "sha512-jaHFD6PFv6UgoIVda6qZllptQsMlDEJkTQcybzzXDYM1XO9Y8em691FGMPmM46WGyLU4z9KMgQN+qrux/nhlHA==", + "dev": true, + "requires": { + "async-limiter": "~1.0.0" + } + }, + "xdg-basedir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-3.0.0.tgz", + "integrity": "sha1-SWsswQnsqNus/i3HK2A8F8WHCtQ=", + "dev": true + }, + "xml-crypto": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/xml-crypto/-/xml-crypto-0.10.1.tgz", + "integrity": "sha1-+DL3TM9W8kr8rhFjofyrRNlndKg=", + "requires": { + "xmldom": "=0.1.19", + "xpath.js": ">=0.0.3" + }, + "dependencies": { + "xmldom": { + "version": "0.1.19", + "resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.1.19.tgz", + "integrity": "sha1-Yx/Ad3bv2EEYvyUXGzftTQdaCrw=" + } + } + }, + "xml-encryption": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/xml-encryption/-/xml-encryption-0.11.2.tgz", + "integrity": "sha512-jVvES7i5ovdO7N+NjgncA326xYKjhqeAnnvIgRnY7ROLCfFqEDLwP0Sxp/30SHG0AXQV1048T5yinOFyvwGFzg==", + "requires": { + "async": "^2.1.5", + "ejs": "^2.5.6", + "node-forge": "^0.7.0", + "xmldom": "~0.1.15", + "xpath": "0.0.27" + } + }, + "xml-name-validator": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", + "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==", + "dev": true + }, + "xml2js": { + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", + "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", + "requires": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "dependencies": { + "xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==" + } + } + }, + "xmlbuilder": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", + "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=" + }, + "xmldom": { + "version": "0.1.31", + "resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.1.31.tgz", + "integrity": "sha512-yS2uJflVQs6n+CyjHoaBmVSqIDevTAWrzMmjG1Gc7h1qQ7uVozNhEPJAwZXWyGQ/Gafo3fCwrcaokezLPupVyQ==" + }, + "xpath": { + "version": "0.0.27", + "resolved": "https://registry.npmjs.org/xpath/-/xpath-0.0.27.tgz", + "integrity": "sha512-fg03WRxtkCV6ohClePNAECYsmpKKTv5L8y/X3Dn1hQrec3POx2jHZ/0P2qQ6HvsrU1BmeqXcof3NGGueG6LxwQ==" + }, + "xpath.js": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/xpath.js/-/xpath.js-1.1.0.tgz", + "integrity": "sha512-jg+qkfS4K8E7965sqaUl8mRngXiKb3WZGfONgE18pr03FUQiuSV6G+Ej4tS55B+rIQSFEIw3phdVAQ4pPqNWfQ==" + }, + "xregexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xregexp/-/xregexp-4.0.0.tgz", + "integrity": "sha512-PHyM+sQouu7xspQQwELlGwwd05mXUFqwFYfqPO0cC7x4fxyHnnuetmQr6CjJiafIDoH4MogHb9dOoJzR/Y4rFg==", + "dev": true + }, + "xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true + }, + "y18n": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", + "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=" + }, + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "yargs": { + "version": "3.32.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.32.0.tgz", + "integrity": "sha1-AwiOnr+edWtpdRYR0qXvWRSCyZU=", + "requires": { + "camelcase": "^2.0.1", + "cliui": "^3.0.3", + "decamelize": "^1.1.1", + "os-locale": "^1.4.0", + "string-width": "^1.0.1", + "window-size": "^0.1.4", + "y18n": "^3.2.0" + } + }, + "yargs-parser": { + "version": "13.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.1.tgz", + "integrity": "sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "dependencies": { + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + } + } + } + } +} diff --git a/ui/package.json b/ui/package.json new file mode 100644 index 000000000..bbd1b6eb6 --- /dev/null +++ b/ui/package.json @@ -0,0 +1,159 @@ +{ + "name": "haystack-ui", + "version": "1.0.0", + "private": true, + "scripts": { + "clean": "rm -rf public/bundles/ && rm -rf static_codegen", + "codegen": "mkdir -p static_codegen && grpc_tools_node_protoc --js_out=import_style=commonjs,binary:./static_codegen/ --grpc_out=./static_codegen --plugin=protoc-gen-grpc=`which grpc_tools_node_protoc_plugin` -I ./haystack-idl/proto/ ./haystack-idl/proto/span.proto && grpc_tools_node_protoc --js_out=import_style=commonjs,binary:./static_codegen/ --grpc_out=./static_codegen --plugin=protoc-gen-grpc=`which grpc_tools_node_protoc_plugin` -I ./haystack-idl/proto/api/ -I ./haystack-idl/proto/ ./haystack-idl/proto/api/traceReader.proto && grpc_tools_node_protoc --js_out=import_style=commonjs,binary:./static_codegen/ --grpc_out=./static_codegen --plugin=protoc-gen-grpc=`which grpc_tools_node_protoc_plugin` -I ./haystack-idl/proto/api/ -I ./haystack-idl/proto/ ./haystack-idl/proto/api/anomaly/anomalyReader.proto && grpc_tools_node_protoc --js_out=import_style=commonjs,binary:./static_codegen/ --grpc_out=./static_codegen --plugin=protoc-gen-grpc=`which grpc_tools_node_protoc_plugin` -I ./haystack-idl/proto/api/ -I ./haystack-idl/proto/ ./haystack-idl/proto/api/subscription/subscriptionManagement.proto", + "coverage": "nyc --extension=.jsx --reporter=lcov --reporter=text npm test && nyc report --reporter=text-lcov | coveralls", + "coverage:fast": "nyc --extension=.jsx --reporter=lcov --reporter=text npm run mocha", + "eslint": "echo 'Running eslint...' && eslint --ext js,jsx src server universal && echo 'Done' ", + "mocha": "mocha --require ignore-styles --compilers js:@babel/register --require ./test/src/test_helper.js \"test/**/*@(.js|.jsx)\" --exit --timeout 10000", + "test": "npm run eslint && npm run mocha", + "build": "npm run clean && npm run codegen && npm run coverage && webpack --mode=production", + "start": "node server/start.js", + "start:dev": "npm run clean && npm run codegen && npm run test && concurrently -r \"npm run watch\" \"wait-on public/bundles/report.html && npm run start:devserver\"", + "start:dev:fast": "npm run clean && npm run codegen && concurrently -r \"npm run watch\" \"wait-on public/bundles/report.html && npm run start:devserver\"", + "start:dev:insights": "HAYSTACK_PROP_CONNECTORS_SERVICE__INSIGHTS_ENABLE__SERVICE__INSIGHTS=true npm run start:dev:fast", + "start:dev:insights:mock": "HAYSTACK_PROP_CONNECTORS_SERVICE__INSIGHTS_ENABLE__SERVICE__INSIGHTS=true HAYSTACK_PROP_CONNECTORS_TRACES_CONNECTOR__NAME=mock npm run start:dev:fast", + "start:devserver": "NODE_ENV=development nodemon --inspect --ignore 'public/*' server/start.js", + "watch": "NODE_ENV=development webpack -w --mode=development" + }, + "engines": { + "node": ">=10.0.0", + "npm": ">=6.0.0" + }, + "husky": { + "hooks": { + "pre-commit": "pretty-quick --staged" + } + }, + "dependencies": { + "axios": "^0.18.1", + "body-parser": "^1.18.2", + "compression": "^1.7.2", + "cookie-session": "^2.0.0-beta.3", + "dagre": "^0.8.4", + "deepmerge": "^4.0.0", + "express": "^4.16.3", + "express-session": "^1.15.6", + "express-winston": "^2.5.1", + "finished": "^1.2.2", + "flat": "^4.0.0", + "google-protobuf": "^3.5.0", + "grpc": "^1.20.1", + "https": "^1.0.0", + "lodash": "^4.17.15", + "measured": "^1.1.0", + "moment": "^2.22.1", + "particles.js": "^2.0.0", + "passport": "^0.4.0", + "passport-saml": "^0.33.0", + "pug": "^2.0.3", + "q": "^1.5.1", + "qs": "^6.5.2", + "seedrandom": "^3.0.1", + "serve-favicon": "^2.5.0", + "statsd-client": "^0.4.2", + "winston": "^2.4.2" + }, + "devDependencies": { + "@babel/cli": "^7.0.0", + "@babel/core": "^7.0.0", + "@babel/plugin-proposal-class-properties": "^7.0.0", + "@babel/plugin-proposal-decorators": "^7.0.0", + "@babel/preset-env": "^7.0.0", + "@babel/preset-react": "^7.0.0", + "@babel/register": "^7.0.0", + "assets-webpack-plugin": "^4.0.0", + "axios-mock-adapter": "^1.15.0", + "babel-eslint": "^9.0.0", + "babel-loader": "^8.0.0", + "babel-plugin-dynamic-import-node": "^1.2.0", + "babel-plugin-lodash": "^3.3.2", + "canvas": "^2.0.0", + "chai": "^4.1.2", + "chalk": "^2.4.1", + "chart.js": "^2.9.0", + "chartjs-plugin-zoom": "^0.7.5", + "color-hash": "^1.0.3", + "concurrently": "^3.6.0", + "coveralls": "^3.0.1", + "css-loader": "^3.2.0", + "enzyme": "^3.3.0", + "enzyme-adapter-react-16": "^1.1.1", + "eslint": "^5.16.0", + "eslint-config-airbnb": "^17.1.0", + "eslint-plugin-import": "^2.17.3", + "eslint-plugin-json": "^1.4.0", + "eslint-plugin-jsx-a11y": "^6.2.1", + "eslint-plugin-prettier": "^3.1.0", + "eslint-plugin-react": "^7.13.0", + "grpc-tools": "^1.8.0", + "history": "^4.7.2", + "husky": "^2.4.1", + "ignore-styles": "^5.0.1", + "js-cookie": "^2.2.0", + "jsdom": "^11.10.0", + "less": "^3.0.4", + "less-loader": "^4.1.0", + "lodash-webpack-plugin": "^0.11.5", + "mini-css-extract-plugin": "^0.4.0", + "mobx": "^5.14.2", + "mobx-react": "^6.1.4", + "mobx-utils": "^5.5.1", + "mocha": "5.2.0", + "mocha-lcov-reporter": "^1.3.0", + "nodemon": "^1.18.3", + "nyc": "^14.1.1", + "prettier": "^1.18.2", + "pretty-quick": "^1.11.0", + "progress-bar-webpack-plugin": "^1.11.0", + "prop-types": "^15.6.1", + "proxyquire": "^2.0.1", + "react": "^16.11.0", + "react-bootstrap-table": "^4.0.0-beta.9", + "react-chartjs-2": "^2.9.0", + "react-circular-progressbar": "^0.8.0", + "react-copy-to-clipboard": "^5.0.1", + "react-datetime": "^2.14.0", + "react-dom": "^16.11.0", + "react-ga": "^2.5.0", + "react-hot-loader": "^4.1.3", + "react-modal": "^3.4.4", + "react-router": "^4.2.0", + "react-router-dom": "^4.2.2", + "react-select": "^1.2.1", + "react-sparklines": "^1.7.0", + "react-test-renderer": "^16.3.2", + "react-typist": "^2.0.4", + "react-vis": "^1.9.3", + "sinon": "^1.17.7", + "style-loader": "^0.21.0", + "supertest": "^3.0.0", + "svg-url-loader": "^3.0.0", + "timeago.js": "^3.0.2", + "vis": "^4.21.0-EOL", + "vizceral": "^4.7.3", + "vizceral-react": "^4.6.4", + "wait-on": "^2.1.0", + "webpack": "^4.17.1", + "webpack-bundle-analyzer": "^3.4.1", + "webpack-command": "^0.4.1", + "whatwg-url": "^7.0.0" + }, + "nyc": { + "exclude": [ + "**/*.spec.*", + "build", + "coverage", + "haystack-idl", + "public", + "static_codegen", + "server/connectors/trends/stub/*", + "server/connectors/traces/stub/*", + "server/connectors/serviceGraph/stub/*", + "server/connectors/alerts/stub/*" + ] + } +} diff --git a/ui/public/assets.json b/ui/public/assets.json new file mode 100644 index 000000000..1d8d322e4 --- /dev/null +++ b/ui/public/assets.json @@ -0,0 +1 @@ +{"ServiceInsights":{"css":"/bundles/style/ServiceInsights.css","js":"/bundles/js/ServiceInsights.js"},"app":{"css":"/bundles/style/app.css","js":"/bundles/js/app.js"},"commons":{"js":"/bundles/js/commons.js"},"serviceGraphContainer":{"js":"/bundles/js/serviceGraphContainer.js"},"servicePerformance":{"css":"/bundles/style/servicePerformance.css","js":"/bundles/js/servicePerformance.js"},"vendors~ServiceInsights":{"js":"/bundles/js/vendors~ServiceInsights.js"},"vendors~ServiceInsights~serviceGraphContainer":{"js":"/bundles/js/vendors~ServiceInsights~serviceGraphContainer.js"},"vendors~serviceGraphContainer":{"js":"/bundles/js/vendors~serviceGraphContainer.js"},"vendors~servicePerformance":{"js":"/bundles/js/vendors~servicePerformance.js"},"vendors~vis":{"js":"/bundles/js/vendors~vis.js"}} \ No newline at end of file diff --git a/ui/public/favicon.ico b/ui/public/favicon.ico new file mode 100755 index 0000000000000000000000000000000000000000..b2222bda2653aebe53f7aa866705f7da7de064ee GIT binary patch literal 8736 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%0A9$thR56B%~h&21QRIQ}CKcq&# z(^i*x?e@x+ugPG1jy!AGyrVw9-?22YN4J8rxJ#D(7XN5~&{&dR*oOqj_n6-73#MzY zUH|y=q=}+xXGqn3hXM#z)`}>AeWq=IF>?RL4t?g|$0LoY(&*!~x^{+smm14el>eIf zlMsc*L4BgA@o!gIR%>~r(XGDhwSSCq6?ioRRQ5NffBf@!)pgy$y0vGI4!y)JRS!Zc zh~+Ww^qxvun1_lA6YX+}9V&_58+h2=U||lBC-cv>i-(V5&^yUTrS$F45Al6@cgP`m zx)+~VJF*V0{H%WYTJ(uyqMjr3-=?E-M~4x1VbS8)MFA3rfirfrX`ZjT=PRq3G%MD% zaz)d;q3^6KE@eK>pW>WlGw z%Q0JsH0zy_xA+-~>BAF0K3@UAW{Dx#Zh1E_8md(v2B2pA37>&4lCwXvA~fIa(Whcn zTAQ^@=ra%5Ey!$sSV4)R&n&xB#+r<+=>g#>hkmt<5Fh}E_x>h z9!gMxtJP-0J=R;=0e;`dyhY-~j>2rrXhkka3sNC!=ZD_jn{j}t{k!5JT(fKIJ<|IG zRjL<|Z)wjRme{Kt^)>hn*=w-v1^Sv9P#}*izpqb6IHu(Fz>(4g;7|V{-|q_-bbBT#6W%D+Nqjv zRlX)991}J$@jge|O0w z>&t9XeqL*)IP%wYalq}T@H+Xtn=8x9MBF(7We~NwFzWy4k*D-nCK?It6jMbsesZh>${>VH7#{ZqS z(NV`e=O@?RR8e`n@=#EMoVJ_!mLJNf9ii$|TJr05sY)sHWd3p&*labOk^K5Fa5C=E z-{*cck)@;&0E7lbv@+URI39Mzw=FWb=&)q2{Jo2Z>QP%hR^wXUt$n1!!b5YYRp5bD zqi)|Yt>YotGZiQr0jZ~Ta-VWM1)YS-8@GV(|DyIE${G#I9)sl;yHL_uRY0>l zcy3Rd=}T#qP@?gNehXp+ZZEu5J*tE*9-qG0lylpuJIgXaHb=|qRQJ;dS0ucpd*_Na zW?@agHp|C2X35LR<+u>Gf!T^{T9C@07)qKz{SVVc}cbS+meZ1lXvNpGM$!9 z<%o&i@lxo^nS&wBx7nht%=WqT<})odV_(sL7ALblzvEf@&qG2|!B(K|mz>^D6*aaNn*AthNuZ~0@0M8 z-1qX|n}T1G=?@#C@YG?6Do*j*RMk@=6xD$5UOFP=QBV)__r(JwDeIeqwA@{Jj?R~I z5V)<>}s?bvFR4Bh;b{!IrS+RwP$2KJ&ztbH8<>7(W`0>NSe0))y$Y6ngPI z!q+cv>K+pMr~^50V>!k3j?N9hL2Q>wX>{}o8v7dLsMLQ+lqnu%D%k$rM|)>Ops7Fo zai$M2aJj>!YPlt#AiqDq$4R4e(jFqd)E~!whOg}H`ua9L#^uj95MEFA+3$71Xoof3 z)y1X`#4dV>CZUGK^^{6{>J?goT~LWW2Q)Xc_!#{tu}u)3N{n literal 0 HcmV?d00001 diff --git a/ui/public/fonts/LICENSE_THEMIFY b/ui/public/fonts/LICENSE_THEMIFY new file mode 100644 index 000000000..7af402ad8 --- /dev/null +++ b/ui/public/fonts/LICENSE_THEMIFY @@ -0,0 +1,5 @@ +LICENSE + +- Themify Icons font licensed under: http://scripts.sil.org/OFL +- Code licensed under: http://opensource.org/licenses/mit-license.html +- All brand icons are copyright/trademarks of their respective owners. diff --git a/ui/public/fonts/LICENSE_TITILLIUM_WEB b/ui/public/fonts/LICENSE_TITILLIUM_WEB new file mode 100755 index 000000000..bbaa23a63 --- /dev/null +++ b/ui/public/fonts/LICENSE_TITILLIUM_WEB @@ -0,0 +1,93 @@ +Copyright (c) 2009-2011 by Accademia di Belle Arti di Urbino and students of MA course of Visual design. Some rights reserved. + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/ui/public/fonts/themify.ttf b/ui/public/fonts/themify.ttf new file mode 100755 index 0000000000000000000000000000000000000000..5d627e701e86da4dde4cbb072995979de86a3db9 GIT binary patch literal 78584 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 z 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 literal 0 HcmV?d00001 diff --git a/ui/public/fonts/themify.woff b/ui/public/fonts/themify.woff new file mode 100755 index 0000000000000000000000000000000000000000..847ebd183be736a7f4e7084546502954f58f9a91 GIT binary patch literal 56108 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 literal 0 HcmV?d00001 diff --git a/ui/public/fonts/titillium-web-v5-latin-200.ttf b/ui/public/fonts/titillium-web-v5-latin-200.ttf new file mode 100644 index 0000000000000000000000000000000000000000..28144d584a62ab0e33b1ff5a898c18ef8222dc36 GIT binary patch literal 27768 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 literal 0 HcmV?d00001 diff --git a/ui/public/fonts/titillium-web-v5-latin-200.woff b/ui/public/fonts/titillium-web-v5-latin-200.woff new file mode 100644 index 0000000000000000000000000000000000000000..4322d25f4da719f0e95d6aecab8b39f840e36628 GIT binary patch literal 15248 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 d@}@nZ{V+{6E^HcxV6s literal 0 HcmV?d00001 diff --git a/ui/public/fonts/titillium-web-v5-latin-300.woff b/ui/public/fonts/titillium-web-v5-latin-300.woff new file mode 100644 index 0000000000000000000000000000000000000000..536005cd0424b0e004726f80f4fad9915dc18829 GIT binary patch literal 15564 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 T5CE`(IZ89o_g`huNWlLAQz{M$ literal 0 HcmV?d00001 diff --git a/ui/public/fonts/titillium-web-v5-latin-300.woff2 b/ui/public/fonts/titillium-web-v5-latin-300.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..33b815c807c040e70a91d0e24587f8c0314258cc GIT binary patch literal 11656 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 literal 0 HcmV?d00001 diff --git a/ui/public/fonts/titillium-web-v5-latin-600.woff b/ui/public/fonts/titillium-web-v5-latin-600.woff new file mode 100644 index 0000000000000000000000000000000000000000..4273a4dd0a20e2ca29c04822396e047d8f9c8242 GIT binary patch literal 15268 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 C-rNWP literal 0 HcmV?d00001 diff --git a/ui/public/fonts/titillium-web-v5-latin-700.ttf b/ui/public/fonts/titillium-web-v5-latin-700.ttf new file mode 100644 index 0000000000000000000000000000000000000000..a6b6f0b347bf645b7b9904b093524ab7f5570af2 GIT binary patch literal 26592 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 G8~#7=yx0W* literal 0 HcmV?d00001 diff --git a/ui/public/fonts/titillium-web-v5-latin-700.woff b/ui/public/fonts/titillium-web-v5-latin-700.woff new file mode 100644 index 0000000000000000000000000000000000000000..dba86dd3c95ba421b492a0e7df7cf9f6f6847124 GIT binary patch literal 14684 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 literal 0 HcmV?d00001 diff --git a/ui/public/fonts/titillium-web-v5-latin-700.woff2 b/ui/public/fonts/titillium-web-v5-latin-700.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..3c7211dd140598e12d778b3a8f1821be1d5e9f72 GIT binary patch literal 10960 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%0Ao#);b?Xud2f0n3E^h-pD^ z0a(VC)F!bt_sasgp8*S$tf&5+{%{mUJ_H8p22Md_YR>}5?hV&Y-DeSt95Zf%f$RkZ!aYU>nU*Wt5+J*99Lm)+eS48xS8G>85 z`p7QaMtClV(-v~zH`|=0&BTxgw43M|oV^&yjCW zE$VhXvXgho&( z4vk=fxpabaEp3F*puG{2i?`n`1OeHQ(=gDeR7s;=6^1M&iU{dcD#pX*%a@-}8Kknv zgx27Tn?rh=dKigh&h_-_XhyHb=p&{`e}aL665~tCE0Te_(9@74uAq~GRSH!oIhRUt zdx}N;zyy-OZuQ5Db8KrlL#T#;5AxPg)6>lL4C-*9?i0zSqgTd9vRH|r0Mso4U3P!H zhz5mw0mCp$qw(l_1>u6e)Y3Gim0tn>~|%8@te#3Fzb?kl)BgwFdWb8%Td#jUbMXA0|5!hIfPMhBP%ak$ literal 0 HcmV?d00001 diff --git a/ui/public/fonts/titillium-web-v5-latin-regular.ttf b/ui/public/fonts/titillium-web-v5-latin-regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..e053d2e505a0186b6ef6fa90eb00aec4eac54d3f GIT binary patch literal 27920 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 literal 0 HcmV?d00001 diff --git a/ui/public/fonts/titillium-web-v5-latin-regular.woff2 b/ui/public/fonts/titillium-web-v5-latin-regular.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..a421b4f82eebd1a4a5b75ed0ad29eec26abe52df GIT binary patch literal 11608 zcmV-eEvM3VPew8T0RR9104-Pm4*&oF0Bzg=04)Ci0RR9100000000000000000000 z0000Q78}tT93Tc@0D(La2nzEs)LjdMFaQBI0we>5S_B{kgFFX=Y77S((OU)F@g4^# zhNnkSo5Ls(Mb+lF?Eg!GHbzLe9a{CmMVKaH8tu{O+ zWw6sRcNw*Lv5byM;W_-KU6yO0K7V|hk6QR`MEwjL=cDElM9Vq*lvY(U4v)t96P{)b z7E)z|k7Rg$n>|9r0E4s;t5Fmci})bXQeq@%V`0>E(?#Fr+E{oyU72@t5uuB8yIQ;T zZPyZEOE$ar9*ZOxi!EK)(o_`^j}xVNjKm`>SZ?Cm)-PH6s$nhE3k-VTgSKOsxo=-K zx!6VZ5~)pC%|ATHc<+x4z~9@9c05{qpotT$1Zj(+(axh7-p36DCPba8Xkf1J{jd$cmTmlhdac&GOVLCjQ3(Q;fE2->a;E=AvJ3B7MO=2v0fqtF z+E{U%)@fb10ncP_Gn-rKBL|o*X<9ygz{BNV*(}qYyZe{^d2P;~WQ59zqIYBm2~b3P zy&7p}XIHqVy44;^hL!I1cl>}0I)UkdOaOW|A%q_pkRr#9KMp_q+~y#Q%=7rG`q_W- zQ}He2ljCS1v92j#RrS2aRah0q&!&BeS54REp?^uji#9sT-C`*XR_ z9RVSZK>F870O|n9z`)#1cLu2P6$QSzEl~sP%~@R~z(N9e#Wb$3YAOL55YQV54m7u2 z{Q|JWt<&#=l=N~jWsUHmWE^=ZnbB$yr;)eM*+0Z}Aiv=D;4YHBT4kg5;XbvMp)R9t zqu${@mH%t)&fSEzMf;*fcyIJdo3FNHe6;OrJG6Z;!9?0j=pxOL9qm704wJ%fPPLee zm|x)#6zjoA?U*^NGd3|lBdp9J(ZNJI?}&Evc7mKlPI~f2r&6aOa<9_?TnE;WzT)1&ZN zlv0pQ_&1g0Khr2Vg;zcfN|gq#RG*cA1UlXas}C%e+R2-f>rk&tLb-E+u!$; zuK!!FrOooaYrk^FE_-OU=U!Uv>u}zX`va9q1{Y7`@8@e1s0oEcD2WFHj3L}GX*6RS zvn(rCty#BOw$!%m*xk2#aDprBml!wR-9J8JBoWXB4gvNAiB&vYi3lZe;wlN;J3KV* zD_ANSHO*SfX^T|nl`hgf>q)QlrJpirY^V&Yjc$*t*a*wGm9T2fy3OKZxp{c`_ytN( zCL}B(x{dTq%TY*pssRcBwb%0J(7wI0;qx7z8AIYu6pv(|!*xRFOV+@0DjE#wzg>{BK zvsEl-1*_Jq+bk}Gn}?T=U!VkqLc$`Vr;%z(^hrsj6e>L@gRGpq{c<34=*V$7Q7b5= z9H&tUjp3`O@fCzQOANup&id2X42axz#kwNW5aXpCBD-BKrlU03oDnvPqen@SBuVlu z&JJm2*%QM)<{j4gjLwt=bR+iBUXkiSVIA!j{y)@3{Su?^KZ9e*e!((kRi)VdI2u z$Q+PIv~eizzR;ij&a&U#|5cVaDu-bWlftqF)B}O)C4_2XISES2kg?>j_i^mb(baCR zJ7wt+6BGl;XeMSB*3SplV@JJ><2a6c6k9xei7;NlaU92S2bD|8Rk^k1!Bd{cH}DBb z)jyciE5WqCR}$wtF-uRrtfb@RxFeM_!jD!Ha;7DlQ{Lh{kvF|$??2(h>b;@TI zlCroXtNDI?MT>?bVHln;L9CoXZC_lz#5oO#RH|?Z^K9v)02AjeOW(b5H>`|Wk$%T*JFeUk?%DBjWIkiiiw$pbqxb-D$CWZVAYy+o5dyO z=Hcbz7brnMNLWO4Fb}FPMG^o2004kt7^Z0n0f3;V1D8B28O8^LE23yoP!T>XxiJ65J zyl<0O7Fn@s&AQFv0=ap3`S=A&kS8Q8BHA}O^gTGl@rh9vmPH!G>Ar8>DHPrs;#r7G>Lx-4C@pcD>_V zZa#q()JnAD%{PwSb2Tp#g`th9g{5tPuY?TLL-?ThA7wvBkrYd9NfQEpOBvq8XeMSB z*1)^iC76(eMMR%NY70ppPkrf-k(Enc_La+}maB5B?ZMN7&jK$}7@ux!y%$pz-_H!u zI2!k^JO-{p_q#_(xJZ%6I9q}OVsVW#>@}BG_4W>OR5l)y7Btcd!gdD}b4V30A!WM&rDHH@%p zb5-MphnJ6E;4(3KQ@j%aa9m$szY3Om2faDRCJDQ@5jE06`gr(0nIxq;CZJw}Z@*Lw ziLUw-qu{6rYMdsEj?|sr@w8rHLc*@g#c`X5mychd1PLKw5z*eqla`EZa+0t6;XnWf z+OtGEG)|%)%v}*Rf}jqGg+ieU5r&1Z5SGdeBq9YQ$50eSQ4}$PAP9n>W5f3HQDdw` z3O))93=9k)s6#={pM#02B1h;k*!bRujtQ`P%?J{!H(F9LL7v5=I75tLVrF6eVX%-L zhN{D(pM!WRI-sXerCJTHT6O#NxK|>tz3~=LgGNo7Z|B<}F!bxmC+6RhI3?v%2q-~W zr&!CJR%z3&L#M7m<_>~@2m}DJ!E(sJV}yyBg>@g+VH24&W!j8cbLK5rv~*NR&Rd4I zv150(JLOo#so5pq77s5Uzrf`>c?lFlTp~%5$|Lmxf*?2zoT8vO1nPE(98C`!BAlAc z$M{gSpU0`$nAyIQxcf*Lcv4Jna7f?uYv|SZ5TT}sj*LUcmehPix{e8{-r3GzbfPNM zO4w|LL#yIT3K)rh^Er+l?a=vmY8%nQIX8sq!9|ZPvi&0Te1qUUMNDtO=c*j4611t#o7TF5ORrXxp%dDhvnxRv8)wtMXl{ z6PV(kR-wWL5}Fk!ww%~=fr01EPl`}1fp~0**^;m&WlP4EoV6TixdLrxYd>oT*cxGL z6v7F%PO)_v!Zip;7&zXm`s|Em&u+o_+p$tOW3iLV4z4A=GCy$`iTxY8h4?=F5@3OX zgb3|NwS-GVXcCDoX6PPoeuLl&5h4vHqP0l*z&<)Mr2ZTXgHRMjQPfnKtYpfj&6qW3 z-hxF-vt5gW`Gip**SIOSlsor%c=Du5>}>`arG|Q4GL2BST?UW9g8X=etCSIfUI=F3 zw;KAaB!VmI#GcuLhCo!0|72ZAfyjmGtIkc|(|I~FThbT@;YR!zlxi9w4?%lQBXxf0 ztB_1U=DgO0>-t7Kwk~ZyAHJ#3S%Uj};|9`KpNl}Ra?}Ld0S=X1ZE;|)gG)?8Pqg=g zL;6q&u?iw1j+T611HCh_0q@s>+BgA3&O6h`|47<zu3f8dl@(?7=C@)xSEZJ$ zJSt)GHft-WU14`(QQI#J&@^<~jWd;K@KpQ_5LHWwND5MQyy~TG$d=b)USiDjwwa9m z*{VR2=X*W$Zm=%vEmQ*Jz({3UkHO}tLLt`ICRY>!1x@IZzciNx=(d+_wmF|sS^Yj6 z6H_p$`C2Hlnf8~+DrO8;BoOypryD0zYxZ}O(UBQLNN1-VztXTIH?r2^lxg(#>Sz1E ztInJGA|@Sm+--|3^Z{_uVsn`un&;Li^4h3Cdj|s$lLfQhsX1*_9;0Tkt5}L31azrM z1~Z+EIIp4Y8#!pF)1U9q4mY?jwR-KUo8AF-d+!Y3TNYC?p%qrSi65}3xwyD;o$SxG zHgd{$v9OmoBdkmUV4B$n>sWA}(3DNqA+of{X7s5!E&fk^%YOF>>zEwp(mUY4{7KyB`mC#2bZBm?XKN;{D(t`}cFcWMP? zYTbnQnRVnRNCP@*)Yvqls+%h2Qoo?mA&850B+FFCtT08%+cm7wp_yE;?Y7K#e%6b1K9~D zJ-=i@S(c6#RLCE77|5A!AfFf!spArjQ-CtmD^|#uh6r6$_bb&)4j>8COqC=nL;7Bt z1*>cqbOQfBXvLQ=YyJYE1&d-QS{ytkCKrj4xPl0{6VdZxU_g^8OQZ@F=vuXk)@hR% zTlACIVU)_agYq46MzIU7t2J##3m7Zc`2fJBy5?#dd(uBlVAueM0OZ@ML$QXweBW#U z6}Ify!vLCq7Dsr=9nNqFh=36aw}ZN~@ijo<$yoqje*6Uhc7VNLxUt7Jaw3%jgTM|} z6i(g)E}#NK9U!{{E+QOa2R91911?}CKn>#DQ*IE5NTwR##T)e$w;;QS<8aJw1F^Ra zB#X-dQX`&V1Q3FQz&pN;0Yqa_00G^|9xnyOG_Cz^dM)(p;M>ZOTQyZv@dTGoX zyT;##?9mGf=v#mT;R13{TaM<5%FTi4;6=ep54eB^qKf0*Q(h1-Na{*qIdC~8Al?8w zcrZYfJBsg0^s4G|^LAh@DU7Y-z6%U>h6gD#i02!PtQ|x?W}74Z^-}j)1;&XMA(LTdO;t~ z0+=}!U~}F`H{=5#o`84&+8Y22KQZ6jJ>gJ8uxiv4^1-iV#9H?&T!z8w6+T?MFa*b^ zcoPRatqFlVFd7&;AgwB4WqZz91f)qZ24g-SY%v>_5Yi93WnF5uX5eq4|Od4;hX?t<#=>8L=E>i%v~+b?1a-;vtM%~^@juG`r1T_3noVwAD9m4113>Q z8by;ofwqF%YQ_iF0v+3q9?)&3?_gzRgMk@z; zGeQj|k&*M}rgYngl~_!bo?Et|#67u$8CMEn@|mW6ee>dWZc`}#p(GfZAS(JRw=@$C z5*ERrtmqH@iwk;eILwS+d@&cdrm8nFb8``%zTPy$kWMV5OC9A@ik5XzmTeqcEOx5g zTVv2MBd8QHMI65iw$?Ua9c#ucg0);|a?)^@)z)cx9Ke2TPWe;j>hz&2GKjtg?}O%l zJZ<+r@aIQTv=^VbIkeenVgEJJkAM27-?>AcF_RCc8#7Yk@M7~-d1(BmY`s&z=JX9Hng2#XGH5XhKmbTpjP0TWFx@G3XSn0Uq8^1*pk^rr!Q3&{bd7#) zGyg5+7Em!&s#xPuX)hR&6B5}Ji#UjNlowyAQ-`l&!6WCgZ2H%9I3MzxvL*r+rzNbb zz=fjNu$d6IS|YgIol6Je08$mpI5oQgt2LX9JxTwD&%QhQMayQ?&hEu;%swwX8wRP>s;xIQSE+0{bO zJIYwNduuHU%uRE&gfhCTiK$8jZ)G2S^Ro{7WD9|kqsod6qQd<_T#uG}vy}*ae8Ghi z!1WEm^PDMja~?a_VbXXd((B?@x_I))~8(hPSfW0d|zKr zhg?Q(dAa*~49G+k;|5cqrD6nb?ENl-ZWKN}1}^3HIDi8N(_MLq5e{ybo7>;qZzu6p zYO8rst`>*fM75G{(a(t$xshSZb7Q_O0X}33JX@njE>eI{2yMhLm-Uq_(=!e%xsn0# z;!*Pt9$a>6|N7(mt@}UUWv=FRLezY2h)emKp3!jCTA*3~{at(Q=HsR8Zo{YjUH116 zoVO>LSvNI*M*f89bcH*|KRBJ={xRkPBvaz8O&kHTQme|>X{1*GLppJN^yWT+QUbZZ zFoSg#!8cXn)?e3dogLHNkUWA?j7)WKQQmXTza+L#SV8YM(FwoXGD&7$vop>hverOi z=R}RsxyeWU(z&3z9!{lSsPr7rzV*E*?5s#P>-d3 z%p61;F?9Yr$?90%s!-f8Cb*s{=|O@E9v$pvP#4~z4hSsE z-(wje7FtM-Xyb@s^Ty2vbDRgv|Gq{{ed3|NEJnz5|CQkU@aFe>vevo>>~#@pDi>2E zpQdEPF5-*S)qJOjEmRA+2Oi>I>Bg1uK`h99omJUGq=&A=2$}A$5*(w#Tn+>9k}avz zq-GEh4g@a<0j=3MVbQFU=Eu`N>I2yg(xUUWp0{zw9L*Ir5#V}8`E(&^mXeu>;2^9-Yj>gc2-{kM^VzB z&xZ`jwa!1dvsnyMGa>==r3ImfF*G@Th~O@q_LmqT6ELT(d~8B{_nvjQct}AVwqGPj zTuAi;|Ahr@vjyzw#oW2+#q+TE)JkqW1q2D<@*kK7HPNA49wfFg4MkvL^zSKl$bSXh z9BN&S5RvAbbD3D4lHHt`rfd5I_Z zw@B$yjtICcKT7wBLLHI9Z{>geXJq2=*2CcK zsVlb}Fg4ytOyz2)R*5YeLH)}}(u%ZWw;ie+JLry!TCrJ>b&to(T%tTR`w1h9=ji^k z?Hc?5&EpB>BYYLuinzLO{?+1vk;ltZ<9WsLywoH^fg!9UrsV0-WsjEg2@|lxN6?wDMIvMVkMn4LMeM0$|@tRdqC-YOI2)$>y>X0=?2TAS+_k`+g-Tz&It=Pb}QrU|#S&8ITi8QdyA)tLBVBCj}o#CH3 z0i8IEAsWbUWMPk9o zxEQ1^%yPSzqcnsseRfE3es@nSOJp?Vne}_;{FOY)`9&B$; zslJZtlX4k)Xw>1d{^NzJoq8mN9-N|l(fo}nRTW?=BQdu*j2gR?D^t~7c}$64-c+@> zp}i_ZMRXLO7}+lIOJfLKN{X z7-92i#Kt1tKkx8Aw4i!vR__Y;Rq!**- zrRl$?Aby;{|4j1)BXaNian`<;=iuw42|rjE%E-nyPvKh`IRzox%Ls5*p_9Clzp!6! zpW`PHP*>$R0m-+?Z^MjkPf43qCzoj|xS*%eqxXdF83UTh?;-trcdq;2dI)GH`ynOP z1M;8%lRNK(x{Xw@scwH+-GL2R1vo-P3qGS(-W9{{!5Y2*a|giVbK|9xQbsOAG6^)3 z(n;Wy$`dRUi6lU+{wbGhy%X#_x`v!;#AuTKT*ggZ#=*!M1ej@?l$vSa1}n}8&Incn zdOr4|Kr4gT{N;;aE!s?|l1#3$H{+_nlH>BOXR^*H<%(Yv%WtVH-^W^pD;h?l8b^Tg z<>)KvDHjXTx_LF2mR#*cA#yf zlc2KFSiZeIu#2o|MLTd;`K+Gs0l4Ok)hST;Ot?;@y)!GUaExBC#3{9BMI4K!cdR4l z$+8YV6&kU46l=L7u&?BO;oUMz%%Jzi#+_<4U;ew~*ShAS4~<22ze>UEgko}HpPY@N z-y8=#CKML4$I21|8{ETUmamvtwNR#>NvlA~#DRd3fVA7$x7E7+lu=6B9o3y&5H0n2 zyjHiDyO*nbv=#)Lq0@#E7`~KuI=vW11k^e1;$IhYXNo}&hT}$`;JAZ;;W3$6`&|SK zCa<<#VXK%@bC0WmF=HQRA0YpBFOTzB4Tds-uPIpGkRD**rApTcA(?eAHiUk}U!k%2 zHL!xPMj`r6iDjvmY3>I`)CMMjXZ}7;e0T-@a7kNuU#DNoAG(Nc9lLLgR(rmwaa?iSWwjzCrV3@Z-{)7$rN{U)QBJGY$b4$|9-%G!bv=TW4FlZlOi`g* znj6M5`(#>zZlJU`W@rEkDe@4OA*~BgLUwaxLu4}ttQTJwf0g@*3Zb&)XfdidP{K*s z?GFTB#9u^(I|cmpX0kI1q8>;?kN4|Dy6UqZk|I`L((HUTjM4PGkimg$u3iW0 zgG=!dg{Ee45XF?hLa1~_d)G5S!uH}h2L_(3MvCF({t^zA<4i_NDvHoSi$p4y%7IiuxEK7S!`kJb&-wU6UfeM}Wdot+W+io--Xnnq$_qa`W$>9Wlf2p1ou zbf&PX|6AWbs>ahzr&Yawk#uqExL2FJ ztQ*r})~pBwjdE+bqzdxuGXb1UMm4jBr%xSCLDj~g*$5b`5TRBWP)#BDHAyS+A*cnJ zYPk^sSZo9eNN4wn_JOT2sM?gHr>2KlsOC5ndz(8dSIaX|9GqR5v}Wl|2DPFP0s9Qm zp!u}h#w0(6o!$Q143C~foDbWs_ZisonHfEU2?|RlGA-+g)|8H{XEz1h{}duaMTwZ4 z!2^mGROr-q4dAF;Zkt6?MD1norEHZr+)z5PI2sC<&5gBi6w2tmYS3IbTYikkhj_fJ zrPp~ph{r!xK093spEq0nh@ZMwn$6`t&F6qPCVS<6l6Eg<7^B@oTFO39107@^+$G(0 zh z?+N+9NxIg4y%h;1B`hXC<}(F89YkPPZV(tc3_=5#-oCN##fIMBd-|WR@0C@ZZ0$PQ z)^)P2`$+C0HeK|6w219@cU`oGS6YLX&L6M9SuZVhhqWPmXz;1a;HkL@z4c!3jM z;nJK{QE7C*@P&@G^V?5ACl>G*96fv*It|{z7v=66+MZBuf*)j$vb)oV~*`P>f;V(s?g$;ISI4b@^i~qCFGO9IxHB+PYdU zWUVYV?kp)%3fY2M!2LZec}6^LH)MrRjz#3Vh5CDk24@FFC8Pj*95Igdi5q^skO!hg zQ0g*Hs&vbezm_yKEv;9Y zXsZ~|G+%H{jMOvJ-I#7Pgkx7p`B;4>mAggcuHR;2!&71V4{-+B!|v$Ar%lC(9XD8p zIj40o?Os9QLv6Nm+0_YfVMqF@!S2QE{5aK0xk_I4;fx$5w8UVD1y`*7=;2K>NvD3=rK`Xu?G&|5-(@H-glV&{R`;a1?Zcvcp%Iny2dplRLUcuWkgIiHHy;hKH%0FEJq;=Zk_ z8QFna2!)8g#qN$ct&jAH-uu2^*#B5 zu|O-**8F69Y=~eA$B-&6dE$ZyMPYWt%qfSN4kC7Al-jP1F3GD;e?)ggzgLeR_z>|S zym&T&$E;KCRSuJX{3ioSxR7kCGBMw#!!9U8$H}z*1k7JQ&Yw13Hm>kCRbMo*X){} zloMCFjt9oDkF#8w6(dNEmVv>`lmC1qKf(^$8&n9SdhIc*5`@)nit zZYis+xlvJ9$QHRSMRj&cniSaV6fMu`MFCFWRmi0PbGpw`zG|gFv4v*|tDE>3$|MA@ za9#q@X<7mKZoE{09lIgE63{f>Nb7$^f-@{~@jMWOM<^~L)@)|S;E>-lyN?;)*3fs$ z?%D_=hMl{!fh>%tU8ltf&;D%`8-ewI0Rh29&cQ|Lkz=bfTAG6bf-9Ut%54K`^SksB zS<8|!RTGZNuC*qfd$)$JeY@eiLj1^}eC}>Obop~GCKeK?NdgD@s^2&MzDjF%U<_hL z(zPqFoWZu zNQ%}{lDr+f!+erF(OQvgce+z}K0d9!wA$X1l9Y&Hr%+b_`x`p6m}wk8n2WwzawRSC z4^2V51bE!p0pNKwA27dvzYT{hOh6gD!1FD)+{S5ZpJ3*`UfxK)R~!L;0sqos!6^;d zc-!tB73?N%(16}%zYJomwqiZyou5GNucuzrIRP5WN|J1WQbduWu7f{7zzMIEbnXfk#l=jXcd> zFUST2OTjwsh=UlcGlqLC)54O&tvKBL!li%n%vr?z#w8a7Cl}dsvIey&mv{A#;2}nk z0x`=;UULcgEZSUOYLSqIsBXg6BKvl&xjqG1PF-xsD95TjaHX)iN0fn<2guRKmd>m&Oy#g9DU+Px+rtKHOHC9 z>en{lEM+(zp}Q}}Lr^T}X%dqn#5`c`9o&QoOBgQj-XF4Mo)`)a7~liM8bAgJFu(x- zlo%ku78?NL3S7J3uv}dT$pT$S!AveI_13yj0)r}q6`gQuCF)Gd&mW%>78R&;jPj`% z6|pFnr$(f3G&E5XXo%^^BoyLZo-nXmAs;mn?V#4EEwE;lhMKBE+Ef`MHX#}*u4r`% zR{HCc7A`i^yh5Hb#=K?{URli-1&SdtUzRSiU0Ps^)g`JhMWEIynAs9lTCH%xiE`o0 zL5g-CLODJOH~{Z^C}#i14->)8z4QJ65qH84|G*H%rxlZsxp0*|e}KmC^NI#ee~E+$ z6((E+nn+RTqQzi{6(=53f<#G@rC?#>;DYe*r4kSl5tERTkyB7oNu#ErrK6Y5AVa1s z*>dE{3mojyrQ0zZy|mbBEA$yQXt{?LS?`n2R@r2!GoJXU-yVPX;;WtZyX1lc@)cOC z&=p1(U3SA&*If6C$t^cc8&Twg@7#6U9mU>wYnf6d%2X&yF0-(Sa5fD5AJTkA;8UZ&bjBj>wfR~ zrLH`kt=53)Gk)4eXt zU;vT?ns#>F0JIiN&mcCxlx z&nGg7=m*uq^q$Jlh*C}S*`CQM;?`IWr( zc#Wp;l%3PW(OdhlL!%Dm#%@0tg><{8V&D=7bkci}iTk+pzxj=)DdMw`;+kk%AI_AUEiWPNHSzO%B~L_vn09!`TLJ6N)ViinctR?P97xuPeub_% z!hDCXyc%J}uwTPCH~=(ka$rU~zci)@PtsVt`UtUi1hfPMMoFGLW?mCDJ+nnze*Mz# z?{gHJvMhH7=LY1DHt5x@rS9%m`ZS>p>n0X8#IRjf2pY7AOmP?saCA52=Dy>8<<9j> z%cROQYB%ilzF70V9@^sP|C$3%&55774QXRb#T&QO(_=rkvB?IaU7pWY?J|kiSyDue zZMt~a0-BRV%%6FP#sJ{mDL@{o-yB}M|9&S57{kCOK2M|^!+r=1cxS`h*7^o?0Rvfs zpO%5rTQKclVK`tsDK@2ly2Aj62+E!SEF(Z(l#Lepw3}K5zR^G1n1vUB5=cFbpcHts zg>VGH)8nI!lfeZ=QF2*3zwKB z)rnh^ji%T-tSDfUzOgJi4J355^oSfcBQ+aAD)JzbfZJT>tlUOAyUJ~vT%FKI7o zPk*0$pV#STZ*lZI;Y)lQ{Spn2m}+sF${Jm5lESE(9P~cvKH)xazpA~xy|X>?x4K8p zZ_pFMllU*3eUdT1!g7^~vd;Nqb#zx`SJZ}R&(!A>sRF#YcB_#e-aqkmiO!ME(a!@R z40?3QwEYWew7!{9NRJ4P*pHCLtfohy9ihWxc%rXO!lu)pBhqTE z;w)6qYSf5PpHwxhYFBgQCu1a2DOQTi+7!n$$H;5zfP1J*_%-+n zYQiiB+g`7`@Dsxm{X5Vd%w6V5E@vA48@>+yDOUz3@#me4gN%<{hgOSLIdg<*r5|Q& z$-PWQ`ui|0t-t9W=+5ZgG`M|swL-Q&wSxV8_}P9&s6a5UZ~A(6WPYX8S*^q<_510! zvAOk9FS!TQV*I(x^odI29CMTt>tpES@LRVd*OTa@%%h=WoMWCNVa_~UUV=hA8%`@8 zT+VqeBp!WZZF~1AnaSZ5+CHoL8R&wCLFz&HL58TzD5JE}OsGuEjQLCf)}nOJjDz&W zj15+b48BbBbX%@dcIgSCagOnd@oL>X>=Y#3;o;3{9Wou*?WNGmmxsWwK zHZ!_eyFMMA9A#X3F5j{6Ff=hX`Pi`(rzvbJt#B1B8Ej_nclLD*1s=!m)UFo{eVj_% z*ExOaAM1|XZCE{RX-fZE{NQ`3CB!WRk!vb+>0{u7^W6CK$=BbP$~XIs{>||%`7IXW z7IGHy1QG_42;va75gI)}EN~Ha9c2;40(J=+HH0^q5s8mL4mldW3dsj#K}-$z1zx&O zyq70PIE*!7OPW(MV0dd7IUcn@A^%stZ>C%pK3Bu%lX|PwBj_0%lOwizlluXrq38bZ z(H$g~_%sn;Vzr`jiH&gHg?d=NF@B?QRNLw8XOPX1Z7)19Q*-kEb+Px&`fZL|@ig;- zwZr|n3XhuOV6vI>qqD{RI7U6DWvvB~ELkCC;hFr?c!b$PU5HxX$BK`zE1hQ(XQ_lN zlTu2;OWAsZJ~;>w-Swo_-;gQXYeVsZ^~IuWY=e7N#2?O|!+E%9*pEvk*cj zCAmq*P6XjdKexuovzGccXC|jeXRlxSH{AQk2x8D}!C?*|HGD=`#Lgciit}?NVi5%p z$`ORd!*iX@zW>sHTZ{2HxY3;OGQ?GR9P{=T^74Iw=zx~T5imUJa2O&tQ$5MQAApHW zWx3D;HJe?kjm}mUb>XaFCS!HzrPNfKx7`1t8uRi)_+8ei{Y_iN+beiA!j|DWxji|% z;!Mj{=dK~>qf^U#@sIY~o6q+rW5TO%`qwu{89%tPzYr}gR#JT5t!``BaDLrj->>Vf z+d$qXz~^D-NwM*6vR*8f-%lKCvMsj3w!J8WtFYQIX*OR^nM%e>=Be;5$1iUv*V5?P z99{9O8mu_j;y$-6S{1TAwC$>=aF8~waiY8IzN$+4RPyfirR4JR!`9clr@Vz8BIKx$S@Es7qiK?^;@_U94oBPf z6qC{@#9SYAeWH9T3(>8!mUx$wX6EOz=GS}#_T0T9^~P#9EB#jI%9nIq#m@VGCMG40 zDaYi{3vkV+ta3Yicy+OVsl0!&1Wm2+^55AUwoE(YOu=ReJNLZwmPKCR=&`gIe(+s+ za+<>$Vu&U}AsX`i`uM6{VrsdoQBpBsz-+*@{;^fr@TT4ExpnjIh9|&YzXi{+{zY@G ziodC3^P;7%fd)wjq-q&++Rq@ym=Md?QRPX7>numg$A;esekRVC=P5N!iPUK2z zO7%A*lBk2In1ebf#t~IOAhM!78B=g24@~J=ultF`80%m67 zs|pH?+(hfX0%f(~EK7ft3D9uR)hzC(4<*n-AWYc&7B1b7D-9cKUlo9eGG zP*#od3IG7+SgUKhXe%mwFtxX3{$yrvV$SSt>+q*H03hK0;ZM=l+~pIgyRD6#^9Of9 z@_+RB@TdG&H48cEKf1Vl5hT}ER3R0&cQPmCV&-IKB^N>4qc0%@0hWL3UI+mPnbi8vau8ZeD69XuL;STie^zez&xQ7%_djI_ z@jNO^wnG3w1RyIRs_qUvGk|TzT4;H-JP2?p3M>ValLr`_1w=$>*EHdph7U?ahf}ki z@9%dcm4vfNVnoZ%W6qsLU?I}G4TO+Nio~Nt$Q({?*jM>Y=I|e`&DjB&EiNwuoiCCE zz23?Wjox(`v^z~Yur|G+{vHW&pu-6OZqxZve5L;$Cnp3k;1^9mEckCww>$J z?QfC4w+%9A{;cTd9NuTczsIMm03`sJH$e#|3Hy6`x{>IH6q&$~-i-Wve39W`Sg`K+ zw=2kN^sE1sXrsXEb~VEhu~d&%zT1rNO|c^N(%ollY5~yolxgpV^$#7j{6EecF?H(2 zATaFD6kpDt{;@^856F$1HVwuN3n;x$+~8BjpT^IPoG;?emo);EQ@JfunM$fzG3y_! z;EOvLd+H>aEcMN$c7S>8J=gXBO>5k?P&*&Hn6S5$Coudren37&*{r**=-NEkQf58> zvNVg$u;;N+{K7_K_>D;Ft09Gn?Tu^aj28l>&>K5Ign`QYS2bw<`|1(J@*1}l3<>3I zS!NK+hTh$@$7C26sJ}s@dKYKSn;ebAJ3}Uv86jCK3N*cj5F=;D|=_g4+r6K`A0-8VB@L1Srj7CedhCH`K?X%Y7z>bM%ik( zJkw%@n@D2u``Xa}QscV{tC@q5$Bis}PlELflSpkpoz^j0g%3-e9xA5umMmyIlX=_u zYs1mHbKngiE;8u-_J7?`d+t9`GgYHOH_E!|2Pt;+#Ml`GW}CCDqTwt+nxjuZNW>h6E4qFtT$R8t3k8uSXGhz@k_4mWu@ zqwMjjmgRNS4=>7aWG@=L=fhq)j6_bsr)d5nWfTPgrlCrEb>l!Ocha&rd>uf+i#a@s z1a%5_{zAxffNzuZVlx}d*BJZF1$mFzHWW{eXeoC)-o>+hRI&L37F-^iY&0v~{lAiN zffOJ)y@b1x-MIk8o?|UdUONU{i7YsIgQ&9Ts*XlnNXVtB|DF%@ot|`tfG!S4`M&cvR*H&eUkA4%|1`;&G4C9 z8dSCLB0J?S>43TCC7AN}-2Tw{p(d8U`(rw};Ikd)G0XE&nkvqYkdSO|>X0UvR8!oF zTio)iQk?Mt0jTpnEVErzXu>WulBhWrs(vHsY9W+olzYs!ABm89wjnl%W$mLqTbl+d zn{-z7T)_+At-1o~?IZL21;og7EVj&TzwO;KW)18_%DP^XLLMymgoIjn;D1$Pw{#wr z#2-bTm%w%SeTOHG&lxeF1ZE9a!vnnD9TW zS%J%XoQH|M>C>p|dpMsg14HcHh{)VZUL_8cR*yi**Bb>A&zGGu;5s?~aA_aeLesVtq~S1yKM!o839j%ZhgKx!2o=^rYyMK3d~$#t>vr1if*q zO*`l{>rGf3JI)_C(i~EQcwy-qkwgrz_zO)!3Ho%d2CNXay%1T3(MdkKgR(RnIt@z+iFz1L;3*SIsM+}qo z13M`CK;=};bz6Z0r}#zNBC^WHa(?@hfOGyp+m*V@g5b6zFcgr1(erm>@5;nm6Yypn zqA)oY$JER3v&#DcIFVF~2hGsUnaUFZnq*?$-B#W6jp`v;l>DZLF{{QpJ(wFDwK1}Z z)?@j2b$J@SZr4^nJr=E)d#>UBe%uB@X=*+}A5IMTQkqxvFwB3wF`D@W(dv&`0EF#9 z?fGw;+83%$>V5`tX?N&;^cm5X`lG&BeBZ=edZ>i5z!@$)jbhwZ;8(ee=;+U&`unCK zc|dsGAm{xjB{6a+CiiPY^`>axeb@ZS41`QXMQ2hiL8YpCO_!Enb zDpsP;Sz5+(pHq2OwxhvF##I>}Iz$vtRg|YR=aXwPogMVQSXx zSR+VdLJ4;i0aB^SYu?^)xLocBS6Oq;)q4rZxjheJ{J3?+EE`%2qu#M>1znY6A;zlE z#vjcy{ijl)`;;B@`{2Lk1I(n{+-5GX{Y%1Z1VwDlJ5KfSA~ozZFY@*ox5pRx@*U3XskC3qP$vKRC@1@fBOkVB zQZ}lqbxlmVjmOQtozBIN0jCa5GFs9LO z>nRuOaeBoL*8bGMXj8bd3$q~oV*bDjF}SWQ!+M@PXbbU6`+8(E;GyqS#*P`Y4(7-f zb3Ix@qrKBC7aMD$(q_>D4YscTO+2O}!}HRq=E9Tpi4VS{D`RlP?M!nXW7uv1aRdGE z#EEPfa2r9lkml0Be^xv5oAq;Sr*;4e!1V&Eq67&IBXFO*9mjQ3srR}`zQ$dt%s{5t z((E!|$S%Hy)dMvyh%p>+jkxJi>}{l&cDh#;uwoBH^C&e_=KjoNvDX4>f5`Y_;V`u5 zZzJ(PiflI=-S@a{=)#RleKpYI&Ve!RJVzWPD%>qsRKc-po8OEn!ft;d7w{QO)n&AA zH3dLV`7dx+3soq-MhNNXsW>{;ZLN zmja#9_Hx!$HKIB9E+v2aCW)tUOX%!D`MQln#bv6RG}~=)#1Q<2y5Co8ZbvW{bLG>N zS=24w7eFSZMeWeDYil~AeDD(FrF8VCt8@d@;L5d5tVq%w@!H(=XK>38xbY&r@|A`1 zR5k$0xsvgo2Hld>C5cZXq&p&wP=pvkR$qE?We2biaMk!R#HdA#uu0dOc5+i`9ptud zAe;)A8nbZ*sK|gZrnXY1|DX&|OaMvXe?bc(9dOM-IF>=Xc0H)a&&j>>CaBos-^)MO zZ@_TxK-E(e>br?@v6L%(iL-KDSU#caF7#fFkuAEYf2aRIZ{YV;rsvO=OO|!VHsIr7 zi>*}w8k5pWI+V+bNVN3)==Nw}T}9DiP6ULjiN8bOc7|U4a$pjAbcr{7jmPxWgag@x zY4V?t!w+|RyxUS15ZkO#lU8BIN}2f4g9esKR$H^R)e~?Omk%3_zr@iBU#OWQK_hNp zK#o)xkH8Wj*I2E^aBu2z-f=^jkYQj>1R1GaJSGU5cY|mT5Y2!YY3P>6r1WfzS!UCN z#_XUP_JYpR-EvB6Op*tUc**);c9|LZ2L=aK?sD4?GEp}{RoYxW2;^eSOrAR|=53!x z73xvDmwnqbQ<}*8Ktr)i?Jv4AWQ&>&I?zrZ`Z#?D$6DK=<3y;XiVExbuFM|8&X;MY zQsf}(SI_f7Nyr{B{WK6zrDZ@FZtmEL(p4wpjT04kLvPaOCd+$%QJYJmr0m%^FX`5**n>cLxSCKkvzCMI@Gx*f zSxoreV#5njwTSW6hu<}$fbwf}oG=^c;>i*c`3Z_>W4RV&?5C)naSYa5G50v=n@xr~ zi8so?)0v%COaVl`gWg_*zz6ogozOhjFYwfYdTw3%Q`hLc_E+PW;Ir-5C3%2U+866H zwr|UVvrJ93!h(PwwG?#s2;Pn#EP_NWNm~z6 z@ZpPvo@1Q(vhWA7DJhrR5^Pz{Fs?Gs$)2;N$~|7|%k+d98B(<}(#8UJ|b7PUE0!QiZz(JA?L6Kc)bhimV~?CkqF=8CHEf)TDN? zI-NMiSne8oG#%@eJ)K>*=HoAAWnW|8#M1lW>a0aC9zQ6jny8t2R0{QnA1am|?4{+? zy#J&e5N&b%5fdsrRR?;?lj#yulS&IFvvL7H1)2#!LF<@EI)vhLya z(Wf;tT7;7w<5%i4lSL4$b*v+gqagMC$W`P4D5aaZIeq>w9xj_2I9?JM>ij;O zi>j$#q9Bn(Ag*Xm^&|{|>MnorNXoydL@gL zs-4=G9$$x-%9T7!&Y}jNp-{uskL>Ga^irVHTq>&PrX3K&QuQJs@5L9^2|gd#wvnyY z0q`gMuUP!okY}q}KMU-sgm3sF)H&GCUn0n^>$+2_^fK!6emwkAhhr0aka9V)$}6bY z-HOm{#Xzl`+;j%>Ca&1+XfrZx+Pf&rnJyTE$#1L-ZPS-JbP(NZQ(s?0o+EoLwQ7oi1Vud`$U`q)$Hp%~*Wq4WKpE6tXbVX_(JWlF zu9Blb%7K%Al{dmf6FlNeUc(7jZkf5_hW33E!$r;g!8IA?iGL!U@#&hJ{QT=H zjXj=%2k}Pul=_)+BmwPpBaERy+$MvW%4wStj*I|#%C^1?PbXKWNiHMy%<|nC%^&~3 zU_FLHg?|0f7$Hg=0+Tz|RHKgl{=xjajDPFMa;j58dJ?-$erntwj6Jle{2%9^)it_R z{h^cfx?ge-(XTrTz2DtDvw(>RnZ=3EG+(H9%tv_s<%0E};D)fK;s-`9ZRv2(bS1E7 z#|n-=pZw@2;+~Gudh&HQD{VsR#?eA2RLB6Eog=XEpD_Uo6U8Wz5%j#b%2{eYy9r=87gFN5>c!U$L)m~<0HyTe^nu81V%Q95~U0qlUpz@{OdpKU{cJjy;+^*kcd7s##|MGL(0WI&mTDPxue5DlLgl`|P! zK&V@se*S=lNE%U<@c=uNmfw{qN4*j12x-Q&xHDsfZoLVAc)<_qyah9xIGi!2soQJ^ z@&JG66mO2bmK;8nL}EEakhWoO5$D)?`h9LQ7tGtR>uRer6nh+2ZJjoV#8#_6SeE}0 zIaq_f?hBuj7`$ZWrhk2Hgw;ko+t_@#uyzx9R!QxroZDy29b(+5@%aR2(Nd+CbH8mW z^5LNBFDJzS-@0SMJYXxUirCKs5%a0w{&B1)D3ii1(;ozW#j{w>Me(k`iXboTGgT?K zM(=qur5A$qu${lrV_2{bKPCpyqvHy%{2RvS6-njwl~SqAVI+%mYD9^g_cECcP^{B4 zAL9I9zq(aAx(t1=G}%aPdo`yW3XM1hX!JHhF6+kW zk7~xy_~N4{aDC+3-i&)=U>+;HNvkI75fDGiP?n?}3Ndr7!QUiU=IkteMxhbT{9d-T zqegfGapQY)ZZ3%n{2~taiYSWuq!bagh5R0Dzn4&al@YPcho$6!64m4j*ZaX; z^LNErF?{RL3t>e<@<93ih?RPHr6C|GVl8pZEhk;JF}kNQAow!!mVkU)ylYB6jRT&L zF+klkH(O;Uw_gb6RD?7&`R+@3u$D9+ongakB8JxzQ5&_4P*anfap;zGOjOf1D`xa| zQ@kjr7e_uY8R1q`Otnae!M(qbNb70f%o)i$HpE2XYTM)J>xiydi^^c^1F`@?;j1QH zo=J}c`?4SODIgz{3 z5(hFbU#e)nvhYpyi28GofX?<}Jy?w`B!^wxA*VK&s!+?-;zAflnG6mREQB9vsuGm^ ziQg?47U!bOy%Sv}mbgp9tH%9cfIoj2lGK?^guadGq2oSn?5grjKk_M)g2TiES%>3 z2hvRJ2i4UwJxMp6<0`CNaT03vUJ>74>zMisoo4I{XIyfOX_38OsWFxkxwv4n!XD^r z;63~_ul#IUE?f-H!?nadV1R`$6`M(*@UAxRH#K$yrGXA&wHM<{V}>*r1~Mr-*;gZ7 zNo&k_BT8~Ldtz7??toG;@Nb)chXMUlFoEoTKap(R$@8er`v!@}GMe~3siy}~S~qk4 zfo?@OBt;lpz6vTN()P4XEzEgYO8y|7v5LDz=%J*$Ey#-vblK#t^4+hoa!Sdxo zCkfeO0>l_UCDe^zQ2&M+$wb~Z|D@t-tLmdDmo$1rN;jv^B=l^;Wf3!~Zi%ywU4GA1 zhQue7>?XRi2F_N)IhA0R-*?{vqLXF#fbebtIW{fP3p)1+;k(s0f6}gtpA$g|4U}aq-sQznphJ zg{Y#)qxnHsku(zz!8#eOH)63X!>iHO1Jxo|QtqV7p}_sd0%#7VpbsHgn$4wK%a;7# zuLOlo$ia{R=@h@Ds8rA=nMn>j;!7kcQjx04uS&rea+B0nCMbgZ9+O}V0`N?-?PylM z0)h8adCO9(J4~EI10;GNC@&u9uH4n>Fk{p4rWTU;OnY8B8w{m8<`L53Sk6d?fHI7I z@FIupLYCKWiK(~k^HZcZIS=+>36so*R)m1KT$Psfhs{l-ZYn{el<~xjL%G&3r^CcPR zB-$2cp$;2~gt|ZQxO)BuTIC6Y@&vk0Cqp~?*=eIVi$q*AzfS~aLJX!VjYe!^bm9pX zRQo4}B0pSHbIwokjZlvB&5hE&#{L$^@#keFp;WSG?TKZwp-!hZl#@ZUVoVVj|Ah_$ z#_TJf(7hU^{8nfk%*bG<{peq2vz$5}b%}PB@CCOzHHGTDLUdlh54*r1V)`7iB28Q8 zk*yymB!J-2(rsSoIM4XSs|viEg~FXotVfwrF3W<$*iW-B(T?2&am}G@u544GpBu{* zfRwf3xVmi?&u3AmY`q>@Ja%@8vzBB6#wQ-4dE`IO+Wio)n|g9SyF)DTkNPc@`PlQ)l z8sO(QqiL{!cc0ACZa`IRj4kitM}rwkdJ)AInaEx`I%$aGq5yAuhcR|LLdFT?NuzDp|z%nO&B_IH|h5 z@1Dwv&O8KHCS)W$^g&T-!eCz5EB+Fp2j%LnkwA5R^{)l3dl}TB@)_|YoAzS{2EaeG zJFBTfy_n+x-@R@;x#D0bzK6LXwGe^*d>;5Jt z%lG`Q;;RVA(-n~Kwd}qh@xZ|Y5bJsaEnLLED}P-_2VGncdH@5B`FbVbtSGP;hs^fdXZjekS18{Ss-eWR+Pp8jhYpTx< zbYi}p|Mt;}AVzd+8l|lUNYy_fnXlx&*a!vQQQZ9_*8Yej(fulN@Eubwk<;u!`;)z! zgc_I7v17k8bCdkwvm^Zb;yvVVH`J`;uSKtfu3$Zkuxjt=QBU^?% z0Cs-&avGXcg{DjtBo>`dmz-q!c{zhA;j;nk2Q!$GS-G2yk2-}WHYMZv$6AWFq^dh+ zUE|$8^Qa;zERqV_s@LPfbLNO$rSoH+Y3dZ=gzN8_y(@~(nR$?jF(KiskMl_;N}LuH zmFgvQZsPm4?)u|1yhbXrBzYzqx|D(4>T-$ue8TXN%QNkIRNnX;-JsKHWhp!aK0oyO zA=Rrt2N5wSQNB{u)5&|94rDU}1jEoLIV&cnp0~+O6!07tv--mo*irsE>iM{Ab9xVt z2>>3KJC_dot1DPSKCswQ2y7HXp}sIEw8iuo;KFPE`nwi%xJqCdJZua2C7q9336j>`9lN=&mHc~=7-L0A)bK_T}X*L7@1 zm;xr6vV)5fvTkw2gB)VWVOwYzQ!(;X(jv{LLy=YEpYjN-DjD*-@utHy-Q7q++IXbs zRKJ}-1-kAc#=XFsmiTy86E9Q3hqpSr$`$)Kj`^2cZ`K&k2jz=Q07OQ`K&Uu7I=l__ z{krpl4Pwpb9J7j*8yb14`kS%Dg?aB6UM^CG$<05M;T=7aF zGVupFhd_T5Ow?uDuRX8mYP!m)zP^~pQZ#&d*@wB6(DLECK7=`nxTZ+cLVp$h6Y*G{6-VK|kBYPN4G84JIRo(1Sa2f~ZBvo+|ZY=g4~y3Fbfgtghp1sJWh~qkB@AF^j z+<)TTL(j$ejrH$LMv09fcH$j?6R~`v>IAn?V;zcJf~4 zYxQg)X-Iyi5f7d;8saU{z81hnk>_~THWPR?LAjT894h< zJ-qXvdi)eCCvKJUWUc1*3yM-VK2Opq4~7^eBg}tEf%m4th5!0{s5}%Jm|bq=fWGE@ zksnwHSj_{~)6-ZF3T5^1R8%pwe^P%|DvAPe!|j_R6>f`BZo}Kv9N%mAMD0nik>DDX z#=p~XARu>G~IP0dBj3F;JNR zCy872WlZGCcK6%VZgOv8fC_5u)!;YlR{1|K4w4UBMQtHsq9KiP)%{3IO3u(EzwFRN z&2t-DnQZQq%1K?T0FX*eQH8q;9*Bp^vtGwrK#C?8iOEt7%fVFrgxKO$=^e5Io=G7!QqEKQjwL#C=T#7f@kCUvb2 zQ*Jp_^wn^D`82DMv}H!AQw<@uBtKJxCHa^~OFF3B;W@_50d_?Zx5tSvNXl6+LF z=y{8!EYwSwmpsE)8S5x>Y}1+Icr;LepFqa!UnWFoQ{{b)yi9+fXE$zTe6$%|qPQesz+*}~6rjZj4iEq%`YMPi^VDzz;u&+| zVE(|gTk|(h;`m|w@uOReae_fpxzbM*Mg`&_v6g|y=CS!h7Hv+wo%L8SDmh?sB~HPG zt;dJ7Fcu^Cs|Gi$St9hH55ovjNUSc>h@QU-k^jxrx6HfOC}RG6g%loiijVu6H#p8y z$HpW2fM9@F4{^o(V@~O%K)dB#2+dwc*Qg$q&<+G4P8J2P9n8afhBQ5E4#yItPS7n9 zl94a#IR{LielENjk?29(RHIt-rI;ClYD|3=5?Oj`^xc}%>iz$dJGYGVSNiE75hW^s zi`mYX=^voie^5%K;w9ZZpioLXXhO}lD?FZ!J}G_=@lZ)CH~OqK2_6BkS3|C!PH%?Z zoRawdtx_8Nd2O1F0pK!ilK~>o*wy-3~x+LH~yU>kE)BK{DjPUU=QAG zcA0bX$`DRw80+f%GY4hf78^s0avxwo)RqT%1q{E1~= zCFahhzMjPc(IX}^P?K&D0?7+S5jI}MtQX1mo3J1DkO0&{m1v}4&*~>C#}mJg7Esk~ z;ReG21la{wHvMjhc*i1nl0>J>8HsS7AU(EHJ-^)RK8r&KJ*+~%Qj8cD>h#aDdpw^J zftcM{3<3`0*0Bv~jZakut7^|`-1~I^W!>a?uxLAkq$?NRdb82Cg)GOl1GTA>dK7p| zAIaAUW}(jJQvnivD%rB9NUWJ@k@kURB!D44Rx6hM^WB6Eg)9(fk6?VX0+Xm1`{UNS z=P&(M5sJ6DLSZ16j1?bR>@SO1?2zjh!oZ+C5T$s0QjlXyfQJG~VDQlB*Ng$!X04#g z?<_OwZWv=Q%`_P5`AuP8JU2_We-~bxz?>z5&oD?J(IBnI{-oO#a)VS4O0^3(4?csb zv`WvFmLV4=rCsf)AtF)(zUr5P6{cVPKGxRZlw}0%i)H z*hb|KI0&am2!{nMj6J3x;WeMcH-pAY<{j_bEJ`D6UIzW9oy``nKj-=w$^#bmtorBY zoMyAe<-2CLE2Jo=gfu(Bj_)Zfau;0drxBEEzk8gU^ybddBMPHudru672^7`jPm8GR>Li+G6E$_ZMe%V-P76IBkcF@zS}32XC1c-I zo66_nml2NPMRf2FzXno9-k#)YX11|$-z9d^qrbw%@zxq^at2eS@T4?mQwrTPdGq z7CUjoqL%=d;RYz$BX|IuBb0~%z33q**NjLG*?&pc8kG&S0TfZuLo_zCxye$tNEPpQ zsJsj)0#}tLk;Z%jAB&(x{)sAecc4ifLD>w~MRjx5%V^Fq^gj(X`!N1icf%W$PZ95h zki8Eciw`$#B7nH>yW!>OUV{pZdtht36Jd+1Fqzw9D6Fkh#2A#O;ETD*9v56CA?+b9 z!0g{~YQ##oje{>_!I(PzP}XvtF+6$7Fl0t9VgmT73*G`wR*=dxS|0;L{kDC&usIG+ zK>%+a?7y^K0RiJl295@aa&4`7CMR!-H_jQ?QJJU@ukM^`>xfi#7FT~pFq-(; z9~vyyk@H3BbME(EqXYG5f*p5D|e_i>YVz|)RRbO32tBli4&roj?Zrbf>e zYo*472ZYsZXt*xsrsl~=<=-K2yOQBDo%UPpYGCFj3vWh*@D$P%sLTr9>bXOR4K^eM z8266g7886oyqnJ62LgzIqx#BuyNfJVGit0)zK0=Ur{3xVUxk`%%9K$7$q`Yx7yr(W znTDYFharIZ{}=+`1pJ=QG8cIXh72$A?xt4n3O@spyP>2X*VaqZ^povUd@&Rz)$9$0 za#9~4c^RfCOK~k`Ii@6*Mm~HKZo?f(!I|d47sbh-gC!y>m>UNP9(N;lmbxH;NVW@0 zI9CdVFyqKn>3%4yq_-Pew89`yrMI{;0w*1?8q8$Uh-EY(I1zHY7z|35agqgfp4bwC zebhftn*EY)IQAmL3!wxRD&kn?X)#zrj8aa&_uR}k!PfZz0W{nk#%a`ut^&ws(d_QGzNGRrw#~$}K1bb+y0~GX z@`ro=632BdEEZO3u32~GVcQi{ywz!ITC({sz7!C$`7m7Sr9(3J2!(fI>IXMJkr%6i zG1arPv;&{N?DQ%m47J519oY`&iwceRF6$+(GH*ZKyPG4!$RF%cDNMYXN^~o+0rJE% ze7fG;XX3B$>i}R#YNhQN@%uDnF}4zKd9yb!V_?r}AR{lUbH>l%@2vlasQrv72o;$X=^8)x6xI{9Y1n zf(Yegk@}5S>&=}VFo1fx6;>bVM2vWi%|-V?d!KkG-Tt^tDLVjd9T_e0JV|_$#E9nN zg^{_P)$f)4Bwf)}H5Rsj!5$oCJzA99seJ{ZyySPp5F;O_s&zyHRAE}Qq?~x*hYt)x zES2UGC9)g2$3MVglIKN;$S;hqOP|M|*C(zhC9pi8MM=`+xrOS8kMQ~`y;aR1l#K^5 z0G`5mU8H}Z;Wrv+U2;TMBycOG3)F)>iOB_qS(Zixl^^>&NEjSCoduTz!9iQ++(A)=8Q{zntafRvc=4!X&^N%>-#TqD6NhgUslieIb?nRV zjB&6f?P23)Vj5Dn9rAavw}i*N#b%!}&B)no+m#*CPeNf^-{_jZ({3jdSc((3u6?Oi z*U4mwI><1Xz38kG60cJy_Li?>V+qLqf2M_TZ`m#qPqR1Hv!&&}@qd{r5`568%eUBQ zW$iK4WDOM`o<-dXjg(ayJ4NYxAZ_%G3;v+$&QU`FFZyVg%y4IAo;UGcb#QB|#~l)+ zOrR)j+cf_5+Zh6V0vcL^eO@>k&c~Ba+AB2=`op_8xGFgkL z#F4GS#h}+QUJw(9M**O%J0=N2V8FQib)q$isyzq$r|?&7c5ZtbinsU``XG~O z=p~sMi!FEd2giz%-B+%7p-!JZJqq^*mVjxl6*OTC*9yzRPd8qt9a_?^nLpoM3}woA zzEoM{qZ@$p3X)1p#8Zu9)mvHOoZC3633*_chNS^UMa|A|dnea6g^dg^ z+gc8$-{~pST7O{X`8(+{2bvW`!T|unV)tQ;yAg&c6R|QKw7nv!GJFc);z-m>`t$Yo zahH7aSRkEe%rvpumo$mev`%e#*FUMPn|V#n<}q7_W9_GigIwNZVc*0wHrfDu-7-(? z4E|N5I{~|`P{Xagi>pBUhY-NB{~Uvf5QFY!3QGRvfRM-48=)n`q!YUB57bV?_0oro zEKFHb3qy(>k?hJfRZ~h@%4Wv%px@b!cu*~d;8H=Eq3TaUicdTTcFi&1Sej$_`jP1t|g-%H+;2hHe%5PjT(o{OTqYFF!3QMu--w6QV( z3Hm@5_S8=^$df=Ueh-EyGIBQaOr(VTuE>`q4Fcv&E6{@0MR^M+u2p|sZL0KmXG)7T zs<>7UeHs@W*1xWldT{V~ADsUEv3r3MwZvzSK7W!rY6+P?GwT7$$b!ATNmsOE?s}K@ z4}SDL%Idte`*3HDVeIe)E`fTr_*7iO@;%E^!Mf{3>75A#)uyM;qq@nnOiZZ-%Oxqi z?5%3?zsY*uX^57&)FeN6ScW6i4I)bX^ zA^#eWtY<$Ped_vOeDgmkUtXI3#8_qi)JFgRy#~#?K2dKm@(-8@GW1HFFPHX%542;$ z*CgNpC@)b2?0`4%$S)%rdDXTNv%)+ve{`PN3cEDs5>>AJOKCNl$R*cgW0>0ns>Qv4 z(6ZS)-U9>)i7i_OFgy=41{MqdM5&4;&FJ+4h0Z-^jN^()N_Z+_GzfsV>gypL;IpQp z>Lpx(Dhnv4cpiEOMVr7k6?B_kAte(n%1shzd)w;>LJ_0T(m14(2Xva>MfF5GTif33 zXN;Ep4W7kzKm1+si0kkR)1wAgO|TUjC1Af}c4L-&7o z!E2@tNGROt)2Zi_Pf63jaS@N_7mAWV;1JC0Lnqf}YT1j|DRIh)U$Jc!CO%uWV~l(z z8r5P@R~=DykW9(=@k`BmlFUW=PoqbizIKZ3Bsz}}(W8PdP8C4X) z>dhCvO%Co{bbcXUf15>gyG_z{w4lFzbZ&kSyElBv(%wZ?5-0Z3dNBLpa$T&fUbf}z z!+U1c=}kYq)?X!*+uUR^H(I}c{VQ6RmGTG@P5?x%G807$k~M&!TtYli+3VGqN1U8R z>?o_?L9;nLM>yz4T?tzpJK!g&q?*#x8a9$!xF?HrOKjklE=Jv=uJMx0h6*y%RI%Wo zxZ;B^M?Yl;W#+-r!J%n-ve){;)7q}T04~vB5^K4B0VIXL&MfV+=V3;&|JMaT2$g2A z{kEKGsA<5HNMKF#K2$qpk)#R*!Y7;XtU;*3%%dc3b2{c^>@)X)pA}BOEEa3oR7VWG zCzZnX;g`ygKS|UrgZH-h-!(1M%zi!R~;*RBWZuJHUi3uV#==+;a4@#hTSD3f-Z%-)ri;jtEM9h!s!ppk7?n zXAv?LYp@>ZsjL3QWwZ+Z3s-KQk^EwuAOmAoqgs00d>9O@5HsZXa`7x)g8*!RhsPCD zKQ=WVEzy+{M^78QXLJ1^XgrR#j2R{wPG`{|O%?9*rM6l}TH>@xl~Rkd&^E`Q9EF zl+*u^Y=89)B4NTakO@Cs{XQ?3@w;Kvd>WVfZ=!+kIGmHJ1cPy4K?W9be2# zOLH8!Wh%jkUC!}JXf|6-zpStSNo9<7n}n6ycV7gZh|T!m(GF)nM(}J!$Z@d(D?tx4 zvz+q!Xh;Y#X~#?+&ht@py6gY;d*wOMA#gA7RA0#NPVDk_8@hdHnpQgdJd6N2Yk9lC z^w@I1%)&?rdy><2zQ0aYFYp#|-8dqp$MC&fmTr>K`5TOnYCSsbIp)-$s!F=pjhP}( z{S2&xYMj~F$JZWz&ouU?PwWlinVj!zZMPji&T-{9@(QExv)a#9 zr$G0{YZh$rWZimdsiya`GwoA%5O+8sF!E~gmZ zrxQ2qDv&WwE8Xb!ggO~FC!aJOFn9WT^o!GRuj6tBZfFvVn_EoLu$&J6=!d2loK2>9 zmI>sx_If$4dDxsL;)V>FFMjBKo%@@}Izt*1(ClG`jgu7P=GyldFrf+IJ~)=kx3Lwe2j zm3`NYF7bYgQd6utiP&u8cM>9(#B>xIHeZ(Q4VS*g;sEPBUwizABKMfX2e6{J_VToI z&$ZbQ05Y*HmeVg z9DN;`Zc4eBIu4EL3)+)uX;-RV*b&az1Xmlif1CBUVtitbv|rGb_opf08&%sF)XiJj zO45aY-;=z0-Y8Ff4pf@d)}TLr_;K5Co2g9K*Q#E_%HME;_0&vJFZ(*(UHM$|r*`FM z1`jP$NT~m2DG|JxYCb6TP&i=L#|#}#sMZfxX3P;q)TiAcPB7MODuBU zLjsw;YS`*Wn1kLIWt)S7^ZX#}4zO`a9+8v9t`-O-HzReuuqvxIJO72j?7N^YQ7=Y= zHRrOtlgj+xP=-7$6>YeF?57R-W+*up(&Tk9Vyw^4x~ZsTq3{#! zTsN&^cd@lJgRK*-%LdK3Wz2Z6d4Bq%vX){5XpC*yJXH`|-dRV&+o~%Kn9MQ5?+8wet!6$~rY0&fkvmuV zK3){eDJz!&-df0ScDoq5`iOh5fbe{%#LrfyY3MUeuRNIOog$>Aah=%FjoVR>udl$& z$K_4NM0Q0I8fqPRRbo^$K;4T(E-rHXMqx9;d7fiPCAK0|EjcJdDd)v{FCBsuJ%pg5 zpm8zymS|6uT4s2+uc6nYWXkY=6v@a}S1o?Geqj=kK0SdWA8sH}%| z;U7qHA$QzDgOjZ`N)=U70@o1UaU^c`*SU z+!7o8p||BU<|ckdF7ry813hM(uL(oPosEgSsn{!>kk9tZYx0~PQ}IA5|c)&&p?8883(NO6XGPK#%%E{_09n=qZQe(y#Qe z81du?%0P`(FW-+-OrhpX!<7ED3^lDiHBNn)>U5Sbxc?lxpE(bvTT`AMR~fgRUngGc zpa#pYl>Sf}c+vo9S^MbeYWlZ-W6)|5^#_nk*6k`8d>j(RPs3)DuE!$Mn_LUTlYfm# zl-~vI{`3T(6!>=>YY~h5=niHQzaMnGJcur4sa+U~C*oG&5Id7G=y1>CP?xY8CT~g$ zT)Apl4j3*vV1ea6Y1b=wz+9jHMNFxX!*95MTMDo}ScO~Jj%C%Zn<*pIQQ+~w9g^8Qv)YZdm>UscJXwdJR?j-BPp(-h#($Q^XI zn0UJSgy1sd^)V!;BtHiRAPFWCU@-B9<-Ce`kf6FC+>~d(fo3_o>y!@a(l!iM+h|(1 z{dh&Ev!fY>7BIWO#CX|H`N)@es zm#u5WBSg#ws}Ey9s+!G%-36CcuD(`%B(N9Q_=61NOIrUN%}&d_#= zP93wwq9hdWZmQACO0F^WkJ8PJm#5TEy_%Sg4lUVu6jJyXhb4d+y;u>6L4fk<=K!t7 z1^PLoM4DJ7@DGHi(VCe~saemo0EYlZ8qaKsRC3t%#uyd8RJQheYR zJ}EcupH#ltP2{!u1HffwMg3-*pHsEU; zQjIj~0a@C@=M@Arc)C89lT`bvrczJoOy#Qg3_uWR=EA-=nG_ED+n8bEE{Dehe|#g& zRRF-zOv#`6eZ(ipg}i6TOquMqRmH2$+EMp`;zzatGe{1Xm>uTRp(7jud@n4#{aOId z^hbZ0rZAA7%^Kc9&kKSXV8CFO-~MjKenpQ@+;%#74t8GZJ8imMcEfVtA9M z-E7m}HMKOJ>mCO4KDFzwTol_wbYb~0G72Triu@#Xa65rR4i;FDw09Gjb~5-dPv%!q z?=1yHh~#hfcP9c<&4G2iQxM{|PSj59DH2+@Beaj@q0{^d>mYnbv7Kwp8 ze2!07)lRuzKTU~>fC%FNGUi)u1gqj3cT0TfscC3Q$Ljt+c|xeY@Hp-zHh%)+zd;q*D~wLju+Aq!qJ-URT>B_~Z?T!3lM-F{BS8Rf-mv?+qkT zd};^bI4;Oy@>*2q*>5OCYmg!}cDYcE-0q1c2Y{Yf<0wygkuzjDo1BLN^H=uFxDI*S zHO~66*0by?(@QZFRxzy6in@hR9+yTd9Bc`e%VnDOw9BSz{qNhXSyR*4^Ak;8FTocO zIo%qd00$T_prl=O-tv!6`vEueJ8mrTclchJW#lJrq~f{0a?X7kgv)eLkT+M|H|H#g z2Q^}!Yg$XJ0xf@$8vXk}hrFG?mC+ud|1)WY@;dtEDIN?iQ3?Q+l0s zcw#%YrP@Aa<|8{jhmPFcXoI`FNEBWXSdH9JC zY@F?;urI*2m&~{T5;zgTG!RfXX!a<}vVn|+JI=b~D7?G{9A57;a-VwhS*GO5cUHSi zs=$1{PA;0))dr^9zA`BrT$v@Cy3UY19DoYK<*Jg#e1RWHLH(Zvj6Ks{TDo>_>)g&l z)c3yrT3eO2S_hw4tp*QX-reb4?%s?4Z{{f$gT1f}6f}4WX{oIq$QZQwl>1$5aF#_3 zz)ILLU5`Z#V`N-Syyp+JIIXb{c-HeiEor{o#pY^z+MKbHcy*jUVElGYKIf_6Q3Iq)?U&Lg@loh$RJ{j!E5?)+kfkhvO%6 z?r%8r3(ta0yDn|LXL25fovXW~g6cT~Zp!}QFG|jvekl=oxn^~{%0KIOhR_`ZoMsI} zGN%y7?lYe8*8g69&!Go+ZLVDgHVN+UUyWD@O)naFPmRniVI)K-IUf3u-5{X$vCK*T z%6+dKON((C7#1gKudGkj!Yn_Pb@<$TFNlcFIWnuGY>X*@$du1hqkg*QeI0}d%*kYGj8+ z(ZI+m^?Z-B-#=UQ-lyQca#N~1WU?9 zk?%>aAfRw)N8u3es;sR#`73pyZK6YkWPKFE>9yu}r$FONeskH8Y#p=f^%l_p*5u6d z`ho+llA3kejpQelhk^6sLcQpRxBMkh$2HKQTDtAnuuNGlL-mY1w+lOQ_}FtWbHwPt zj4!{I+l5-JSHX$nKC*;0VmPlogc6JOz%4{smWWpHxzpKgoSi~m zG_QJ#Ccp9eL~{DQQMimy_CX_UvXo(Iy|%0_^Lu7dJ+}bz#W1vZO4j2n2by-~QSWZ6 zjFA8Vvr4*YDa`%t;s>9aROW&xPKSsQQBP4fN$WEFi*OfzQlKDpV^oQuJQn*_)bC!h zS^tF*5M{U)qCia#C+VhjrR)mY!v?xFf7689xL;x%`e~tL&_70xOY`h%a)r;{U%OKi zp*my6TuGq+e#i6mtu&$lUw^tJz_>ggi=(#BbsMl#YO#!e6HY{}hlRq_@=z(_Suisw z!2+oCJM+=EMqJnh4k9SYX2f>lCWvrsi@fH@1eiNKBTY11^erZDa*ln!;K5Y87#&1;>^kHY20!jW<7%Nhycd4LfR$fy+Q3i zs{OA6U4o_psB*eMx-OCn^95G9rE)6_>}o?AvMjLzIOPrhU|wJ6r^p9tvBF@|c`C1L zQd8Iddcn%LtP94So;cBt`CPx5%Vdc=A`2~>goLVr0BKOk-XFZ&&`?&<&({X9yIeUi zivWlHX#frB>#Aq(?}U5pm5(`@bR&Cv_u0=d*Jss#pT-7R_A zU1-PnsI_C@AbU`NW)pX4cd9*xOF7ct6iEeSX@$mmp@TO(?6GN0OR6>&`Q^$u!mS{c za7Q$=AQRT1#YyN^dDIH-=sigD^e>d5I>Yhn^w%va&zQ%5IqJ-isxXH9O+UINY$!a1 z-rj~xsXpJCiPeh%*E?7s{ediCf46>|8ft{%#>Vo1s8Nw(CX(YTmN*NMDt3D;VRa!_ zIzHh;=`nJv9fS`o8Ca{7*2t443vKl%~qaq%v%; zhE%;Vp8}}h*Hkr{_)p(qCItMr=zQS&pD>4i^1r<7@XwNWV93A%Z1P7d(JG_Xk4+J5 z^F+12OwO|2Kb>DE&)kQLm{PTd$vDdLBb8{MK0x>}0qdeVZ@)5;f7lTiTIs0tgG`HZ zL-VT5t3_;N6tAXFca@`PMZ8Pj=@sb@VG~pm%kqA!$6xxjJnvGDK4Xfr26p2{WvK}7 zLSVO_CdnDgOwaw{k)1m?9(nMqa;5HVcia|?W zbxoB8^0%JG;=~gXDSb_Psp|tF75A$N&YD>!&1B*=ZBJJ$4MX0uO(YQ3r;}33QE4FV z<#IA&diGzVBW>Wz3g-5irzi_F_BSVv65;17n6aaoxkpt=ft`DT`GKL`{AEZq!o3lkid z{jAdmTKm1-SfdMVqz>cbRLbXm8ji#@FaHc|e}%-B9bYGaI`HkSEd2NE_x6Q&z79PO z64*WP?w#=*p=;9>p^zcV!YVfS1n@a2tIZ({L3%G`e}HA^Jv{G}wkxP~C@`WpmCW%O zK3&V-tqvDl;yA$Zs{Sf~$o7WwzXhJEq_` z06yvRdMWZpBfjPoEak^9QTC8dVn9O5PEcF85)VJ_G)YB|+=tGFZ9;&8HO>DY?xnWE z==2qh51(GI@3|d~g0|ogAZpRqns~*$L%fNQbnlM*mT@Wu3t0gRNvfLTh!8Yk-IOZ> z52;ea=g&QASahbv1Rr0Gspt9ly#ok1^p2ew3h`6DIJNX4D61S?ZHT_n0Cv!?NHckS z9`0p1zjI=m&WD3tDF6j-A(~HbMV%91I9y;7vN6bd{c!WxbAwi~(aNd8ku+*ooz)uw z82KWPJf14JL#xJP_nX`6%X7^Ih(B7*ybbfrm5ZD52G?*w497X(+C&^Q%KIWKYxI@d zl`;lL>PLcIFrwN#od{6~%VY{{@ABo7J5QVwiR#})7%-4(&N7?&8$lgmRl2X)WR_k)ncUo=wokp4>1_aearYzU*Jb`ZmdUshL!v>Fx(E92 zt%IBax=J9GdI4)9_eZ}50gA*4QWptJPMQGZ@I|3=MUG( z33(`YXp8L|k{SyV!}C_buKjIU>%%5ug>+Y6vC^j=T19`u4Z@Ak^aklOBwwM^&IG;C zTKy zBOp5~y=4~vZM6J7F|D%}`W@su)oBJe&XQkdd}XPlz~JO zzn9ALx$dbyd4J?hjyc%=zPi_<-VEMY_r^9o3r|{;EBo$8F#kp7nCv1daX06%LZl~g z;j|0rxeYSs?(lP&AdOhQ6i*+88hDrXi-m^-CfrXq%&tQk%m9#~ zk*F+scdQv65qcHa`Q=*U2@>`ynHa0I-7a{B;SgbDkT{ zMaRG;%DTAp?2AZrQ;5udFC0-3f$>^+z@~2)!4Ou}R%@)UIg;H!vtAYBnsf1CD>TKRsf^oXFW+}+dAe$T5==({Sr-+e#9~A5Embhcs0QkzLS^oEz zSHY17G62U}&<8E(yFr#2T4=r&O#Ej@i1b|^(>{F7yhQ(nEu!pQ>5e)Z<+k1r(+-0^1Qz;q~m2y z`)8k}EJYTTXrc53<>XWG-$6u54sAOuy(q{H;>m(uJn2`ifTy|9mJ80YeOe5xm!9u` z2L}YoP?{T$)-`IVVxsvVqvg$hikH6aFM)e$ zk7jCN8Z}hyAxpU1R$(iC0twsa){Hfl(I4eDRjK%`+b{*^p8}tu{XT3=gi;2ipL6tl zJAOO|+}agc<%=SeJ0LZ#v~oHmh~0nhxS2qE=AQ5`U=Xu5t3NrHzD@T})ZcJ5LL5Q* zJR^Q_UCBb2VH_`B(C)sfX%ce!tZV3z+6i4Sc<(%`QKFmYw>#@o|7G#(v*-T^_BisO za2N?xJ&sg8LKVD+t6$EuY7Cs;Xau@m5DmJTsx|9<+fUtIs`nuA-#k;B_#CZYM(=q% zhNM|`hMtf9wiV@a-|-%-bP@W6`6$lw8(S^@Q{N8r<{U9)T;XUG(i)XQZox&2PufC= zO`mXlf5g)uu9zGr?sHl;O-Td5>@}P41!BxIW{K31uPn4-kZR=OV zBR%9zDNTtL+0ABAc){q#?GEK5wq2-GoMEdRF;sedJAIh7(g0cP(~GIW9H#MW2ImceSse-HyZ>P#%iB4BK82a+mgoG-bq} zESdX`UHc~#Na(^;XFn?o4-y9B6(zEHn!F?b*F5OEKMZ`xoAP96X3OG6q;~Q>ZcB+z zdg1bOc9>=9>(+JKfd>A}RS+nZW=x~87O5ZoN>i&p@0M6UhXh_ zc_h+@FS_1(OKQ?!=;}K)lJYvEI*Z1R%Lgm+xg%%w49~LP9@2j&odh|Wfd%u;*PW;l zH>V)bl5@z_uG{?sG=To>cQ&i%>a7;>gw_6V$`QmrG_ZH`QBPR)hpVV77S#3v&37_;t*C^^&CIk_m1`yR8`}i_V*O_@$C$N$g_Rhe2-u8izDm!FcPUH?e`gL zLtpO;MJ~{it0Z8(L{CD$>}=MU{+!Q+Jrc}2zL#wxoSafUV3er=XX2-;VqMe*0Wz4;-ycwrqQ}`1?rJAl_i0jpNPgE_lG1j+LTu*|{-BYqSW1t|yAfzKmMS-D z=_%0{p|dzm9@RXrA&cUn)igCvC80^Bs#)-sdACI=F(Z>^I_m5jP~G64e)+Ib698a7 zo4hF*6%3Gqx3gl9x6I-@;gETOSjXnRy5Ll795N3k`c|q97{yX6wiz?WHl4^cCtPAv+kmKx_oYxwsd;}MNbpo zw*k~qTK~=Xgn|(anx;At7)_)}O0h1M^F!UsR?;4gc@qP?;0g}{fE5(_=ABTebx6uV zJAd2Z1Y^3cYH&mg$Z;V#yo$Q6PK;Q>aUrW8FfrUO;FzYH1Mt*gviVVpTVH)1OeaDZ zAl!NbpempaE~?W@7?LTeG*Y0Rv7eEfYkElih8ZiaXd+uR2!r3{3d5<*3LvY%nxufL zJ5#A~sLIvysk{ocEAG^XP0j}s>wcYO@-Ajgln&#R9D1z?BP*F$!fyD6EfXzfJHQp= zox8CyG9E>I(Px^44=?Ez_T$~MHexl#WuTcVDb>=c3 zuy=}oxQE;&^f>XE6@xxV$R+rsgGWjW{M9x`Z+65|t_U8NDt_kK_I8vz=}xKhXhABd z6pMlKYt*5^vI44KEF zuf&O}jW8zk&i6Tk9Sg6ouIAQIlT&2AEgPkE3c90hq6T9qrQo0`TCJYva16VBNT<&L z715qz0^7!Twgnd=J02ra=o`MF4z-CV1L>*{m5k*20reJHqa=qgt*;O-e7y8?lfo{O zR8{yT2w!jxTmbyuuOKwTLit3)`21=wI(QV9Gvst>sax^^;^D#0eyjNnPwUx@&hT-{ zhP4YH$5I}DGd2UTxF3dQA4m9^p~Q#=jI>b(l`DqJEHeGej4k{seJXye^}g!Rmkco> z2e>n~X!h^6X8-{8W&@ujm6R1w={tvDf^&~fBVkYFy*L;z=5(%JvI+kQJDT(b&P|m84eKy-GceUO>@8y6k_rP}n4ItSV+i2g z)w^)(6o1+gd+s$BOm_=xI7|=^&7xv)P(iS&1bqus+i*K|W(a+`tZ74(XtcEc9Zjs$bJ>5SpUWaxK*ln+4l@XYi?#WMW@1MKJ7K@W1Jfc@_)oQXxv z;uWhO2}Q2y?ZQ5bAOf?70B+aqoxt|<4rSyOkI$DW!^?hT@i|#U_qSbM!2at2RC(B6 zbvGb7R41%S2u4ES^>9h*LlA&$y>4X5jgX&|`se*c=G!?K4DVODy%nwE{FnJb*)`07 zfaephiO!(!8BKkTnl#buLkY43rM0bl9BZ6+%U0a-=E6sPluU_|4ZaK=sHCWt9M5x& zA3sTra4{^z@Da@9dLATvee_Ez#nW4J^u;&ig$GwGcK2cEGr8RlmJzDWb$4Hc9NqY* zCfjzmJ}9xHZJ@yz#p05toRY06?)k|hQ9Qq7==H9_j}!(ELGg!b{tZEwyOsq zT{nym6UqfsjT9@wWtZVr!`Qsk?+y=5pZWZockBJs=~xeKyYF+VePF61{JD0+Yh#xA z8}bc34yn2$;J8uim)p-l{o$j{y}9>;&aWKm;!qzC*EH=VOMxoKf`V?|v+_`jo#WB{T#|_Xr1^ecqC)QB1mP^xo~~SERTl>4y&%D1RK1H$ zo5Kbn!xzYq`B&%hE4-+h^OK9Pn1i(m!l+?k{dTD`9Tf`v6XXmOt9ztSIoa4VohBp- z0u>?z*7VKHZoTgrR`s)1o^he6v@%=wmaZ#rI)1u9TR*PN=MD6*mZjBaR-gPIRYY-X zsGDVb{K*9bFFe_?zrRs(+Mp07WT*t!hp$5$3HS-o9dT6BhJQCZBiYPYZpGPnK98Q zh=dx!yz8xy8TyT$IP#eRN{%|B%K5~5KD~Kka`i`55QBE`8f=RR{$at6&_FFSojQ)= zLk$nVXRz_!O1U@|ZInM*5;}+bpToA3qGaP+qF_T+>OG0I;yOjOIeHW3$@X}W@N|}T zA(<825yg0z*jSJQ+>#bbK84Psc?OVK4NXq)af$t*ddPM+f#4rPOvaku^^LV=EQ=Ag z`GYsu)e^bDKA!#P6KsxvdcCV2_+3W?Y%SXy6BHlp_}QDShrg)fdp#~SGf5{1V48n> zmBIJ}5(pT8q0&4{T^3QYuS-Bu!SVO7G+nHx;5cLr@uahkz{kr@tScIESC~E&?(z3h z@AnP2_o)GxNR#m+t}2as!6}MgR<+pz)?3L`LQ;>+y{dOt&5hPhwX*!-0@q}?;nz6b zCV_^>q*0!tV&X~a`iN@(I|%Bz*p(fP^4s6^hsoJsxG1HY z62a^sp*&n{3!D%|+Leb*V_;IeR{wgLrJj1Yf`LknvkGai3s`;8Oiq@-$@{vY)hsRM6`x~#HZ$TZ zmpUl3C4?Vjog2)-RL|9ak(XtuI&@BeYTK1Hnmc!dg^NOu{gt@31CsoURb2NxZYFg$ zI=ED`Cyq57d9|lA{H#Tg5h)uU#p`p;Lw*Ve$E^BF@=mv-I|9}sfA>pw%~3+LFu?Vp zD_ERBo%Bm4Nk5L@y>9h%ytr;4YNzu{rupWU4i~S})1Xctiu|bfviGZ2o2Qk1;!d5< z%|`?D0vmP-vKM`;5~>{KkLT^MPDpUE{_erLq#OnhQCNc1i34&BSiLwrmKe*P0>obh z)v%U7ILYo*q+~#DuOPUb6(VCenH%w9n-VJskA=OJ@0>Ug0N3u9^UAXyQQWwkPa9)O zJ(i&ODJzfJLq4~SomaiN$c!b|pX_wxPOTfh*_mD&X^Ib9iQGai-Nrkp3PW6q8v%D; z$Px2CIEBY%dzN)#F8z`=f9?m}g1krB*5=lwlN`c?4PU~aPMYc@&t`~O} z51vC+%%m{r$Q`HN)x^iK-s67t7_*V)--$EfzO|5Bn7r#um0=r`4o=Ciw9(PTXY${ z{)?;UH(}kw5S0)2kca1HnzLPg^|s#0eZOxdWPg5eiA*HA=DQnlIg)Mdwz!%Bsa^FQ z6RGkYvCN=VBxQ-QW#5LKE6AU_bA3&bevyB0T+}nkdd+^i4x@`QLIS=z&#S7f-D?YV zoE83iO~~*5NXvDUvS@bWt_&&&RC&n|5r@hW|NY8%KT6@nJ#ckss&$$exVg6RjFF}c zeX6hOZJG>N7lHpVbnHcTyK{AUk^O3WCbCXnUhy-q!UrORF8Lh?US>p0^22u`Ebcce zspLh}_x*Rn(7(32c#?3GksG)pINH-xiuA}K+k%my)Cy#M+Oy3s?VsmGjhow?*Rnvp zdcFZ)+S?ERmLF=>LI2tRMC|KERUx6zvj_et3oLQvn zmN+cZJNkfZPQMeEc0>*qDoZ<2Eoyf7F(kPO3g>@{D$(+NjNR^$_swKfV5=+DIROtI z3Rb##aaIZNdLBj&$s*6`BA2omI{DZ0y;yTNu*<*{Bk9tk^qGIdefjzLbw6!9DmQRF zd-n7Fd*eER&5+|eZZXJ zb%e~56pVwHli%hDBYl6aSj`c7q7gwr0k21Ohk9{S) ze-bxeVl&issgu=~#leRuzjQz@+#t;_GdN&dWTV!n(W4tu8w1NRM{0y6EuV1D?t`8S5YLohqwxD(-ooki^bjSh3&t((Wjfiz?cBPd;{HGpBw))B&s^8K4N|i zY6j&qXvBQyUz?tao~Fp+I0b%d&cI4AIInn4yRrJSZ!sU&$M8Wj&CeepEVkBQG&3DW zuUGf_k(j@%*<%9mN6V5X*c!DLB+9p z8NP1gl_^^osL410!R2Alj)j}#gXl5BsP^oh@G*ICu}k0RrDCGWhvS=M#dP*>7kc!9 z)|+f{%HLUBGMJb_I6-8Wb_(0pTuRp?i+lu32Aq|snGYtfWID^X&VqkROKi!QJs`%n zt4&%snqaXJRlU?zau|)vu(TfpmgYQfx3`vF6eAKAogJ^j0qeQ}6p{#K`oLdb6gWic zY}6KpWCB&DRT1{N7|zp2cQ})g{=`0$f8RAn3&p~E(9S9dMJI$qxbpWON*(mK zb51O#T^)y@t;rfue%gr{{Z}C;qy!$h*}T)(bK~O^AdQMppQgFp%f_}5y3SYw#K^Xe z376pu`wX+)@`O zLm9T9HFEQLYAZVPo6tq|P$^lpPCM%-@PkY(&e$5072$olFDi*=`yHlTvqywW%IHkPk5iJn?4+HT2?2uZ zZ^1)Ct*_OW3v$7Zq3hY1M?N`f+b5xGiLt|~EUIY8Yxy4RfJ8-gHv(8Ok2z`FCWvuX z$)blj$^KEbK#_u*LD6cf85=eMBF_L_uI9$8Ws?}}%E9?I!L-F{-M%j}B}n%^K3ujM z?kgCZaWdIqMlXuFDLKw|iH9R94U)tB&D#)38fT!%5B-6hx%S+f8n<935X&x^vgWAeUOW(|FhIE=({!tjExmL4VIxVLGCP0lG<@NMCmvjYhm zt#`)2%%Qcmd*aE1BrGmZwK-(A+yy|c%Z2mfZ9;I5vCi86mwrTc09w}za~s zy34w*Lk%o+R$S@`q{^&s{TpsO+kXJ{*j4Qf1sg4);$!z5HuzQ##jfA=RL0xlSUyD+ z>ec6YU=>~9rG&B$)Q=1t#!QyWL2t3e`;92nOH7kFzFJ(DLRZSa)UbWvn#<0E_1PmM zS838b&Ocx8qz6~pGY+DFS@1&WF4L51a_5>zK>AC_#fCEy?{8^`#d>t#S)6@0LSwKp zHm7xc>g(W^t4Glcp1{{%tz~p1V=1UzG<_1n^(86@J0UB<>d$J&Fmd~h>JzYU?3evj zc!E{|%1d`1TS!(T=;K_UR;gg(1;9?_^f|yCSI}em%iSCX_%|k$NbsS2@M=1Wi7wfm zgz>*(xY!^@1mE>a%PvuJwzBx`EXmL29O^izMDEZee)KBpDQu@_@59s$khmyHSCYw4 z{UL1EQD}ij)t>@8?y(+Dxz&Dhf8m21Kt$=kzjreHS0L~_ri~^6L0gzcN2-xhCvc!3 z;76*cW)x;J4jqfx1(qX_$mq#+P|`Q~+X|Eua9cDi=sbrf#f$m%U+pN zNI0%-h_7DBRpNDSlrBB8a3{43JtR)%J_xe zt<2-^q(M$_ZP;G?u{~Cxf}^u;YCpe!WsWn=249a{dP*Oc9-+Bi`6EC5FH0zK7a9yQ z#ja{L%JkC2Bb)W@uFi+Pdc3RMVQ1_rvVt#f7Rv#!aM$WymikT)kGc$?@N)-sPc&omz@H3`JGYjnQ&xvLYGHrzsse9?E^@!c zLTV>y{+=Jp@_A3(MhfS%I_!^$_UGX9lNeHD8dJ%wn^riW_w@cu{lp&18jYpkGK`~s zp+|}j__Qw&)uJH1rO*P6{7-%|Jn#Akwpo}os70GZW;itkfk;IJI$h7yvtv2B=m2u( z^w13Pc74ld5ZB#MhL{;;cQM@!eMSa;pXuFzZ{JYzha$)(ZL&%Xs9**9P<*q8W5X8f zH~Z^~^Bu!Tw{k_=!j(S^)6KX{PpAaLSpLP^t)t0o3)6uDj7b+tXy{iiXxkaO!J)fm zKuQ=|X+aoT8l;C3B_yR8O1g$_1f-?Ay9EI$=?3X;xcujw`~1D{Z?D*Ev76QHy_!c8 zwYh!;aGTki!)|A=3c5Lc#Cd-bXr9=9|C8BU{oz*1&r4Lg3)WeH!`58iW_5o?{7waP z|4LiL$7EKOq@vw5UKIoy9av+OllwwrSG0cCN}uQ)bV&&11~5pzA1xX4zTk;-sf#L+ zs5m+XzV(j;_gfT4NJf^>Sl_nw1Fyz}I&3oHhCSlv?~-t+C~K+>Yuu&UrsFT7 z584U0jU#PT+qT+t<GX5EOr%T$vEr{Xq-awsBT2K|6 zX=5>DHn(Gz_VnX;w2AG^Tp|%u3Y<;|Mu#taSen~Mzu|w3A3BFJ8nTmd?`;;2PC3*K zaEy+WnmXD-M}cAL9{=G~OkEZDUE&w^n9$6vzAv)25?8s6W@lNh7Mm-XwuIh)$-QuG z&a#X&QmBkhl(eU&ktk<{R>RGVl8?}(Gao+@Uk1aUJU!1hWtlw)Z909_Yh`g=VY!Y(hr2z)TuYLU?AEXFnTgo^e2lbLX=B6 zdI#lYnGB7qve2`Pa&)9_r~o|_{4ax$X*Z5=+|br!#Yf)pHdXF7&nf2T0yIzGBa5JI z33b#*uONN6=8K1|T8Q3=D$0?@NKGwn40V)-ipwp=oi~1>$jI=#yjj-79kNNj%dRz% zE_2lLV+3V@bHU)zpd6d$QWJ}(Rh2S-&VaN@w!38w!f%PN9D}5^*qiGqNgYE$J)jAr zE-0lO_m9aUhAy@V6@?cnu8$0$L|W+X1DAP2^$ z8^dDJ{je8Q7wW%>W>VRMN^tBoJ`n=EShijvebM#@3=q5{0<)q6)phR} z<(1<01Efk&p1TEB7<(SEMRR4=P0`6Qx?sF2AU76THjK533g{p2-+tfrFX$`x)S050 za&*y2h)l=r0 zRR*sBRUKJ!{h!+M!LP0UQRQx9dX9GBhUVZ`GO*DNm>7$TD@KUR&r-e@Znd4d;EyAd z8PymWGV?-xsTsGR}F8az!f7UsYk8fA^=# z+@sMv-TkHG`*=Y6#AWE5^=Wh(3LYFSWEeykLQf{b6=*uikF5r_pXXHsnj>f+G5yOw zisE;q|LWWC8Si{xMk`lukDpD7TJtB&%gklxXVx%eX&HS;$-k!wcsRo%AQ#h>h#`=b z7Xv7Cem30QZl0}JJ&}kb&kiv?>-fj`?c!{Z@W=u?PPgg@J|L%yIE&fmi|;{p?oZ<< zX;0@F-}}ipbZ~~%a?q#WmJILwZRmjSF-$P#F+vlB4Wc&xQrqD^-Q3X6mI@yCB;5pU zFc)t122vD-4%&Ri1AI?YA+_O?T{PwtdX~li=@jcgnd~0Fu&}**%aT}6jRyEWtlAZH zybO3gjVoL+KP_CQqC-9;lHw}A4D3UE6yokgr@9q?tG))&Y4DUL3YB>3Q7TK`IIGMa#O@r%Os z?q^>R*}JC0)9$V#dp7#%)cL9VdjlW>#`82&a8trg83vx!!+nAjy=u%`?4BMuQ6*Ibb^iwaC$3_jjYbY~wFuL%bN1LJPeSIeJB` z`06QK2>TtkEr4h^{kpn5MQJg51J$`$^wXu!e2J~M+u?rY(Y7^w9PK{}psloBec^QV zVF^B#D*nru7J%{_(MT}wkCLZvwLobCVZ6W?K$(5(_5!zb-JbqlF|E&uWK2WmY;J<( z-`MDU0-Yeq{0gJ{6Dnb`i51t4xgC!+0E?U1e8=ro4=a?Q1JZ`c$#Ufh7aubl9{ft2 zh|BpbpkmA#m$$w?>{?=0tzY0(3~fB^b~pntA9qTL6J!lOxs4sN*WLCAAA=Zzx2J?! z_cYdTDFj!KkG$DMk5{Bu&9d(NDr=%dub;1bC&#L--Wc220U0`o&4Fx*{uvT4kDGg< z!%zhL9rR>}`C7NQ zuQ$mb{6SIo^xa5cjh^4!gzF!9a5Jcpx7~XvycOx^$LQAIP9&iey$O zHfdApXhCfDT!CA{NZZjG+2(7mqk;O!JEl1)7wep}DFk&32HGb8Rm_N48Vp3HaNqR@ zXuxMohj@`9#9WfZE}xsAdlgj1Jdo(d%ew5SqoturIpA!{s7s0gtA%mKf(aEUQc0r0 zi=x6&jyL2lcv4^##9J>Q6#+d??`wUDG`d>2Te5~ers5nKXzdRsOl~BcRB+AT#~l>! zJs3s?dL``lmUnERXw5}g1>VpD`0Ic{RK!S|P*Ju8vwlzt)!ov`@^64lhj#6Y%#~2@ z*syot${^>3qTmm3N>y~PRF5}N!SCu8mw-;TS1zIRC_5`xrbdJ)C}zts4H@lY1?e8(m+uRZlw zCVw|8hdM*bLk-qDDpQO!d@e0!k63KA+|%6F=5$d2jVNN(GhXiH1UVy23)#PxDG$uO zCLX51LXJ44G#i9|*vMTAY`wzO@4)*vCaQqw&-l^kReVb^UtEeHzbd zFApB&aQOE^l2^xdOuO`Hq>i67USWQ;%M{K%3;ShbZ3nR~wlyfVP!_rOhtUw-Q*6pv(`aHmZew2I zm5ADZ5^eO6U-^P!OA`F7=pg-K~~7abqvv#nSDN1OT#U13r#jsa@Q`7PPL~{ug0i7esU{JV(@& zdb^}ORUNoSXK>p-xW@%G-;FnSTYSBG(oKhaxEnvcc3)P!#1{EkDV;3fnds$5fa~jd zZNF-ZjFe`vV*-36gd$9cBZmODUD{dr)XIxIJ>u`^)aL0@xwoPdB2`bzQKVWvP1bhZ!Xb(wB!Mc<9>H31j@x}GYnNKP=r8k)7&K3H&Y0)VYEs~=E3K^l^55!b_;;d8 zl2=G=*x>7ryjXxYt2^bCBE2beJo2!AsFJPkBACq$LRkPkHKA?vwri53;c#ln3}#=4 zV)sFYtamTe?(tT~{7$Mk&laV9Nn@fRFDbr%0Vnc)gPbmw)0mu@eq^Yh#shz5^FTR1 zT}iR8*J<~n0iezDj3{EUtr$OSwBO}Lx2jq>w%-L93t|gpEqPhcSC*T^EVx{G|C$2| zPHXGikvtmGDgU~DoFU-f{av|3fYs!g2K@Z?IHc$IRZsTiEM4f(pc37)v)j;Jw= z+>KTHIgmQq_+y0Eg~jdDHPLxGYZv6F-=+FE%8;Yr>WStJ{s)t!jaikuR6Xcmk=JfQ zLdc_3tDAw=5wYS5nDoo(8R8x{^!LUpW~qm8?-$C~WacGhcFSY0Lu>Dj@T?P7E_}AP zrp$)`_Bk81!%FB{b0RRJ@@_8{vfvZV!=wBKTFbd2bAuLjlJ}4#`yf`(TAN!|$(qCf zCgZ?*zvQ=@-mdrPLh7oCqUsmo$oE?Te&CT0X@BUbS&Ri0+k@h5!1B|?)C>ex{NUoQ z{C{Z_+C~4ADr*!Pdc1E=Q25wHZzaV_VrWta|5z&&n_D?brx2yJu8Xr61}?+kgEfw# z8I2mADK9JGykZL9@?nqs(g_Q5)Drn<@t{q>Na`eKeTlh=8VN4zMD*QA6>A{+y~qn9};BTzYO0}?{D5FneV-*m|iCX zEB>YD(wCg}`AUyq%wkF%vEsBi0i-l;mam~xbxJJ0eD~#W`jTl^xZRJY(M^CP{M;=k zxvo){3V{dHPdQI2YZe#u_Kx*hJ|>00xX6f45iFsu+qy3l+Wy@(ZIv=DXU{7{le?>S zy6$NiaUDX@N+7nTuln|!z9fUcGBUgwC`WTu)pvWF?j8!BL}Ev5PAP2HgDT;Xn=mJK z%EjA^l7!7u!LF;p6d_mR@yGfL>>B<)YzA-ex*pe?er^H~Ytv)rOZmgLsWPp`j-eW0 zKB_EPg-6dDp5=xh@)>ug=^1PIC#I(#D=njt_D>V$r;h7?x6mS*#nT+j2%^-P7vOEP|I_vfrRk)_dMKz7&4@|R2ua>Fs_4UEiPcNZs5dY- ze`I@_LQtA?wX=z4XIS8*2dKKW4$qfwY6cF>Im zEj3Eimzjdl<&R%i7n?6tiX~A}bLg3vni1)(?-1TeY*TLjJ-fJtQw!a1RVjakj&*x= zA2=_Bt_}*wf_!+~Vk!gJ;*Ijx=PjIiw`>*gQqA)K%q`zbiKMsdhk>$b@7o3Xh^qYF zXk(v&h>3(Y&!vaQ20;IAN_C)*`wK4yW<~a3Y0H-dXfj$9*$4Dg?B~$kckWz{c!JEn zeuu0E_8c*8G`Y1OI+BzBj>h|JF=aXc1}G^s)o!D=wl0~|wm|eF>;2Hw4K#J;fp*5E zmDMw#&~>Lzb@6VY(mUgHnVv!b(FDbPp}zo%XHU29b+ZljWH33CF9?~)@l<_ZYOK}C-7C^nQkMGsw=^{i)l z9?kImz79K712SwBk;MdKN%U-ndYt4Z9K`a!K!nrFeyG5peDiQA}2IkeAocMt_7_z^pEOhsg! z>7u%jLJ0Gdp&6ZIu9w6f`1Y)w{a(uP;94V}63|nc>vR3uESml*%?a-q{%+Lb;L%$YHw zwjXb!>7rdDqOg z9eSs<0%QPHBL00*%>ss!BKI1L_ZPF1HsP-5RR&Un%p2=G#eGkm-RvR>2t@3v8fSGF zq|F0f$8dx+xUwqw=%O79Mbn)6OC3`>*gJuxDrz(w;MVSR_6F7$G<2((t$^oz?ZNp% zvWprcoXK~*4!!|iA8HLqPMWWF-cZq0Pa zGe5~6I`_hi3jZG%V^_8H%eSj_2{3S{yljRZ|4|Ag-L`o9# zY=o2UhzMfW_wJzgg!rfZzEHbPJGtto<(JlnTHBVVKl9WYo(_q$^~&Q1L0>dcQBkUo zIl4;Fw#en7S6cqNvF(oKybc9ubr4X=Kkwx-XkvFPMg=-#Mja~ds7v$JfJ1hCR$w&I z50We6b}}2F5EW%ej1yT89qU;F&VH2D!)~b_&8k#*%&AAP|45)gItV?IHs%$9AQ_(h zKFV|5l~#e4#^66)%Vjf_7xi1EF&sBxBqXr|!7P+BIld@}7m~g4#BCOmrfp~9D5@@F z9tMbPCgYo+%fA|H3!)Ovc%;KWMKx81yb0lTaW0^T?@bHl-J=}gg35hzcpc}7!#Ubi z-%^7d&DokEje~DzNceID19u7i8gmeZx+a z*7KA-P&co??39@N_TUC${3?kRo3j!@9Z^e{5s#2Wrmef7-3J&>5{F2yWT38)R5R~cq}j9QW69e^VV->rpOy9g zRdPo#Nt3smwtU*zk`n*rmA0;-4L0N}dIUZWYH|;Isb94&*`fN)A{d1P0PQ7HM1MOP z9@09=3&_UQEM&OU&0lZD9zs2wt<~*lm7klsTBVxc~-W8tP^T%?gME&m{FujuyoZ z$4`%jn@%*Bq^ayuQr?a1FnDmX@Pd8Lq?q1&V#E~@c_WfK=a+8Olq!}Xo z{xQa57*abZFaVwd`T$$nl52f-qmDI>Kds zXe%3>5-hD-uk+%?`br1{H8oaKeVoX-}?;M$|8ZUi?ZlM=>Xm{qyg1p|@3891`YDDiqp zumB&>B}u_$?{j?kOpkWOss6fD&dQz1FlziUXbX6GvK}oHi8(|3^|Ti|A;t?)Ke{@n#mj+I~bu)EQWZAD?g$)>vdAcR%NQP8t=oiE!4gsQ-Lcy8%Rd zx8tLj`t*<6g!NhEZB8dw59@*D&;RdEtLKsB5=THA*48PMVf{cB9br`apm@+ms2k9Z z2WqB+B{CYyjz?5~O_qa>dTDrEMg#?NmRjDQKj(OjQGAi1#&H4^18|nu+sS7s?vC#A zD*{Aqu|RhSWg3psyKWS+mztEM0DpY+Y_txU3USaj-64>F=<3Ykp7atOWFuQsG6$gyniJ!X6`}74?`cb;n>P`gP}@GBD#LtRHOIz`Z>D2ck4X6neo6FarI|)Q9qvoa z-CW|xGp*yJc4t^=p!A6-QD#zF{qkVqlyI?*6WP*>wUz2imaL<^u?#CW5v!ypeC(6! zzMnuot-k0F(zm=9D{Yz3bZ2Q}AL;MKC2=c*Sap>=(xM^fU?{d)-(<|J5%z zd+!l;z@_|<{X4HS*SIk|>lQRWEHtSscs>QJia!Le5)utSr#L6zVh%O(9hD2sS6;T) zR}bwA{bZ+`v#&M?fhGjR1Ia?jVxI4G?#_tYEQ6mnpo6 z{k+;1CQrh|ef?@=^DM?Z!fxWP6YoC|HsWig3k7V@hZfKFvl^?0dT1phKfd}EarB-H z28c0N5_Nz{T#xS{isGu3&gd81Uay3jA+4hopN!ii27^%OlJwL-49HK&8a@4Z5Y5L; z`Z+<*sC?xOo$=|S&;6cn8_{B?HIef2v_XSoArHUrys@v5JcGeNeFYWF7BV_=ed&GH zs}Blek16c-0<>Rbu$ST8wy5$vNNezb$@|y}E;7xFdW7(qUm@7Wt65nauZV|zt!Rlw z?YlUSn;mZ48R%?ybAL?`x}$c7Ec6S&uxxo{(#vk?qU`U2(#-6g?O@~Hr7yM$Iy+jwqD!&-SN~i7k;BmtQ^jIV_@Q)S``8#Z705=XI zQDAb!^FViRl)0&o|2{rD+&_T&`c=tqvgZ8R4XqFLpts0LNA0-!Jj)IdvQxSSzAm=8GF|hnr`Ev9b1`1s=4rDVae4 z)3kl2BSK39Mna}bF-g1kj&ES!9Ad1JxXIhq1qk(ldd*U+2zLd4XBfpx=A!Qv4Z82} z+1hw3%g;83tQqGoX&Kq#ucJCf8uS&5jlZ;vdTL!g%P2jXfcWE==brv(9**VtDeHrfX+v`5r-=yWZ)w{YTp4ko#u? z)BL|kZBbO zxP<_+hCcgTp4O*|(wWW2!}-Q|piiH&xZF*Doc^UI(FQYN5}&bLA6{(gQEA)n1AU){p%d69&}W zeKvj(L$Hc@56rsFWCv3dei}y{o6$ef`^0Eu$V1ppK@>AS&%UxC2_tpwkM|OTw8zZU z)kYMObfbb{eiY>fKf)a9#Uu`u4_u6R5Jq*q;SugK!J69{P~`-cFhRh}L<-cGr}Ue3 z9W?h+eP}!Z^xLq&_X&zT8DUa=AvnfKwI5Mcb^pssuH8VD5G>MtxHG)^HfXcQ0|+@t z8F|cURh&ro?A%-x3BGR^Hz_#~**{ls-d_HgFrZn8tYGk3KFbVKXh{ko092X&uSneC zWRA8a0O*-ny15adQp7X~Ub*#MUcU-fJZj>k@wkb`KlWp*nZrp7ywQB7v31`HiGMNE z^_?UC1|idB^18^e50qGTci;g0(a2d1N4n(xy!cr#@in8#UiZt8KQpQ%Cnf-mXyc@N z*oS1a;ezu?cy;f|1rQ>0FW%ISTIeSFql-9G%uUcFix|M>+5Rwv@%MNx3LUa)D(I$$ z%Q+w2@^sLbnvV}$@w1^&dI6x%>vEmd9u$KHhhRRQ!~luh7Kw6EBx}%#u7BSx+02h# zb_^K33u45eOLx=0o`bdvWShnx$@$X3Rt}h7SCN-hHz}{V17oZv$;)RjKi*WAJP(V$ z2>2|NM<|Aod3O5j$t}p+9+6mwNN5=oF(yCWky<*urM3#Bw!ca9;?mJP8O8w)3Zi@) zN|Mm1D!is47dm+zbzszHjcAM{Wa$*Y*>q?>GOK6CZz<@&#GIU+7D0EWVcgOifm@?U zp3Al09i6e9>6^P}7R7#)EwkBbHIuN*#}ZvoobPl>L5)JtTu%bhUp#L-cs^0kS#!}O z5Q%)oZ8L6{8aqZ9Zn}--`G31zc*x0J5BWUDjZw{GYTx_trd`jwivS*Q#XuO^rgv)eja=-}fD!Nb~kk3DTvqi{^d zRs%C&95DH)2c*VCU7hVnv2~D$)byc(x$<}WcnK9-oz>Ryb0?m2TNQac1>COsl(2^+ zxTzs?zIF-iy{e*3P(nVTJfgU0Vw)nXz@}?&G3Z^u?)v6B<^0GQVCDwfHUKNJC1$hRcrESU9_>P94Suqjd78Kh)=MHK?eSI zi87j47NSsML_;jm|57%~Wb0Pe^`&oEX^k7Nc;^I`Ks;*746ygmtlR5)J|-mhe*N)v zbFPzpS6~`;T^lsmjmM++^*1x5h%>tvRW;P+Y)0YqW4;}`FSl}W?_<@5N4TPRc!#eL_b)>^3Q08GG5b(XAiLssW@4_Js%1sf3d@emcprS6YoiA``Da zMv(txcHd~*GPtv$!$R7I1M8;0rDCmMQiSBuW4FRf7Vna!F-D4*yu!fL&_W^E_$X4q zh}6_j-z|~JT=TzlsS+il#JJkE2gn1U=&d zi0h3jL*J6jkg)xpaq^OZhx)QNn}?eaY7&tlT&TuREBJ{?7KPWpv!m&AP9t%hy3lxaN`rz3;h&p6XizaWbgr-K&!ggFii1*R-EU#8e9kzNSD|T{k2zl5T0_~#=Cb5TddAF; z4W9^%8A2rO5<{-Z_#VYq2+X0}q~d6*q4Mzk(2qk$T=tfL-3Z1%O|X(oQh;#6Y+^&M zeE4AO>yjl$HzK85b|mJ$1F4T#tXC?FzcZJHM?dH(HtaUCj32GbNXqD6i~ZWtQfjDk z6}R0NCO7#;o&vPix!a%CS#H0k!LcoB%`kvZCXaKpb^!iGCXBus0@j(v48e5eQ`1sQtwVilWz89vlnInIxscmEVbJHUwo z+SPM-pG?97$|m4^6XVsiL9{87Xn*5TIh^>rE}pZ*!0d(d;?-JOolo@v`Wx}0&*6D{ z-`@QiDW0CP8Rk>S-;+7|;OC+>>+~(Tx3V+?Ska_%ng1}6_G^5Pj{u;yc^!A;`_~8S z)GY4Pw49b!FemmCJ#s8;IH}e4igsj{(hAp<0#CJ`T9U;rz^K~s-6 zD-WHQ9ZeqB=iDQr>Hnze(ljtqvG9GYqJJQL2jJ}^ZCS&=ZyyLV*f+Y{&amTL7#m&E zHr|pECBuT)Lj#kgWS?jOJujWd|8r!GCA^>@1*Gm@t=wo-1CBbYgFFX2@^zolt&*J^ z{$RfMy5b!*gYyQrs=#}zA_%z<8Ez?kN5a>ZD_n=Kf-D1*oivmQi@yYq@dnP)Fqa{g zH$0j9Nvz=7ez!AD5zg6fn%hqEZKkjL;K!_>5*Me7xya#vIuvnpF3br20e3 zgIy~=x+T&J?D$ef;)Z^|JLLX>D`zNZhuOT{C7~*W-R^dq-nRXk_zU~g^{+o?J}qNg zHDjj34mLzio&@;%{Vl0N^DQ2arr4vFSzEigGL4GmJ!guOrxtg`R2@{ndE#c|MrhK#pf1SMEDI?kp*d`0!45z{hzD%pymTl0EkiO{J0fq|3a7X zCPmZnldb6~&%m<4EPSah9uE34qtp|-!rXWt62(WWTG@{(e=EFThW(9Le#qvoHOK__ z-g8Hs()4LImFgqQnUiQqA)-FdzNh2lF+z-Gn&{hHXh*?DranXwtQ~MKFmdLy`D>qa ztT@}&+U#Z}rE~TN_%D^30wd1HP_LYi$e(w-XR27kKba6J>SD;M?%_`{coZFqZE*;- zTH7djvjk@E(m2z8++(6+zfjM8zD>v1mY1*eXyRrxK(TrDgv;591Jj*Z-+A>5L z%>?%&J_8-NdKLqV;J|D^>8fV6i3%+!Ygo=ayK%Snrm~-No`MfhPm`*zuQVZZZ2>b&E#u3VENisJ(v| zvyEVGvcpM-C*|9df1d*yg@tA>`v%$b}>d*1ioQVKM#~(m$^kMRuDZ z6z8Y)6vO?xrSLo9?tV#^dvp$Wu0Db>GR$p5ZIbp9hEPY;5dNqlb51eG#joK&pZAA+ zT7_;5Q)1RHDcB@mcbM21Sb2#(l%u?M_PTln;xNoMPe%XZCG8ux#abEok_Yh`geJSG z->$b^o2<${#BfiSsSq}z7jAHLx^2%{B<}b5_93=o0IemMkqbe_!0$UpjZ!+FNg+`o z;slrvb1@8{=9+17TsRla+2+I~f8B37kdI7~K?)|=*f@rZj@K;@=RE*IBi^}Rj%zCs z3ClW<305q&m*?@sQn_T$Nx}@XFgyh743-TYWA?E6O|d`yfOLmf7C%%CeU2}G%sTp zc^wpWu&{4_-Vf!q3uRU`%p6Zxgx|p*KSV+@Z0>7eDB8_(C?1| zTHPMPaf9vBc!2S58)QZpnl2s}&_QglolhydvQX@QS+!7{{}DwyoGj2XUjcR+j~|Td zr$2W!3wl{K=m7qR7aR!{DIWr|zSatpYDoV(>fG<)Y01EXiGLF#aeS|Du=v$cV!z1PIV-LZFqj1uLQFph#HjtfESS@d{x zTLLOC!l?6L`0T?=Z0(NOq7LL-^Gj3aC+0KdVqJ|2e-Pv(V511o{i!jpfw@&E&Selr zNo}?DGm)SD2TH8Kn@^>+(j^0^isA zBf%LFQ%Kp+*#_?UBte$#f7i4|>BQVZJFQuSt#NHuMg$Z;uHad0$VyF*?&u&Y97&iBBda{Bx4Bir}Nlyk{pX?;x&^aM}OF&AP_Vx}w-l|EHO867>;pY>U6MI@ z@2Ak#@|Rwn%U_1<30?u?_j>;XJTS!|*Zg@Dl>|uXlVx&~;{XdcsYt&?6Mf)MlzKF9 zep1Hww>W+RZ|sRh`(ir&v@VEstR0yIiNOXGKvkl3X!i>og0t{p>r_d`mMP5*VoMm) zZ-A(wkY@~(5VC^SW3xYne5Q`T9h#(MO08541dQ` z(cMc`i$W{E`Fh}+D$~`=2SiGZ4(5ork}?av$%dc97wGaeU+Il6MY1y$c8QUD zBHXn`mJTF2*yW$Rid9o{XMAx08~QlO)m^8G!E$-I$`{$y7G96$Mn8FLinSw^T~k!1 z7^jdG&RbeNk`vA1$IC3iotMnCyrnpfzi)wd+6Jbtb4@@`nz12==ZUlM9`?qPgoqkr^)8hU0L+qi7tmm2orVec%xf7M%GReskTa}paDJ;o_ z2BQU7*xKXpWkYU3@$g}O)adVCtPqkOmf|1gsgYD!_&w@fZCvh)AOBvBCu^KWcTk8# zk0kwk5sD89Uf>kYU8{%hpS!4ge`!pdHE%wD_eRQ5AAF*C(_n)N5szUc252%EgZskv zW+$~u@7^2>LqD_Zt{$2i?X zTXaeyv^NK(s)R|q5i+if-$1;4nUY6wL^5O;vZ~TU-=?+|{Glu@$ZNR78hTubS}c+a4eM{&T=$!D?TPV)IKLJMvz8Bu&dyK-z+ZD-I5O=-bKPP^H!?2d~Z$RT|^ z7-k!nkiz-H1}yUfrzaVv9uU=$s)zXUJNvr!?#24)h*E>l-`Ss~J=W^EibBd2+HOj; z9(=w@#y~5=0&N{9Ei6#9L&cFvWJcpZi@$bo&1&~$|8~%Tl|8ow?3a>d3wbUE$AhO` z7dQa53h5XS6AeQ?xkwKKOuxy$b^En&aA+^W^seGq#@_AJ;h@^uTNz5N8sDHac5h7m z)m-dk{$sdJ>rXE0EUbz#L1Msl(sI2(Nx{_A6;;98*Br=pHqMvOLRsu9$8KWVs_a{t z2WslK^45ytPX})2ak7^z=7Vyg-@Twn3ku#o^Incu!dup|iNR+*hkfV}F%gfjU zi{?f8;-AW*zh-mz9@+ErX5E@HmC*P}kPENC{UOi(Jr;btM)7=>eH{wZ0vejrnL>tl zc!p&q0>nfNswbsNKO=Pdh3g}$;uPCg7!cE%xPwef?`!B@ZgDVMZnNk6NNoBpZn_ti(Ib8&B@ zFZXil%YWQFCloZc9FORnBo2GM5Yu0osgW(DtQmz*w06yYav2_MzbQbGj9Xr?8UsNA zf6hT3mo}TnTn>Q3!A#0FO`-b#^@~4GqO8{_PdY$`B0%t%{RQRC3oZ(khR3|){`Tm{ zfRu+Rpybm??qy@M9nO0b;Hp*Nc^YQbC#wuk`Rkw-m#uun;N6~4^9#4pXoR`+$dmNo zvW(WjuL|=%+L`2{>jrl>GX@9HU>GSc*|$!q%^FHFjz+`+*;K{oNGSfS*)Gd%z4<^f z^-DIdTx4hT9Q)5E9c zoXDbf=%{+5=vwvfz<_06Ny_7B@z8W#3%eu>i+>B5; zUcVrzM+qr$7?>cokFC|XEQKi9NmjjXwN4wVbkRhKP!xZql1%p5&H&QWXHPh9k?>m~ z>Jnxk*d)@ZKiDNYq-Qrn>H6xvF7a4)$F~PQQJ`~`|g=b9Z|E?;Nvc1OblndBzmPYys@xNb}AT8b|~VrbPT_FR1cXMR0?2I;EMzoj+*h6*~c2v{0V}j zYcNB6klkKlhW0ZqOp01Xb2rrV!D`9j6l^0%~@34c<6qqbEqTcIDE?Uhcx5bwP* z_WA@v{|NX^2=|%ZE1cEXy=kxW1WZB`3I7FjVZ|9}F1ISE*BF&~#L1ryfVw0#+4L@!lD46Zw_j_d8BU zXA;kN?aRhmG}dEtb};u0@m_+-os%KQP@>Q?Wa@V1xe zrq1(ERl?2Lw8p9;H^aqWvY^B9#yojqpqSf(JIN`89WDN1RCFQ})j!N;0<*7gHW`NO z3+((8fzl{O8Xv*5t)s}D$;|fG^WuIy!~mUBY9bh5;Ua8uJKud#3b~P;)n|zl`nh25 z(mmL|3&W0cyHQ;~BkSbu^ zo^E&E^x|5F-mfa`QGf;V=h85Ex5ldcCL3Au;3$&(tNYK6yviVm2St)6TUM~>>#3B2 zpg0S_JaowUKKh5;|0C-w!{UmTWe0*ggF^^5xO?#64DPN2!6j&L3GOhsOK^t??ivX0 z4hb&7g9mt=d*r=)-oN$jzk99juI}n8`PHbeHb_i=jpj#3qu@Y%xa#b|)FuWP_!(~v z=B>Xb+B7A-C$OH^;60I2OW28Fa-_GVn50L|`JqcH1Ma#ukjD=CVZ9>qWo^!F7x&vVU16CBF}) z=PgV4#yDx#bu(6ht;uM120a09zdi*e0cyJ)TixWi7+b03rJ11r)-dTi717D0oZOHB zyt43uYm><z552p6Vyl&WgA39GoQQ zY`;1R<%O!D0~^D-(p-0YnZfZ13JQCIvgm)9pTtn7&2yo4%rDmNHf=p; zp2ez_ZgUe6Qw$8h-7#1&jGNpWciU}+TCX(0?+rnl98T!Ds}<=K1l6nH#5U<*zAy*2 zRD-F;l;iOZlsA!s!iuy+AN}4qMipm{)k+DmETa}8peWCEss!dxr~M`QDJqRPff7xt zy?v_Kr#dEU1zx(8Lp(BGNgA_!J9KX7KR(qiP8nQiDGpdW6oq0i;72|Z-*k1+B-l89zw$HNs2<0Ot;TTPe1*hivCl6$0;-S$u%o4+DN0vv$Fz0UfydMh@UUc z`jJ0y8ca5nOpJ(87e@13PmLC(qYZT3D;bS#<(V#|+hi)Akr=CkKWnJ`-m_#lmis!C z(&O$yXqP>FH^9sFogQq;pUXE0uTRL%9dtYBZE)23p(ka@&x~_q@A>q6N*D`64%c-0 z@uYw>rekB^l|jnGAkD~cP#<;v8x1?k26TfXYA}+_NcAa};HT!B?|~G|=Q-D7$dWiD zixFCSuu;%`TG@*Pi_mFe=j&c%t&qRQFh7?@uQH6XFJ0L#O1k9C0_cpXt&as$yG6+D z%=|T(J5gvm5ihoQ+XvU>$EKHg#Jl1=pUBIiLna=OQWh6t!s`Jhg(k$=jK_baz!=_mb8E}=9SZOT1s-hXg zYl6nAXRo7X98c#YPP0p$-&BL;hNvmPmI@Kaik;!#MmMv>8`TI#FAro!H+~3p9Ie^q zmFTJ(1g*e#84DSjtj$sBQlssDiqbk9J9!+H@~uO*d9$OlzXsT)mO*q_N9BpS_<)ZVh(B+T#8)r|&X< zFBtx#{>GzY5Av82d0LNK1Hu;*653g?=`sDmq(n2Xp>OT= z!fgdE56&=K#~{kEzrw8s{iYWLCoaO*YZY1dXG_D@lDW!gf{d7i4L9@Mi$gul_tA@w zwd*GNM-G1K7DIDNh<~Ejal(Bnsfgs~E?wI^2roHFukT_WV>TBR--}S-3p2}AV2}*y zwk6yI_735+?`s=$;MAF#_MXYt0ZM?&%K@J(CI*#&MOKLs{&e)Lvi{q+??b|`M zQ<@7GyiR0FRLJ(ybyM5EVoQv!^V%d{lsN)v(PAuI`kDrG_=3lKLQufAYO#vG;pKzfU? z$#r|<%dJ5|v&9*7^FgP=5~(SvfHmimiLWLjWa3q1BE1IIlL=ssa{hGWye!+I9f3FwB74%5L?cm>ui5 zwq53Kb@L3h^sh4iHQWD?EGVyN1n;-4^w_#=X;6#^{^Faz9-=pqYp z@V-nwKwsn|s-#YpU<#P4twH;f#}Ii1clC_}n=&1}@GmN4g5CI63{hTn>zQ)bw7EF(uir z$OpdiHv90;Wad`XO{D&^hr8X2jlcY9e{A4F4`(H)DO*XjLu#3L3g*d=t_mDftte7un%El(1!cx|+{9=Ap%< ze;H{=vqzUbV%;SW8+XT+AxxV&e;R9v=kKPiBX~lD^ojQJvwUvme48O^Dth+Zy`b`s zGI!LPLHKQoxqA>2>V{K)Pn#u?XDeLYRwq~DZ*AA*NKvv%nMhZ7J+UX+JYv3KDgL%O7Az#>r{>eLu_r#WNtc zdXM{9ox_cJD6w3H43~CBPc?!Hw~@+rU~DEDhoUCVFSCXKrnsM7Iod8JqE0ila-SR+ z_9g@%msR;b;}vA2OTEtusqAN~;<@i(o9CQTNQB9^#fFo3^`#jn>_NDl z+ugY5w@f=4NlqZTp#&e}n=Nv;gIhO;nTyM0VxcRBggfpLjcb04z*rrtMcP38pg*UF zI+L{@h;xa~MM&M0-)Og|k=XCU6GE3#0tYqXo-)x~5bAV9JvW*~*ij7?0xI_lqLwpH zzSWA9_I`8I#yv5M%FL#lM7gvoto8M(#~ie^L%-_SPVa0*z{8xvrQGWJ82T`bh*t05 z&?~}huuZ!S61736!3cXdjXlI6@HU%$lhyAXJ0s(JC$gLl8q00 zc*jQxJyU7V#rnZ8euk=fmFS)ubWFrmC)XbX^1GamME`{&{sAb?*+t;V0X$ai&2Iwt z?AO|g{}mI|WO{%PP(iXC$q!#Bb=>cgH6DsTbwC^R9E{|Cl&L~CnWhHq!Bdq)o{$+( z%8PW@k2Xl??+S`jC_IT7O>C$XwWQXvxXk9ENHd1v54-`nghWtWe>xw0Ley>^g0(yVt zpvXMtVPpyN(ClL7)%ej!Sy~(-Ro?*8AK;R+;But8kx%E!gZ_=@YbUu~h7yq`ZJ=(x z<&Q@#7VG&-x>~8f|K=PYlA17eJKW^e&Zkxv3&=MlXxE5)lmYx^NF_+xmZ9A?=vx9p z*g;V6m-3+OXfHHMJrI&k*F4!E4y5}}u!OHS zYZkT+nyG5gdB@|d%8T8z3y>FP_jhuMg8w=btrAPWc*eO&hr5}~Q%R!Be9>+Bfs%R7@GsCjuo1;x!cPbH*L+J-T+#k1Iu=rrt1&JxEN-XP{ zY?LKicF4RVfae@|a52A~(KU<_p+(yMG?E{}AT zdaD6ea1k(0y`8vItOPZT&~6BQO?Jn7!(-C?$DiugCPsTVPlxp-!Qz4k+sfx0%T_vt zfs)>(na-*NBBQev|MT|Nnn=+{US#diAZ*A^$)bX{`b>UnI`H+smJpXjr&X!^ovAoW z8dac00i7Ifi0?pgrEY5JRcOClefxl=3c0Nl&qqErd){Qn?^%SXW)YJP9X-ilCEZ6#Xr?Te3vhQuVcgo*T%2Fw_-4Ke# zic?Nd?&a4rHDa%m)vnRQyH%K5Q}2=9TIV{w7tQ=Nh#tCB2Kees++riGU_MDHx*2$* z1Y+jj;wER|T-pKCb{;&P3mzC-V>rPX$rXpmy7_N&7C)*X0|vh?cAotc9Q@;9|1|MM zAlx((dmzebrvSZdN%EW?@5cY>ll)%I;poj@@bB)X=|;?lS89WNYM*jzNn?K-aD{fZ zLR$`?2Q+5M#Tl9K^jQ+Uue@mlaYXJ*#1Pzej1+G=j`-HY%S0LmN_om-WPjAw>_cbF zS`A}M?f2|8%&J(3Xie(85L1#s4!++gE(L4%M zHh*sV%PR9{l{)e*0*Els2HqvpEbIdQeraFHt5iMnf>0suRh+iU{6Nt_SQ1HNL*7ur)bshR6j$! z>8mmKNxXFptgMb7>k!__qDj?Ys$04ZFfhoo1pB~^g~q+O{0@6l*Y&n`D6s7%PQA$n zA)npYqwn^vvHF(F2t6;;CpAiY0Rf-RPz)2{=wt0#yYMN)2i5s*WJ*u!R<#KSkPtc? zpNG;wRw;Zzr^BipBZa3ptJ-^?<~J$HU2H|SFG{A_T|$qfUSou*`d$I85{TV&QHZz z_pH4!&utE+xYf5TfxleJm*8ZsNGvT5d*@XwksXb6%+Cx@X0!MveMiSEq{e=XS&ZLBfb@4$H_U$r} zRZLif{g<~~XQi9%`QiWBS?o!` z9o9S{kt^c!`S=DKRjbK! zCqg7iK953`%0wwI1%rzllCj|Pa|DX&d&y*;C;Ee#py5@$A(RomU`5zJLGmc^ZDNeU zjm*gUH;`pmb?Kk21h~7X_ccPah#)=uCH&On3`vTZHl|yIdoYTwEfU?Fz{3r+ z#txmkfQVsU=uqhT_p(s83hJJZyZ~t&=CwnEO-iKKI)`@>V@4J=GM!$g;$s>8A(e`w znwdf$j2^Wm5}q+1A}%7Unu5Ju0YS3hiaG_yp`Haxj1>B2HDAL?IXn?U?g18pK4YsE ztEUCcq4jmX`m2?kA55#i-Pufwrn=i2_&3a=k*E`a)F;KXhvuLb8ilZtwGjYt5J8Ru zSPRrc!uTZ0vs0ie_NuNSHmJ4Zu=>$oQ{J8)Ud#8Tu~BlNTvz;e6)cWV^2`hV8)Z-g z^7lT71}X|Cl5V4muhUHUDW-S~{TsMa^d4>ey0C;UsD*i4$HI0z_(q*zN1>J&FtQ)2 zh%YJ>stcA=%A{(K8~j^dior6@@0SwDQE|8mH*I?*>kT)j07q&HG1l|%ZbFPEFg9(l z@NkR)98HHmCaNyyVkKZY7!b`S>~Xh>5@3U9yuV@`*{!*eAGgL`Z%v1Tn7w|rEGOk~ z(BYMv0|>K`@;Ab&>=7HBDvIz1A{nqbB?;U=J3Jrh!VTQ$0tD zml`ZQr+R?=`_pL=NkA@GFTcLuJNrOmAC;%R_O?YeY$>l7;_}Iw1ZN!K)SV|he%PnWhYsvsH_cmbsEPEzudFX^qH0)lxxgD3H`eiEm)53N?0 zz%}pBjcz-<#yq6Dpja?ds;Yj_@4b}Lv;D`hZXKp34#iE&9moLukctPzf;3C)mv;}c zq}cH#YC#@pTuRJ`gWhD!b@=%;UK8^Uq)vi>GEU0%@BdiX{a54fd0y6ZI_C^iBQ-A_ z?YUMI>#J|~@@ICOclXu6$bSoMtfL4!&~Ri)%{b^(3f{#nxKr9_$SLGi1dIigL)f0# zT5*D~KsZ@p*T&^p&%cP1(EH=5#t7e_i1=p^n@eCEeDU|;DqM`TIja02K)Kk5eOd-> zb^prCnbx73;462gsgnxO+jVd&h=SP{0s!8Ye zBLUBgaxAo(N0Q>>R*cqb5;`>MZwRSG?jPMR8*8Wl(%7I^}R4bZryoh4O*Nl5~~xWWTy{ z*tm4tqJ*jTEMr(S@BLed2Bx9^Le% z7lEqW$R0F_1(+r=2cM0&i!Q7z1wp=iVtcqH%M!Y&>5!>*ybS&NX6kZN;)n9$h$XdG z)&9-rR$c9w0xvI+;x^G_nKEF5 zv0?52C868>T~;6~s|I~C2c6(E?GFGSS<`Q_j7DCaLAm$LH*<)eo0?j4mVb{5v(|cB z_60-`lK^Jwvs~=0De$4@116*W77mC}v@4r~f^EhTT09IcdkPPjsjk%b>+p<<1e-lA zTy%8LR=6$uPsu>HfQ75IJc*l<`0!6xCbopSm;izE9YER#mzuM{Ob4@`0+5dnQJy6X zR=vUUNnvp(N=YN4SWh*bQs6_)_wl+uG)@TI5FK7<;0t6u06h{EuFYJ zXdn5NRnMJUQu{x{fEO=3V&qrcp77t&tKk+u@So(mrCk_rLljZ{MZz{z}F$XoNAcF{QSM>E%ZfEnl9p=Kbc-&y2E1i4k(( zcIWKCoh=yuM#(s~s=ZE77$@8@i?MD{ois-R$JnA&co~EN6m^> zL2U(H0Yvb~nVkU(aQGk{R0gk0&R5zUBHc)?ecCNky=Q?h-;hu2z*FWLxxFAcGx1!h z&I?tpQTMIwiDt@6>#GEI{Y;;5B@ywj*bxKND!MTQh;BLg@zwC-YPLCdYg zKAx5cV{<8>-$uNw0DWqA64>{S+L11Ji9IsM(rv3Q9E8inr7^w)i=;RQi{m~#!LY*P zNsGtZo6s&D&dtAq!+1F~!Q$$`XQU8Yap<6Dsc=qWE!&jV@3FcTVBN!n8G=efsjE-e z^~sFR8q6hQn*NehoXIZ1P)k+9vk9=KDayD;V`r*ysa+R1#*Xko6A*Pf{%{d!mz(6U zno?UG(p2MDJ~1n?SmBFIz#Wm8c#vL>cc>L4-O({>z^5lN%+`l<92t9Ac&;K={`%lLH z&ms2)mvTh!;g4d%60Rf?S;|i0;|~Xkc5*R1twp+u^WV`Gg2^G}BPv?&#D}C>&ZWFC zjmQ$Hoi{VPJ|g?pfi9RpleDp1sE3paEecJQ^pS}(7JhcL(i4e0Sn|759R61CfR$c2OA+j9xtZqDni@t`2`Ao+t8)83VoI z9Nkws`Mvy^)}>s^EXUgjwNB(r=1kR8Y4C)^rCwDnFMvR6{nUjEi%VYu9EiOk8c5F( z2=D)%7~Cw?wYP`FMSJQ0Lr3dUtwC8%jwD(hC@=^dwKFkVX|Q+!>DmRoz_UP_NDq~I z7)ruvOZ5vGECa4@xo%MN84i}>Nz_S3hN41w7hL`dC2?uGySzFM#pf}Sl-e(1D`%qe zb$xwiq;vJS&lobb{^<2Hr_h39zZiJ~Z4l}5eA=Hn<3x=cp(Kx4<5*9$^?H7SbLSM{ zn}*{yTl4{!JDa?kh2JZ%w8PT9d+D^$yL* z#9LA~wN<#~OOF;ciCNyniAQ2S`#=dQI!d618DQvOa&OqU2-JXv5=G=dc8*Mgi=~>m zr8gU4h|vI1An$&qloO>cOT63FpldDP_}JaN&FJ$aD?zCjXB7|6@n(nw{Dj~&hDsfr zGIH4_`^y>XM6+4XJc0XF9@QVtZ;(!iWR%7seI8q0^9?mYTil?rljl{&H_O4ODoaVU-0u>%ixte17ONe^Sdosy6Fb{ zXcbe!X+HG&&n?->JAs<7R;KXge8Z)s4%b6*c(9CPoF1*;=(_-lv5YORVZq$_g-|8# z1g%>bWk!$Y_uM$1kpOuhu!-uoLA{;?tpf39$cA6E3TCHk0Ov+@>=$$}i+4~U#+f`} zv!H1f(mFrp+=3sq$k}2OQ_&|gr_$Rs^;%N!yaY?8enoRG4Zu3>wleVNMAyyq?*b>Y z4BAEGBA;jJg^8ug)h-%@ooh#&C33Gj%;RC*?@So=$L@1=T}1M!?6^su$*;3<}X`zZWtcT7=RNm_>J6y4+>kNE2iWs!;rVtCKyL6CG2{T-K3GI!rSx?&@9ULwp zp8?|A!Y z;776%GBPSlNEk@Q9Rm~~I_grLVlP8o6tOZQ%44pG=TtZAWeZ<0{-N!aB-pjeUl&jT zUK;KoBWdD$34dOgrw960kgBi?_ue!%k6gqoKPb6Frc0YUw_u3FscQkHanp?hQi*?5 z748Jk%&P(KHA7$4!ZI>z>G%Q*8Pk&4+ig)GkBM%4sG~m;){4>fkdoZ@5dYGH2zG!W zV_B;o^f3acRUY2R{X(~JaaWsvTC1Wr@!rMJCc>$zr#Q-ky9?LM`0^v57sVB}f|@q& z{*I6|z(NYoDDQo9-nd#-cPwBF`K0Z~4g3Zzq*352)yBZkJ}r3rTjHcjiqsEczq8Md#!d}B@`kj{R3!>Oy*hs{4A&HC?I^}blv=F;akD5-xe z3%8NV&>of9JLU}K6oIs6w`Vm-KrY~kPk&;WN-lwpxK5Qy)f9nzmlsT zb?e~in9V$CMW%z8HWCtO#-6;(SCBtj%Ui|w@xVFX&_RJUopwXJ6l!L>Annu~w81(4 z-!XZuSJuMJ^0;%@85Oz6nL{{-Qt^Iz#`ffa<+d%4e?+V1rYSKh5f9%lI5qhUmVGcm z24Jq5uXp@EaP&XP?O%sd2ZYO%MNI3$B)4m+nY9`g9modHzSg|nIZek*<#%)1L0QR_ z%a7C_@^ zO7r9aGb-#-_(|Z2q`3#rLRdkg)|RYdnoN>2MPeyBbn|Ol-mr0{x%FQ4!{)P5$D7Kv zwCrEW8eL~eM~m=cF%Gxs`(r3cbpx# zECYW~>Nea?i{GR(_&d_5rJ#2Fj-6_*HJ_>+{m}?lpxs)%O+_$@Oj4C8z1KyEt2wTT~-Q%VJRZczqaMGax&aCQ{V`E^+w8u(+U{QJYlw%1|%{Sc*$llR_B*m5ndtIh_PQ+4Z>jaULP zS0J%!H^DKF02VcG!qN&0M?9}u_<_K)DFr?a8%!f0c0TQKxcL&kRWCcx#!|cf$~~{` zb7i#Qpzfq$U;EV&qC_|_r4Y{X$8-r{M6m?l10x}hheLicn9hp{N$qARK9*UcM+GBK z8hz){pMHVy>EbW#;ZkHSukT}4t@aL0VEb)51^S#}Z8FW?Upz{ojA6H06)n>@S8Q<- z>UH;)@Mht|7lS8Wn{Xe-aSug5|I^sxf90P4vHE!J(a_MSR<$}04`L8&s|hY+I>Yup zRWc=Ng`N>SX%c1q9Uh;Rd`7C5qf^T{CcU#CwGfeD=}lmsE*prXJ=BtRAh-n318_BE zx7lg5ajH@^QE|}!Y?+?jiYn*_=x7vVslDo)vqG$94aKN{CNRn?crbbJIXc_;| zhD|6gLGaSj>M9D?N_#~PH`bz$+WVb?q#Wj}6=bO*Z2ue41W*fNnir`*m|ZnCo1?n@ z_^9KLKLQ9ShWOXF@-68%sOXTQWTBztte)@i#lSQp$KGe(|C6TGlX~^PTaM| zc20C~y@v!=6P1p6P5zg>e5rpjhq?>cN&{@?l(-n}hcB?tgy#-#mnj0E zc*t)p9e<3Cjb(Fj8~s`_zE}y!SWT|cY1vAt8PhpU$;epcaD@mVfnRQi-bq7s8dyf+ z>;v}ktaRF8FxaNi$im;fC*w#FJJORQ1x>MLDH@D^r}F9rLgY3ExjZYzid4rN!%%y5 zDL;fbF*a)(zxuNJbNb9lg{!Oqo>HT`H0eaaQ@@&SWKvzN`G9A>yUu0luM|3KszBFH zY0W^sUXiMWnsGhDTvCECR`to5#OWEI(Y?^qjsa^KxO_Pd-}%UR_ZRVmjn zcqC4nH1DP+twg{a@t=%{v@5?2KydlZ2ui<_KIvb6U1=$1w2UP+@E!*}evi=K5B<~C zw$B=TZcClCd^VV>8DzLjD>_ouk{gk~vQ;BYhb#zb1>j7R# zBa-8N8`qmwUL0;`hrZ-C4F6hk5$r7&Wmli7FJHw8RF6C-g~tg5AW#C}P(A?u=cpg@ zK{3fdc21kw$Un|d(9HNE_3`_13dU5%3zNMrrELvsB0Ogs;c!b zwSF2%?!FU*cJxKC7KfgZ!T`m;TvXI;dO5M-2Y}YBLn9}WEhHQs7y2RrZ~Igijp%)x z1vo!4U2qPgotj+XQ(egF^!SY(G$rDCP`Y3DTimW<8PP1{R~?<$hB5?JDj%t@v#Za- z={i!%E)eii#Hab^(}Ex)L|dKQ&#aXz`@`1-*>diW>y)#ylVC+x0IfcCag*5_2vrVl zRsWV?dZ{)y=^*tJkNglzte}T;69=6=k9J3~k({f!g+_P%jeWOt;*xtPZ%`X%mGq6U zq*eV(y7t(mL)*adLw1U=bK@qy$%?7HB3@8RoyTYBah0)8{@LMpJ ze>>dPL!6YmrKjSVxGGliT-UP6NUKM4w*7WVMG2V zw^j7HDgi_?i>kc_G*n-ij~+Rws0W|0NXxn@c-8r`fb%H0c=3G{qT7WY z%YzQ?w+LB1JqrF^@pD&$!QtVuKo^(!Co*ye=r=|AZ!d6j0)b7aZ&SPJY4w*T940g7 z4$dU!n^-7}+qMEW2c;E+v;%6>?`Q~lv#9!JeowIUEnGIF@IVF+U7D}wCXX*3d<2vt z%Xb|W2VbB>PVS=zeBoAvF~3Pn%32>b0_1)Df0XDx(|Po}CbZgCmIgE@rx)k!zM2G= z<1x?6U%`mh6BlY#ThW{=opy6!C5jjy0JhY=6_WqF{r`CgAIwnR8zjQf@Dy&t`gn9V zI-C2m)C?$LZ(k)kown$@shv8j5Y3GcnL0oaW_)%FsjOX_qFR6ZH7PjyTk)XTQ^C+Dx!32jvMGICQ>bRAp5SoH-~t(s~a zw10Nb7h6_lSQX}FdyotzCA+H^mMest%FH=> zR>0jOX=zZTW^fonIGrueC&vckE$m|n1s0N$3dy!TS&k6Q?kg&4f*Tc%=9G}T_@{g4 zn5?p_gZB=jq-Eu8h;Fk((eOo5PYqAZd=yWxS${B<Jop2V354lPSk~%~q!*Ge0{iuuB28b( z(kY3uZ@Vi#ZwBe!NbL8moJ4&Gd*Zc+14sVZSKEFkk)p)Er z>_HbsaOg@-2IAT`u|H3|RF$)XkcW0dditdu+^h~rQ(|H8MdYCLhHX)y-$qEL>2b!= z(3bA!Nv$&~>YR2YnNF#BZ7sLHaLts`@lwLGW|`Fio|)PG63$*FMP0N9;TVddZp-82 z!3w4#P_HpZfqas74*7adBHPw{$nfyc&F2Yb8yZ6;O1|F4e)Q%8SOKoEBYa%fEtl@} z$0JqEm%Zeg_3I2rVzrdRS7!^(SWkji2jvUbhvx98XtFa}$aDZ{=LubF%*X~CE*wK5 z%kh7Of66HcLLq4=?(qt@YvK7@?`0QmPdO~F@a&WXr$@obE+IY($$`jlC&&0z3JXj8_}p<_cwEyrJ$lz>rkdd0=G{m@c^NMyL`pH zGKRdE)k}qeVrf ze^;@5~h7nfUV^yAKSEK5NJ-o_2|jJr&*HD~XVeUPm!8eO0l zuApZ##PCpY|@X0QXAqmkdujTXU;v@bVHSQe$tEGy|9J+&~msGL~&J zCox)MQy01y%Um5VRVCJpmP$9I8)7B*Rd>K8dLF$#CPG{lMi5Pl8;l_gP9N-}e={qgw_bP5RSXO;d6${CNEB)^zlJmD1%RtDFSC$FX?- ztovj}JBq8XOLRvpTcGERnHB0pr>%b`;Se!6ja_^hlb-K;phaq-sZq1j;Rl5S7$x6|8E?qzY- z$3k0XA_Mo)urI!S0=}Z(fRF9sV$4T{FcS?bW-1rf0)O+$g7@c$}DC;>rVfDs3ZTY{4>6lg0o3ZINB)sRW2 zw~*gbk5t0oFIMOOt$*;=waAJk<&>AlR9)Q5iFq9E=(l1?S#<&iH?%ZY`1Vu8IH=5kBqV? z5bLUF9;}2BjV)1(2Iqg#y(jUq1KH+m6*4rj_W6SKx_@l!^`5FQhqZuexb0G`a+2m%IE8b=#q3#&@tWASXi zxe~IH%`jvSY`LBe>?x-~dG6UIK=C2`KU zuJw+B&hpjbSrF!Mz?=ks?Ciga-~XHbxn7ObbdgP1q6!iQsZAsVb!k(Dw$reUX4UY7 zc9hkI2=Eb=|eDSu2PhvL)>tL z1$_}CYr1{j2o>aCwPnDj%VZ3#pWl&1)DrPg+aeR?^6I%BR3&p_Cyz(OWp+YEWvK|L zBxaA0ei~VX=W^KRJ|p7134X{OZ#sb59kcinlzgL@CXYwYZd}hNj~(C?>N`KaZM~J) zdLBLv(DIQYFhFDBk#hK;f_x+8;FkFi(<-d>kc%SblSFrPs?P5Ak-lRv&xt&lFy|M= z!t%^v6lyV5R=J2PRG*)K51#2Dn@%}O1~IeRfR1xi%1kYh_fI>TpAvBqmLUDATb1U? z8}my01;}kHar&e6@X+sO(&I`P3iB~7t&=F|M$fFNnhrke5uJcg zTeJOJ+h%{{V_|06WTDd7#Kin8-`}(20q;=bc&uqTBEN2hNq;56%ahMvSc(PqzVYzd zHcc(64c`E3nTwLdC2VE0bXlOUy6Lr#WK2e)jRS9h3s3v$krx>VH@bQhk*wXhXwopP zd9ZoLo2Iu&AER({}67kp?&6PaK z$=eE_YYxf;a7_~0)>0lsE5=w?Na4y*f0m>D2TlF!LWR<(w(oC(gw?Eol}Z{^(bBRZ zZDl8#eKE6T-sJ{BJFBOe%5Ir;U%g=szSintD3hEG)JF{+n=HG@VvtTh`4xZIjsC7- z`I2t|ImIPW6A3>Mfba$Xsa#BY)TO`#HW&i?b!I?=XP`5uofNO#tj6q=xr0I(EC;vd z@i}+yg-ss`TmE$&iQrV|6tapCE@fMr%#Cl7Jw5(h{v?(8qr`T=lpgFFeq!dhMQW0; zH#r?&q!AOZMT(}hFf*7|__Y0w`v@^VL53(4n3P${H#JuKRWMq_;zl*Ey9#wAek!8r zvyp$8VrlGZMd$UW?C{E3YCVg&%}iWi&-j=JRj2JFcbz5S?G;g>O#8X{%M+p zmeLDWbpLH_t!@=5v>(rZ zxcKl3`L?B|0rX*0bTMOYb`YIV^w7_umRqZZqrDPUAaB+PCT(Kvro2S$2b8b+mX$?d zw;xe4uBnk!NIyZ$_KhGe>$<{&g_c5uIWZ>=FN0V5w->72fRvz6L5MftrQ z?g~o9Bp=F-BxT7o87D#KUn^ZU2QP8qB9EA9r5i`nfIVu7Ywq%$&^8q9fVhK)_KL=G z<(9G+k>Aa$Tot&41&5^WcY}67H6LcqpbuYJv)TucVv^JM!s?;1pEmNvjC_tM@H2@@ zdh${*CuYBpjcqMdIaeb{^GVM|vCIXe%k;&Kr>n4*jXFCsW*J&C#jJwJM-mp94&DZ> zcI+NTH-p{Qb8@PU%-A|`3{5KxNf@wRV|94fz3S{!4o*XZ5YM0MEZfi8DGJ;UN5rF%_y*<% zpOwVBCs~m-hp7eVUkdK${B33$+shcA4A_e%Ka|l*bI0CA)>LV?*KW{EV|skPDWMwQ zPb+T$QxNhJO>c5r+O)LLqb;9JiYqMat~PY*k=ECZx;dbH$T+CO{_lofX6y$=k=^1v zEEa+msrkLpzfT{-CcNL>R@;dPS_t4qgAO*D9VrWYnq2RVzo1HRlGinXRylJJ$utzQ z>z1273j8E@dzHwIwCVb~)S-vT>Ungc&B7%)cP6$rFfdVgA6b|wG%PeMW}N5$u=kd6QSDtD@PGq?C@CP_At@n94=5lV(kYz-N(w`Yf`D{4 z(v5V3N_XcFN;lFuGw=33_j#XlpX2>LpWbiJH}tn>?|-cSTGzVPwf06lp=u5-Ye*B} zy2&;w4$0gKp=j1p%j?{C4wjK9f~2WDlWDXw5dkL$K&q!9gWlN!MQvW&KKbun&=U|I zm%cmUgD3O|$TF|JJcdKJcIoz<>x-mJ7d!oOt z^zYnH&m^&a*tv0tUC0D2@EMbrkIRDDsI^y2))MwB5HCDuk=r!S?DC1wXVkBbKqwWd zEwhnu?%TPZc?j4@qmWpGY3bUnir~yF*t0GCzml*()^jGAc?qTsn;*f#Rcj)YNHh^L zHnnYFHc6YDeg5pra;D?q!-^U=RfTdEep`z~nT*Zd&HM1$yfb$3kvM$pxEUFj!M)QQ zPfM!JpHb?;*L-CxI`+hRAi|oN;4)PfuJ;;=mHrX+08&3rq>?XV5&+z_2^;3NYY^WD zW-e6Uo#dx73d#bK4Q`M6SC$)CCj{g;^5f=aEnZ|c8z+9^W@@r2168_TGI48&PO zs>U`8IJWr};Ij4hhFIivd{OW6wq8H%R8jpr$Vp~lnZ!LgUuxKhrD7JBZPy4#u6P|Fe$k~^(triNpI)et|suiax zr!9~on=j%|qh^k5tMUaLf4p9LY~8m(FIU(06%Cuhl)MvP-N7TC)>-xZc_EHt&1nBKOUO93sk^ z8;V}oF1*m6{Ibk#7^6mY!7s^qbLi}zR|Q z$Cd5iXO_k>OJSYT_=x0*RCF|~Rb^VP58NgAqp7Ywrqa!wV`YSf(%Cr1s5%)N<#5Mv z>vV~v%!V4v#{(8tLRzN%n;TKs&rY>U4{xQs?#v1^?cW058hlKITo1DpeF3+b(+>~y zK`8F2T2&|GgqF32$uza@WLYS*4lNr^Z``ZIxr;i9()-D^*t3*~q8Pf$9&J?@7qL3S zf(T`=K>n;N{Fz$y1VO5>A?=!*qhyFr0IV5U;VBCpH42#cW_hoU`f8b&B7P`;XVA&s zFmRYNIG1_l@3&lU(=)ZWWd;{@PSw zQ(>@}G?d>{u}W)m%rJ}HVON;|d8QPVJb$c72YS4dmi7AYtNUD*c1}^qk7GErU`>B> z*zuXAN|A_n%lj~<^LxH6JxDggC6ey5nP|2fmrb37{7sg$J>95l`(gVl8beD%V*IP^ zyL-}7x&qyuJ37mbMC!>565&H$#Gwy*&Au=!4tEe!sRnQP$=iRmrz+6sA`2m^cWJ|h zbn2LrjOIl_d99M)Aus5Qxsv13HwUhx6CB)64Wr%q+j)@JCE@vmVA>HR*36gs30bc| zds;*1&YbuHnaOwSgR#-)8Lyg55rJ|p{Q;WRzS=Cb)%GXGwoB1Q#gB;fo%Ita%7u>7 znbJZ@L*pO=94fA+PfO%k!WHck;j9JDY+MVRf4&P~|GZU+zjyqtV8LFVLsgH}XwNJ5 zOC%K`62U<!s=m|5@W76u8b=fSsyb1NXPWr2ZoF_f@nu1}7_l$a5Ne0ykB%4CNB2 zg@3k1;Xd0Uy*_w6_X$hE{xj|no{^R`o>ErnnfWKneKVJiBEs!_?1;ggR^8pU%h$0d*IHjWR~UPPM@O zP0x6vs3Jx?_GttoJ|skOiO_-kr=-3d752;}^9}XYIm$hE4$O_$8Ac2v2lJ5m~z%=v&#J;eGUv?1`taPEqP(&e9ieXr^wh%>* z5UDUV8rhFuAh}bl1H?=gbk+W>4g0_66RNc6gUq0~)9A#xg7>~Rs?gGH4n9++G#AX{ zc*60oN3Y@o)-W0eN>YMd$E{P>rie!=1><+}8WilmMANtPFOA?GgmR=V&F8ZxpP8{k zi=s@}KuSm^vy4<}6MntwfOigGTy3bnC*R0sPcL)xPbo1J$Lp(b=)yFX@BrY&Pbj~{ za{t%fkYSk<6BNcgNUb*|kTPtP+tjWJ(ARl3DnV8jVF-gKDZN1-WCG>x*p!sdS=hYYR^@@soKrJaH1O%=L{vnF6OPSR@H(3c zn?s#oiq4RC6U)*hb#1=sE@nT%>Fw+}kAh@g4JJiv+Qs)vYAq_}Wa|m*=FA>s>?m$D z=u!bKwUA5%;pb-iPh&%2dTK&Zx}^dOR_dQPMd92&Wumvyc?NH>Fld0)DW=!M3x+BHT=7+{N;T8|6n$dVgf(Y z3LV-p7x*pqDgyMAy?Rj&WAmzLWp+Cx4}WGsEzoQ;AgfAHnB(n4HKsAz#wTBo?R8^H=Wg zeS|DmtxY2;8&vO?f>tFmp4d!@) zUEj|2xazCjfgqJE6o|ZcSqOtWrlC!!-qUYFxag&EkmaC&V2QIA>gvn!Mt3Xi?8@-M zruxEs{$9=oo;*^Eb#eH$gXbBA>`6J)rjr{!HR=5LlJIi{AId&QfB2k|5)X~xd$%)m zA!6eycWEO*D?c*V8iN)kX{fKi(~*&!OkT%N*W+W~hI+_|Klc{GL-dmdT+2V&jdIzY z87+0+9Qo?~;li`ii}U2(?_9^K{FewEW@YL+o~IJ=n|1nPcffD4W~xlgtb>`X^_P97 z=pu2@B(ze5^X$wTd`x{!(+q~9_7k&+{0JqQ_in*`*nA(b#NoBDkCu0ZXiVodk<>oT zMBuaT(N<^%%JquxC~Vz%np<5xftU%xq*uN6xLdFO_t5SKfiZsX#)EE2(@==kU{?xo zN%=zrz`K`+QlBtr6yaA{_PE)&U?+x`rYj%B2^ZZoA1-)so_kni=n9nWuw23jSnG(u zG@7yKku8V2?eN7n#7hxzM|?ETF0fmD?V$%?c?HHRU)b}FxIl%Cp|so=`O5q43~E9S z2OsKv{xKR5c#Z^(8{~1=T~?-$zy;l*Z%DRe`kp~Y5mXBTQv{_qEaVP)Dw7BKvEYl@ z6Fpq8D9t&XUr(Me)<9QXrWjsynVg*DJ%209c-4E5eop}lLi--Fx82U@vOc7)rL~Tn z;hCJ7Fw4N}i8Z_PcW3s?@~zpyUaNuk=HVBgf+e0ItK*vBywg9rKY8OzX==s|ENu0~ z3kg}-oP+7xJr!1uT9#KD1S*!q+jsLv)F9FiK^~K?Kfb-^^_{TJ@|{EIql1#LI)vk& zcS3I~obN9z1OFSE&Ri}`6zoF?2_v^1?rTT*BD`{zj*?xB>&jIUbMx=PZfm4*5 zp6}doEv>%gFQD-9tn~Kt2x_rZEb}U(fjhVq^#ZV_1^}}E?Lk}6TRv|W7^l`_Q4d;cp|jw`bXO!AeWycXJz)A&*>Bv{~k^LUg` zK64t7PmjF)wbBfJ_LQDK(x>ZQ;yuxXu{=U18?LN0SYlX}DGQ|Al_|eh+);Buj|_&K zbyeL+oOKn?Ugw=5IbI>~tz-QjcCEYbV9;hf8R(z^XUZI^q5bpl`)f&k!JALPNOi|2 z=eJg6-x6EkAVEh89Vu&&puGcWaJc7tpP21xigb57w}8wEAbav4z0WWGC-$A4uj^L~ zn7~K0j}99ba@`LWx$H;8AL08Q`SpJK$Fibf-RJbr^oIeQN{gX1ydrZ03$4J~Q9EUK zCXwAe1P2m7)lTn+?>Dzvx}HTFDS<}h=LN<`S4E22{BG3CjQISoB0w6c=S7Jy;uA_b z5b-gU9nAYFNFC=1z2Dqoz##EsJT!3T`VaZy-wlTJkHIWA%9ef-lpI!b#)41v6{Ao5QD>w?pHAQ-MBW>J1X|*rE3WO*HP4w!Hsa86JrIw1Vi8 zx6T{xKqQd7TI+-5J*5oTvZd8GgkvrG)9*M#wHTrb22#aTii+)WtRi@~w5Ft`K8(=xlP|p=6V=Z0RvE_hPJ2FD?B5izq zX&)}<_h0q*`my819X1&Vi3 z1IbVVK+VN7k&5*j3+#MOYyDkZ8?8WyJG)kZzHS~--F$P!qMo}P!TsZvj+T*0&b-%J zufsT=L_{3XaO3EK!)MWhlK_j9!RdJW9VUUFm(y)MWTf-)^@U%r=8CylrVX9mir%bJ zIBg)f8TSsx8e;B}&1<_Yy4{-f?hfm`O4nf{)#@#~jWkqTt#m@<`bIt0))%M<(MO&1 zXRNOKiZR9H4a8bkmz7~#?vyn&-z<>@H40l9x&-O8Wb(SalJNL>c06Ck-FB&o^cdzl zy3UA+E^^x&lN&<8=_-FTva>){&gp@5iv`6h-@Nu#leC(N+35M$PaX5#_$Iv!<^Vfy z5r{UDwdZ8=ZsIN?4V7~-%2k|4=>-owbcZLr*|?MBI)2QH>cu0SEH5{Y?%jQgRc#!s@Yoy!)pXeIy5*a>-OFg7VDiXR_xVE=(VcmWt?ZW+~v4; zm8xoobk67*V9$7^|MtpG(XLA|uHhV^!X|-~mSQr|U2Z8R)U&R)eLFv+G-Oks9mRQF~u0UG_&c^(Pl|?YxT(>0?WJ ztdI>TcQn9yV+)KxFWVa)Ye)vBpu3Li9t@AmVa8c{-i`TVBqgE0<9tjIf zg2t|u2HwXEm9sL%y`#4eQZ+Vkh(8UrUKm%d<=GW|sE)czJ)^Rop#22bjeA-htuF){ zQWN!o(se{xt{WaYHWpsFtNzsDEcMdqC$@GEFGEld8Vt(G&LJWt@8Y+ZRc|?v%C2yi zhPB1h&F+=gNMPnh0pQH?HRB`wr)g$skGjw4CUP|bC45^Ig|>{<5#!*thOcAK811Z( zRWu5h{l?Ev%VRV@JdApk;V|~eSEcagM4N!vtYno9bgy;ee&MBAh+Wfput;zHp608< z66NjC8nJ_l^W5dqgZVSnAi}NYDRuQ)L^2nf&6s316R({=-0E!zHgna%8>Yg0FoNQ; zxNT!zIOmzTjn!7=k0ailx%@Qi$EOobsoqAWX0vx7!QRTE;=Hid3FXu^0gTf+mrFYJ z+SBbZk1!^SZR69MzS$}7tw$y0CnAo)Qhv?*{>+MZ#+MiN1qY`C!cjE=zR!2 z1hh={n_M|l4F3;L$;)HjSKavadtRl;%ZVv6=s>v8sW90{Tb+T=q8eGs*|#C@y3|ti zuFPuJs?vm7w1vY7oP(Ygyf%(u2`}jYTN1RBiz>(7@pC{0yws4e_;BgwXVA^Lgf{bD z)9Jp|{25#}g9}xEY{R3_qF*ldxG)@BPM1q`=ktW&;WV>M zns4XAQ8~rXtGB9NvbgL(WLQLxlsUiay?QqZH6hD;FzS8_W}zk~b_RdOO!Y+1l$_o^_`yt#xiL}9NSuQU+6m)-oQCgzI#|w8c1DbHT-ly7 zJ1!AXB*&5UB7^I6a@doYi8pIek1k*ey1h!Dm=b`oSP)R@KeBONRed=KRDB}Q(@(Ip%NPi zmDdIsnHxq(2(;I7)x{#{pzWy)+UCr-tb$EHO5RoEI|?exr0<_4=VT0#3rEAPptaf=(SZ_G86Hq z*f8DlQy~vhQXBrQ4=LVpCBspL!@*$|+m612OB1gPC3&|ZDG2chz?qDcO1>u4k!LUT z_1SSkpMxsDdcj^if5Pa^PdS8n#zN)xV(xQIIQboclfx>DqY6}Pkz=*XN0__ABoCV3o+NNwj?68iSZGZ$FI*!1Jii~azyHgqY+pSUmy=lqGSYr1DmcioJ3eWy zvf?JWV*E5+EcOi+x+-B9rP2Lj&?jVK^^fu$!jJ|9dUi%SNG7O)+dkzQ*d>O ze5eq8(*E9YG14`fjfQE=CijPFmd0Sl3yVHuhlskAdiBQLm)Hre;f~~)j;k0bgd&>; z=UJyHrxH`0pvjo4^?O248@%r?4+V=!?p{h=q$L zMjth1s>gibl>fLM@vwsstpNYN&8^=6OH-CV@J1vyWBm1c!cuorGe610Dj?=J^~B){ zhp&f{y16aAHXA%0A9Qr<->cJ;4ZOR(F$Kh#2}ob40XVa0n_Or+*3NwOgQU~!Qbb7I zMVotDtepcgqAF&m!0ertvd}rrQ1afYnM}hpiJ|Mr>xBNb4H{00(xysy{JsP*yTL?; z6!z>6-b`QaSr7QVH>u zi3t6@)mdA*v!G|B94=WZ8RKp-gsw@+o?R`oF2hP32kyw$F=&1bQ|)INHi>IZwne0} zZ>tL7vl1R6QMM(N9Le^(tBEnS?C8dWnom{H#c9GUt+35(EBQ-y)p}Z84Z+n_w^FvEYXb?#9Mf=~W3tALnO2Q@=E85O8onLl0)k+DJpqvjnG*m%!^` z-6jb0zFw5mwN#o&-v@2(s4Y^_`@<6EBD}!@t!g{RK++UbL=)r8p;?%l(x0Yb%wwo8 zKrL}nQDr_AX6u+>x+V*b%uijZOvK1El66>#)>i_0xxTum4yhX;=>l;^Tk}@c{pf#1*o+&>4Ht+$0)PYMgpPw;z#J1Be z=Q`yU6M|1uoT-4kWot~}Hxy2%)E{|G&oknFl^91|n@gtXl2I?%ly<-V@leoGk8B2t z_IN}4iR8w&D%`lWcFTa@@*KX`{jHUKAqoP8M$3nVJ7L%mJkhfv%Ekk? z!**t+q=zWjfx^~&VfH$&u%4EV+cU2v*E&(Q7ghuDcHF|^t$Rxm&Ge2%k6e}{hF`GaRutwEQpGfY)hpkn;f@nFnsE))oClwiC5Wwe zS#i>go#L-CG$`92hG|!Ql3$ZWAXGWhBILGiSpN6%taKS-Eov z&=>se>0&C(G}-%_$*ArSp_gHA_6U<+_hzdjyQbx!`dsO6#E7pO?JKif^j_B!xmp?_ z%;%|kyPO7<9`d?6+`Q2*L_(uyR<9I0=c`FXQNaT(sKGZik|@z3rMwJ}z41fnR#)f> zu}kNhnVg%==Dv=>vbNy2;We6LK(loVH=~oqmO^iXOPhC)MLt}#za3fjh4t39*O)p-R1eXJvwxfk5 z+_}Q9icv3ZTCeLapXQ#|jNn1MrTdh}f`x{thasks`!hH6Na|T2`#0q8izRYUu;o6L z&I|JJOCJ`$pO+p@DYgQCaMm%)Ws-0W`bja);Y{FHQm8>A!@XkTGd@TO|Bnw{h5d9}dF>oECEaVo; zQMEeqaZjgB0(C{&u-z4ut6x{zI*# z$lS-y##Dr8MNK(V%#>2S8py5eR!-^hfjN+sqYx^-eR5Vy94hnf6(@TO?b0W^?h~DC z)Kp9qrP@|Fq~)oV(P0<{eRTV|(s~Psk@}@B-e9Dzg zI-h|Z&P%lV+jo!|3%;H;QFSCcJ89s(>`95qa`U9FjSKZlu3xG-+@4KkAc}bNw9yor z#Gv&I5Tna`x;vjc_8rz?+_6pS8t|r=96Ta4!t zS4GV~GuU%W?5ujVW_oLl^6YFCL7q3Wb*o#~++l);hZ_=oLJ2nt4vk0Vy3pL*WjKqS z`G|kFA+rJ}k4~d|dH+2J5rB#EI$y~+OqGZywcbDsza|jXcdXUAxyXz&z@W7&b~1_E z_1MIy+&QD@3zpp@nD7dK0!i2qdSns3WrkL%htGfjM?oyKDez>{YnucumbDQ)IS4e< z)*x1NDk{1|+*{o>`LL-a(N)`U&ZfQ2*%eNQdzbUWY`Tl=EZ&AFw1R7(Wf~PBMfHX$ zg%}aJwTW`MSvZGhieC#5B}hU~#b2{* zwRU4VG(XYPW$|R9j*mtatHKB7ts#6|J{|A< z?lu2lt%1S5Tw+=}uatz7`sGpj7ISEP>X0uZfUI%*aJ3t^KF>>owc$`y*Y19$xf)rT zPpMexL7T;akZ$#KPCmIkAr`*(lV~V^GSfvMH zTs}$$P3(Mtn(wze=BG*;#KxjtD%}r}GL*NtaNg)2uuFGR6Lov}0wWWdK*K?Wc^+(R zm#>)+iKb1>1~5?~@7sVh!@wtdog@I?k9^)u;P15juW}V8Hb7I?F{5pUxwl64%Se0& z8fV3th-*ORC3q5BY`Ug1kcQQHh82}@oN^!5y7|j$`wF>s>y+}=Yo#dzS6!j&*jok& zL%f@&-O7bPr}-aET@iSCy|SRnGhCq`cXVN4YpWA0XSlYlUu%KH47=AdI2Q6;@V#IsW>ug@T?q0{5KLJR3<#U3|^?7eczAw7he8^g; z)n7-ZEk>+u^fdWh>GW&3f_sg372aE2@-=nj75!4XJy&5Q`p6B7#Qo5@3OqdSIURBO zoT^a*<~Q2W|AyMJ#O;z&UjKt`aGXzhzwuCU ztKiaXEY1s@SwWQ7_k+cV$#--!!`lrmy6`Z%_O_TTz;s*am*zF}3c3opMk{A=Z+B#@ zSbSwSmT&AmDeN})T=1R<4E^5A_$KELgeu{|cPD}*VnbWD*vLhien)ZV zlPWGm@F_#VUgSD4`PD>2<AXk8-)t~|f38r=c|`wnV4VFB z!Nf+#@#U!t^tA1ScJMX7BD3}Wn*+l2h+cln{T=frM)ZOt20ulOUo71~>q^`gm??VT zCI67`*~%A0k^l+7{@1SPja|nYJ|2z;d|AYuy;kr5s`Gk0@0shQvl{gO#I#^~XOx=Md6zbp<$DInWU^IYUSI(2Uo^t@z{y&DZfsU=k9kM^As58N39=lOWUox)CIqp(Ko<80g}2=0TLn%$;u6716H(yJYh@3n2ArwbS9ufIYo7w6df z7TJdGPD3>(ABW3oaIQ@0c?-E`0u*iXZA3wUo9&3Djg73XTj3`*Vb0iJEIsDQ0aJ^o zOAKLJ59(4|LL|y|p#Gt9PGv!ny*z+w0n=`rpd~x{Qpjtf5|%30LzWS70lbgZcPrCu z94G}b@|H{4f2>fvh2D5jRq-ruc+%jsu4`|M7n`~eT{WUb(CI7QN-bV(mUGsJ^x=(c zm1(!(r?rtr-S#ZMNPGG!k|(QfCBaW6SN1G9H!zAju2*KZW_EKJ9OAb#m>Kt-b{sgg z%RO$&5;%oryQ3jIgOrK8qEj0l5iQ)rj^5LmEj_!J>LRbTnoF%OrAU?HbSlRC7|eef zvM|l=ULt;BjWqiNEXW`SNOzwGMw^`7fssk%5E@jrW{YM;03q?QlKYm2JXJ276hu6_ z_cWy*<;rEE(}zk_zXUGyS7W|F~4U0v#PhXAIL zZ;s6TGmAOhb0!B|14l)Wyf;zav56wF8@fs!MZosG#?vQat7W3ZwbGxQr10J`J6e=o z>TC8`J7mpPPLsEYNE3)>z@1kUE-WzV#9`i-{^Gll=JVOU>T*tZ8poQx^&u%*4{qy@ zKT7z=_92Wby7z@u?`XaeyZe$$9>-&+=grVIQ$=@?O;MItd#x*74#ykDD0)HEj$G0s zo4_Gxx!Gc_GmEaV^e3EQ;ekmWw9RpD-=jfB&LG_VjgB;lF2(5M@@84fE!tM>7p(wZ zR=wPL4eMoN=h_l{+<4bB+= z?&*WN7;JT(Hlfz)=c!-MQ|XEDMk;>tj2nPe8=ZCbv*v>N3^O>B9~KPw?#I;kYc*og zia7jCV`)r1T+fNq;SEb-nd-L$(%U4AOnV2ZGFKp~;%Hal#TVw!&t}dp`p(KV7RA9k z0%W?+b6XoQ!^PE?SotP0UWdC(cj&Y_2&1uTJ_!~~T?4#V18Obkc{g248ZSIks3w^L zIq#utl2&-;5f+zs2eRe$AmCkErt|GdcDSeO7VL~QpAoNq{gr0vMtA!i&gec-QNwp5 z882dz`6<5s7)CBgYYf6pT5`M#xv$b&5?qYDbL?1Ljp>XSK*Vq9p})Z_s=MH~nETGe zetOG#NI}F1607E ziXdLJ%xi}oVT!s5^Ope*sL=Jmhwdg8$Snn8zsAB%Ga)aI-EwsVaZ6?66ClG;{KZQ5 zAHLw@650sYt<|mWk0m;Yd~eK8Ed|#=>$w15KElKnNw-+$El4li{FN?SE?s5CLhCH2 z&?CJI+?QZx-Crlxs+sJwW-d)lKFmIo&V{nHWfUcT?w$#PN z_^)Xmy^*q2A=%euCK%Lz>JMQCCQ_;cVl&k9rlAovB#g0Xc=h-c*WhCCf$q<5R+T@G zmvYDD+jDl-i=T{|Y!|xAt#!9`WCUxVvCdZ&>qv1NtEIa9JpDy2YP$o9Xv+*ifCFO8 z`PMZ+k?vgUG*zyCD!U82+UMe#&Ixe*aIM?jH>N3v(T(xnUFkO>a<#HU{=>;A+!9g+9#BATtDV!L&%-UNJpG?_Bz9`(uV-Lpm zB9Xq&UJj&!f&VJ4S3;*FCt#E_fEtz_w1ySl{u|kgwdOBN0n)ZfPL7ZvOflK__q4THg1TRN_2ko>vo8 z876kNq%mOp8o=sZJ6yPEJ<3J^cXE}UU2D0+k=+0lZbCjI)sXLmN%WQMq!UzS_U8c@ zUbm_6U*JyMq`NwwvJJk*C8;Nj)>E<;DCQde;i@57(5eU&JM-DlY7&nq=LkV#vAA-; zf^GP#U?hBZd+$i#(hOky`*-2ST0ah6v=x3=0B62s$d&w!S|KD7GV(Qt)SiQk|7#XG9!5{R|B7&v9#o$R4eBe4Fez(#Ly z^kFHknt!+}+DFVF_PL9so0OdTE$OpwPCuSAW=&UR_ITD_G@7ZYGkG^EXds?vJF_RZ zFaVJ%JP}Y-`~z#Mg8x|kkndg9m0VykS-=2ynQM>s=|PXP0qQ9~<(<69I^u(lxvXML z!lmiA@Y$G2<#eXUM7*o-4sU;c2l239Wqr z42Km|Gn*fXV^;?TBq)2O8{g>u=fz9^wvxS&KNhRW6MV<1LUNb@T~%^q?0G!!3w*yy zFO33BGy*VfI&fzGF|WB>f2d8)UTxsO!v6JB6S8qVOW<0dH7o=FOp4YYkoKbA)%xdS z@vW=CgtzyLi;Hucn+<@`?e00$;wX597ZnxdWMxU=`z7BKymuS_0c>(;tjbE|e7ptb zzBQH`8XF%kFDDoLPKxfF=#@^4AhhAyWexk%t$lFqm+ z%APQ^TbR|_lm_nMU_uO98aWxTYWO1|4ix*`AOS zcG>(6cz^#pnmuS5tkHl(uJbtv&6?-i#>8(bWYrJQBwigHf6zygP&C(dY%R(Zb_%x# z>v5u?C_U&#(?4A{87$bA2Zh(+qLW;{X#M)UaKQ zcSkzfjrQ{cY+4Yf%th)yw;1G4!t^rWoEbFxy=BeukUnzu4W~ivRX z5HQPG=t36PP1Q06y-1g%f>`=AeovsEm_G87*T36-abSP&84b#eBz`ao)qNENiXAk} z!Ucz<`e8#zws&I!XM;Xyf5ai<)meS8N&TDU-ShLKNj+Bs;*bYjPX{fL@4#F?nAW(C z>g5l@W7_YXwECW3ySb08{CCj`2&{Jl|H+bOP7;3p>H&z*7$bwljV=zCBO;I{ zv~81%>m+;ir2e}(qI{eotx6dKfY4xnCW6#o@uY%=qP+$>NQZnd185(22Q6r(@}^Qi z8ZFad&?~g>%e1Ok=r0@|taW6hi@}7Ko|jX?TpRE=GVlGya%ojL0R8bO>J32;H&N-* zgC6d95v^v#XR8jpF@5eA#-xtJTe^1i-!Z7U(Pw#Iqqm6#1P_~hDl07j)54r*Z`Tx_ z7=eoBP&D+DHSZgZ=KLWk4EQ?_;ANJMyLsu-y7k`z2A=pPb?DL*Nv-*6+r4AMP%6bh2ROJ%?GuHN%Elb>Tl z*LT*QA*0w3%q|=e=A*=!m27|eEnhDxaHgq8?53;&KH!fz zIs3X%7X>sO8S(WvslO@X{u{eO4vARx(0WFa>Q_n^6)z5VK1g(;UXHe}GeokYdx$)7 zyj*AS-QCrS0QxNdIgUSLcnk!P?7OjhnX6>Yn4w25ZJamVySeS}Ci5i};x=n^A~zp+ zF+N0%#YOtNz4_ODA$R<v(JT8*S#FV8A~R9AX4I zGCV_%^xaF?s=Y>V(2c}nz4^*=nmh(oDeboV@59EorUC1PfhNQqZfj%ze@yS!J{+<5 zfnp_|>itG@_2*Fjbq8r_AiNhi+)n#ViReEryaa$+Uj`lLe_hl+cHch>5U6Ts)CCOv zZx`MGL1%3^lH|YJ;veg*253gvir|t~|J#K~0D>J^J;(X)X85l=WET9whLd}leE-XZ zw7vi^yX4Ek`2X0EU(2hi0X#Tl+iisGf4LBlGlMg4$i~or6XgCn7FIUk!P2fGn|J@0 z3wtsERD!(wJ8ATPn8$r$;K5mqR4$nR)tvwEfUAI3m(@40`X3PIufqf891;RNSZx7U z_||_s=bu~te`fgKY~ugS@V}qu|1-nCowxsYsUSeY{TN+UQ?q6u@$v9iikY@Lmrd1Z z;+}@%J_|{Y+}PN679)P*_rg6c!&Gtr89h5^+q++!X8^+fZ=pby75%JbefaeY^SP5* zUe_+l!otEht~3RpvP-(PRGx5mcS!;Bq#t>${Nmd6tLAy1Y=#eaibH+!FB!bR=}#>+ zR76G0T2MyU^I#vV`PVzABz75l*PBf=J}Xx*2LSp`zXb7P^)EBo3;oqFD2_N{5!%TT znO|tw>Eg54VyYstb7z6>G}dQ!bB1`C~?jF#cmMTi^58(UtO?%}cg1rq}8=0JL_ z1`^)dO#xKOvy}(?`aP{M`_a~Pjxv(}aH+4r^ic(=PWyvL?ZajZc0_Li@9wi8uFqlt z;(Fm-^-+j?QepUQ5+1_P&ei&Mw>X3>RP)taKnp4932zzhqd4$-$X@=4H#;&Ar14;} z`Y#|RlEUAO8`N?(Wi^EE@ZLhUCtBg|P?%|QHm>MCZ<44*9%U`+%;^G6B&V>`@;_E8 zlar|a{71qnQr6ftp?)&Yt`Cs9a|GNzz1M7Bm7mUIS2p*})t02`^BFBDOf9s9#+R&E z;5$?0Li`TcX~=!$G3w1TBTs~xu6JDST35e{JUmqz)`B{8cR|Jh5>S@3=q(FlcNB<$NGQZwKbL0$5_8V-vt0Z6S*QxIJTOMi&wAs{tP>)J+yRX zPZDH{T@OglIY&u{{&L%2`82-YSk%KWZs1IGNDlU2Cs}QZ9`87D6s{+9nJ8v4!Ii=} zRV)c&yXyna{Prq`_z(d|%TxkwN{Y{)A~*t>SXSe33l@FYR<*46Xp$M+v=?ZQIWIFosy@dM*# zgSBO_Ixb$-HWt@|shW!R1s|dwX2oRYD$C)7%CfYD3dHqk-}Vg>OiYYpaW;>^t8vV> zraphp-GX_IoXmH}Bjd66b5XsnSIlhyXF9dS9}?vUa|Yep=e~6%820|)_}lwQUXvr2 z3YeV%Cf;re@wn9di>2G~f7zPQ3`i5HpS=Jew|#>AxnSNVR;Sp=a3;N6VYL)(W$bCZ zzDXLAIaX{XIBPpzGjA^rcNFqfys#n{f%*D=9(8T9Mwc zUdd~FhNgr1*JEy|<=4Ph3C~5@#TpZOtMI1MQ2ma+F{H-V&&98HZ=W-*XH2KE)h!}E ziX!LX`=@Ovk1`3-SC?1tjQ+K0iGsyh6X>zi`FoDf^c4R=kNHE;&(cgcZfzs?Yv(1- z1iiT|x&|65hoeXvo|fOVnVAQ;YRSU~aIaIDfI^|uvKp*su?d>4F^mH#E^V$ZX}NMb z>JrHa^~RIqNE43hi)bW(})_&%(kFf|S ziG@%9#cJ{WfKDSAjn5~buWk6=_a%>ZGF{nj4qM4oRrTj+tQAoG1Ffk}@97GkD^sD+ zT^Picf5Z?EsZH&9W<8L}9DRzd%Kfa}ehFP{9ue zb;R7D%XgJ$(p}GrC$)9;q_c?^NHMCMKlfxA;t3B2Yx{~haL=0MD@D&8a_2x8{b(H&cx@>5g7f(h{3DUlH z2{?f>reYOjkr`fx+4jZ8%+OlvJktO)$JX;)`|5o*+wzLdP?tQj)1JL{O4PZ~@EF%=1G>e*C1fgVcdBO^tYr(cxKEC4?5=8mZHH-A=LA`mMf_ZQX~SG)aMLPK zK^_ZsbFYW?Z#SrDYrnIICr`d_W9}O|dmi7>w$%(WC;HK>|2CL@r?e*Py47sSL-9l9 zavKE(wyICYq`o?5rIO;DOTgr^mfaa*187ZLWB#T~b|^Fv0qYL zb#zCeZ*7BW&AllXTXzw?yaFrpHjS^+zRyo-mYH1%TXrFP)Lt6C)xFvC}lriI62ax79GkLITg6p(OF7D5W(RN~Kty+%kd`=o3eW5ezfR-a zC@yF?wX|EC)EfCJpg)PpdP6I%nwANjZBBR_t}yTRg(soUO}{KVfJ}thVF-o@P-%4z zAQ~2T@yI?{XjYNA^@mII*OK|dY&E;202uy@=8dyD6tBX)7}qHs+Zpb7LRslgoy0h4VqT^p9iQnYkqNU zhObh*4o|rqct^6`4Oyus95_>!+i-LZLkDKn?7d}JT+OmJJPa;DgS%_c z;5HCQZ~{StLju9w-3b!hf+m6BPH+zr+}+(>2IpJk*^lgf&biL}{XeVC~ikOI{5oX>gx;w6$UOSm?SY+4J@R@k2BlAGC;afh1T8u#qB5tOv}Q6;-a^v|A%ZBNgdaX4(Qri`I3iOg)Wu8V9ON!qC#roKu*zRCXs~ zQdfky-xxtsboHV98NlmC*5%dpGR(ADojq9u4()`9-P|5mRchyVw)8ZQRat9BD9f+_ z8E`pFp_`Su54l>!_vW34BCpcyB5uD7Mo8-D*d0WF)QPqLa`Q_^vBggODHkKRGW4X4 zCJhlD2c`{JYL6c5t*HAZ487#A#4*(-i>8WSXz27{Dm<>-yQoYFrEY$inACWRnCMJ6 zYKBmLM9kNi60{6TU)TD)&WYTT(^BTs1P?w5A8?CWIlBS>?z!q`=SvDg;iP;XKZY@o z(5F626SDYyS2FB_T`0wuC=ggZuX#A04(TN*Ve!~X%I#Z_fg>q`gUvG#KJ0Rg{Saaq zycN1d=<{D?@Aq~$iUhNqVw;Rd_eWY2T)&@VUfJ){t;!Tyj_&XX4lrv|s1%O&U(R+0 zRLK*}$$NSF_D!^uIu~fwkbCQ9`)VG3qz3itT{eF79BZ(p{h?9!W+_I0^1yTC%cKTZ z^utWj0NvokuE!I4PQKo(DrlY#NT%?HR2B$655_E-D>A_#ZCZ~z#@RlDdic}d@&vE{ zA(s?|DmKLXHn^i8Tym&nX9bn@i>0_X!Ai*N+ev;r$gPsbX|ODY`mVlV7>}jKa7~JR zLWQ^t4Jt)k#zXYQXjdOk0$WA{Hw{CL$JsPhMGNZtxr5;-d`8RpD%l}?u~4Tc2W}Si z)UF(*T@pmZk~1ceUCf3stpbSM!WZJYhDeaZf?v-iBaBGWvy|sI~FZK)`=}IHV~gQ6M-kD-&Zn1~r-`s058i z>N^n~Jm51AD~-a%5U>T-N+o=e0P3Bj@@7}F$?7KD9bJy%7kLyigG%t?tK%ciwht<{XDd@b% zV>JNz`nKo#K8sU2)-O}f8NflKd25tceLG$TYd(0bVzf+G zn)Bk7Z=jf^A%5@U1nC8TB68!&=dDP}&zFjeduk{pDs*&ks^_BCjdG-=ZU$0R*`2%byPYK+<9mI3 z8@xPiLkDLYC>!QI7K&UzdurCAk)!BnqS?^p%Jsen6Z;hTDlD;>r`34h9$hxyXeabr z+*{APOhY1;-lc`Z4Pu#Kxa=R`p&V=eOs6*;>S|sau6;LTI+4a6pVuU>#+dRX-Fz1~kuZG8`d!klQ00{}T zx~O6QDJ@DraZb+H+PFko9jD}dS`goj*B!(7MuQCRQzB?B82(f6QUj1s(##)B<{1%! zy*k;5Gk+d((sUPm_W2=3jEQ~Gws-uy4j;ng+sY$&7h?LWJC+Xq*IY*y4-I=5I243~ zBI(J%Nz4gdV~fpqEZ4QH%}}GZO6WRsqcf&e^1qbd2ksUN(m;G>Epl~YlD71qd-J)g z-`BIAd!**0Iy|$q70g!ep;vS|Y);bY&vE(8d6(l3pPB^v1#-sdKheq zKR+SD;HN%jPMm(ef(V#ah22#(@;tJ*_`d<&SZF*pY81ZcF+=mNU#c7Tg<-t@NUwaV;@b7KVMLCRmciF9dlp zn~|tyZ>-M}S&|3iUZ4@sv{Q?pVEdGgU`>u@I=SX)-HE*!xc6nh_!XPBF8zba4ngO~ z4c0Eauc@u|Nd~$=im(=*uengpFMZ%7Lq%ua1c@Y{=G+fntq@{xx1)L@XY48;Io%T0 zn-9S#q@-FCoVLn)SDo8FyN;wbt%!UBuw_VXPv>+D;_0vvtVfN#E#{VWvdTW$coO6S zNoM==#%EM-ok@%QYf|YZ!iDj5m(yYHmx|00>>Ha46L6To>q<(N6KyZZr^MNA!~q+)9)qaZXovXQF&`t_o` z#`VL92bn>!H~m})iNTkVDte-W2h#AfWTF|gS`$~H=6hrAsUig_s`~Y>Ge#ZC-yD)k zlnIRY!oyRDPOd|}ST4`#sjd1werS;#4@~p?tt&@#J|Y{Z`$5FJq%35vdETe`t!sEb zrMbw*Zc*RE51wN8;)E5w4EG#md5Xewbe7`bYWG=$XD(dFcDjSIzFAo*ugg@)bc1i7 z?ZaZ9wTu-E(!RKy)ER1q>yDCZc@He5ygV8V5}sOVrC-PfuII99H;`EM^US z?Nn#rXML;U3g&n+5Bh~7Jx`MHyjM2;!$84Cib2pUvNOe|RAuZ!=}K%Ah*rPR`W zs3mx5S>b}fS)@wKkr3C#_LsS>lkmI)L`tU-LH-GaVn>+%w56aEqD?dRf)cwTqQmzh zqF0PA@@8dEKg_vFWqL-Iwc2?VGipp7BTSA|{tSV{^1Se`HPd;|cQAFO$Idp>cy%2F7p z2g_q$*=%m}2BVWAix3|GB@9C3{^{VoAT22v=YU!Z6cSuBE>Y#rbTzpga-7^ zO?i4)@b1Gjb~$kdGt^{Qvt_sLb^#-efpf0u%AmGF=5M79LG*2v|R%hjoe ze)b3#Yr5k&v3_<<&5#q;F22+9G7-Nk#dGWQl?=gZCi?BG#gVijYu(B<=h2c6r)>nI zho$TL1@(hp)H+%PIPgLg%l9YXS(Pxtp7VrO4(c5Ht13p^HC$T~Te)IgDD7Su{bBYDyEx{?=x485<9Wg9+|Ev@&ozF zCLHi!H947;Gy9_Xh}%`qj}0MAXalxHm=x^j2P%IpkX@MZF0TdmrZcL`VZ(#`lCO-V zpF9zdu~VjNPNM(p#S?^#D$k@Npk9B!?1Eba9Q9_ZwChFTj<8GqJeo!;b&~CM#9MlJ zCeb@__Sr#hc+~oZC08c}DH;io-2$;nv&EHO(4r{u1GkBP3qtyi%rh6Ky%%IXL9)wm z6wX)M6{6_N%L|9EKy4gWcsnV=uh+K^#3*}pO6C>NVUTc;>o~{LIWLLiLE>Ui$E!wi z%vjsV=O65jJUm|f*^`)};4S}E=+lQZRO8Z+j*SB1wUBtaAkZ1Gs}B5NaEHP$wghsB z0D)F!vu~AQs(*(j4={UJHFE0Is-~NCW3@nS;*j21^DpTD58FYZ1{4tJ$A{P{GaOUr z!}Y#kY!@r%gCR2HAoOnT4U}V!?oXQv7}t028*IBdY>2I|(d~})bIbg!> zqFL}>i_&c8cY_X4F(06U@f8^tkHrb>jB^dd%h%cjTJe+ zzruqY1^WGIVFVLB4Bn;-;2&!6l-_p&Q_~|_8Nl-sD{y!n$MR0g=rcP{6`xmSnTnna z2Zt*@+730v5A^r$&me7WKKPT(Xic2DF8J zG;5oU;V^P@wSFnu9mnC{An-oD;rPNtITxbq>UoRWdi}Fe2}b;0qk4CEyG09~vn_gVslhQI@~+{um6ua(_GWp{m-6Ok2Pj`-`BRjO zI1fU!;6)7j%WT;UWKL%pHeZa>%`ptYmE3pzc1*fqr|&|FICD*KVgn&HI|B*{_t!bO zuW1C1t@)g+A5=&lHmlGb6DeKo-Y4SE2*0oBD^N$ve7M+)HSqi#6h|y%mavsCL&#kk z*!`-H`10a@FU^2*-NGUva*>E?n;S2D(Oj9=mDA`TNBR31t+Gr;bwjQF4p*bDO691@ zhDzpJXEcgbJ`pIwdi8Dn8#Ce2R|rryH6wDJPb}NqF8Kz0U#+<+io_G&jq*t}3QTa# z>UuXA0vlcqsaA|wU!s)>%{jVufKGmb&u{DOiaajPu{E!5+%%z3wc*wVp_3)ck&P>} z*3n$}__47|HE!m4^1joX&fSVNkqLxnR+EK6LdpjdQS*vCUc>Fx`FaHn-B5QOuY;}$ z%X+wTXDd}fnjkeP*c!3FmX~Nyrx3vnMZ&u5ox-{Q9?TXw^+YC@)7SY;56_ynh&Zp1 z?zZwH7gy(Kc@hTZtM!P>^a3^FLmFLd%fva~t8_&P+*(kiZ;I`c^SWMhj2^afY<-KH zB*+wC;knDCJ10AR6VGTg>$xkhF7tp-^h$_zWQt)##3Z|l=?hnpM}#ZEYW1kjY@#O;{UBm7pRi1r zwPvOEScynKhy7Rn}H1_WeJ;OZpSu+S<@a5 zN`gq09<=ZWHFI{Q^QqXE{_{XV zm&2!lrD$oTz%)3~FF3dYB%VsE9qUNFvibQno*G|igDqV<$%zrn8gwDks-8%NUeGV+ zZKh?X30RG`M!j_EQ6fmA_{XBc%&8jpsNdqX^wb<8Oxz8 zpoL0Nv;1Q}WNkn|n>aNcwaGzq_M3Hh-SW%XPH!~zNUp_CMKRO}m~;D@*67PD9($Js z9_OF1ited3L0?Kjnn+z-I3reojSIMUhF~Uh<5AbpWzD~?j=bdCJhJ)tQ|o124F7^1ogpPtHwg-9?dPE z9aB>4VUxnE9KVEf8Am?*fJkA3ik0zh^=WG$%z!yLQgv9ZjpM-0k)VBeBC60P$1+{f za#NvLn3ydb4H90y!pk*0D{Rp+cnUb#bL{!lN&Ew0iF&twgn(I6NR(d9C;OZ%JfB-& zRC+H?%f~7EPpLs2n+utCY%oY*lJr+KTNN<=MGW{O6kRWs0Y^uZ?=!ZCIpUszy8q<> zgrqDW%O-=?S?5Q}8S)tkUq_my$H}Zu-*GLgC=_;4zQ!avV2|&KA;Q1ROBNVx!+|zg zT5x|U==0GbCq+N}S=Hes+tEy6Bp4G17#&*#yw(~Vyql?e0Yf4(emfS8casN=Ff3CL zIU=o>MH+rL`^3yyQyK|8uGvGEdXHNWfuL<19dMN9W9nLk$` zAb5r84rkHK1C)WA>9m+a6kIJQ#Em}3v+BI*ZWyhA-~=a0KSA!_^D({PXWS( z>b%dp_>o4P=g^=cu)404>F9UWYM3jUw59_=o7crnUjxn@YRcW9{?-wKRjc$0Nhg{W z?_$0NvPB8spogL(MWn#()!cuU%~nhd+WEv1%V_eTfe)X|WfBKpp^!Fu5OyZ8GG6Ov zFZd=1ZTo$C9keHo@f+9dX``b*)Nm8QZnH?;JweQvuUa)Mv52Sm?x1O(DDR@ZF^1WM zXVuK`tlfDnsDLfLX`U#s=uHut1Y+Vz-uQOX z0_v`f7jT4)qT zzusxi7CY~LL7Ye~keP_GZWV1D!Ib=SuDIVq2iXy3bMy1ru8c#e>0IP{{6GkU*4i&ZOJ3P=PA>kiEvJ{$NMB-15fi%HWtOIh&QtT%}HN2|RGN` z9;0S_l+TdUL|M@tN$)R+6X)YNzY*Zu#B+Uyh)y=*wRgP4y4EUrZ23^nlw`7)3!$!i zE#&}{+}%d-E%QR~VvA?4h%#F~_9gf_bn!}xb?G&$)&)z9j?SK;T!qo(f<9?3q7j(n zLE=iVlu&L9u`Pl_nWzZ1>)WFLeP4PbV&$LWsZNEo6XcDWd$}=h+O{gx} zI-DG8IvLP$ms7jue8ETFo^|ex&3w)K zdIMjN@KbihABtx;hHHF7^NC!Y`Cm%BrC`JHa#7*-ns z3E1boWC>U61!78!e?&!d#4bK@nzJP3L-0yZLr%LlFxvPvbDpxAii5MON?t#UmdQS3 zmlJ>j;aP~NXnN4m6CjxEOeR0Ldou`4-0g`&`DnYsN^CkpsEpJE4{ zqmd_8eQx`4&T-lj&1^hA8B5NB%&^X4q5KVE=Uv30!==Kj{96ZSTx)DRSfs9kT@J#c zX zd!|+PQm;gw69$Ps%t@Z1;^?%4iOPc}rD~5I+<}{@ljYURru?v);_&r>?MimEIPO8K#497j$&*-xPt`OolY)=ob#i(zKxW=*+#^@d3#2Hl%st9EKnAhHkvT4D9~I{Q{k5GqXzsvV55 z)iuzmnbg2fp8sYWLq+R&%zXE%@42U@b8Y0}*i_2QLns|+nU4jmqVE0`Pun&fquso5 zr93QKXOyV5L*l?btq2rH03DmBvx@ywD!|uiNAP`04N7CyCP>xCB~~-uSK&aKy>fIU zQgf*-vT6Hqy!3^--1d~`%6ZnRJBh=;_;T=lp*y?Osf-3kP%C8snnwFm)dHC2&6E5= zMY7@o!+HEZGjWYH6{WIn>(AGnugaH3$=4%}=kLf?G|>z8TZot!8YEZ;{h#Cb+EDgs zXF}l$Fk+c}u?ovdXV?-^1azTr{=DXt2*U#f;p#W+nk5I2z8^0%WQx}|KMy5Xy3Jpm zuA|hlxfFlX(KDPQE^IHn(H14-&kT&Kr7F(Wu`4UzJpL)JQx#Gth9j4$FUhF(qYlT9 zV%0vKR@S&Xkv<1-&UEHomM{|oE@P@93huAn=}nu-kN4z- z7DJ*_)q2l@D;0y`tz8|5QFf53ZMtdX%eMl?r;FsT9=u~0g5@>(zFQ362h1t<;@?S` ze{}d#l?}O{qx7`VV$IXVSxu)?oz-Cb(}4xX7;=mJ8Wbv8aSBwIdg;;k3p802KgPq` zidj$VTm&hF3?$qcH5)?Iu{NS1e#kBP#-HnqJWOW7n&`N(J*ddG!XPy_zN!V}o)@tV zBKPnM^m;U!A%6xpm$Wf$P$0*)X`ZYzJ@zsG(5a}FY3xjpLlxX$>?*ySy1JIV2uFhS zXqQt2RMoS3i?{Z&&2Qtfsb@$LWE%M8eED4*|(Iv$_md$GNEHJBim%-Q2Udeh0g#d!SBHax~6 zATpG|GqUFxt78A``Nyy_2hwYx_A8XJ;gi)JT~D=Wag>&Io$___5MQ9)laxPI_b(f> z00`r1Z@~I@Goo6qqHNnid3!%SM`X5o4e|nj~ zwP7L%K69#(*MjgLJ@{W=|NAdAA;7(G2CHKKqdJu$;&k{#>*b#+{?{x2%LOtOaIe^u3+DeWSvxBX3Z%^4dW!RZ zA7Cj=;9lgmPXGT#l>g{w3a7_)YTo^4!vDhn`#mZ?CJ&j!|1MdXETE2i3&d67|Cl-d z@$Nsr{-5eT4*&nD?xSM=|5M#8CYY_EW$MRFXbj~Gw^|Bo*6vh(qrHV>hG#cB^?GYV z%iok&R@&@LyHUTzZCL$l>V(LH0mvJLTQHH_DeST+A44bqdCKAgliB#yn4V1EtkcmD z*46cmo_sv7j(6=^Z|rGdJ_a1P-02QavvSW_I+3Sn&Tci{Za5HrAYXw@4*XGCAMjXC zW)FAe<1cPpf!s;C>#jm~8gso!g*wf~wgx`31Wn5(2UR+sp!fLT4&GhiMt9l^tEMlN z(+{hI1#8X1H`!8OJnYWnX;(;oqW$iPez=C~>*daTL=3V7sOXhamF1%4wM4a-&_urOQ{ zM)xXWubum=MA0lAs5({p&ZEyu*_aVa^u)cpiu6IchzUb#HHR$11BDFK`QYOIn1-J9GNG_G`bG z4i$EBp7uqOi&91kO-p|w6DDIB&33e8bUvK+TpR>QO=zUTvtu6z$wd4C%pMh(!Il7U zDl}eVnBfYMF9HI3kgYD5)5p4U-5_reTD(2lR^;4TpX}e5>lSt_rc9iV&nkJ$;43!2 zrFvh?z)Fo+$-ZeeaZF3S^ObF?R&}37Sd{sZKj9mIZR3^m_0!r8)aCsAXM`${`vaIY zRdg;N<sbjW=?ES%T-Z#wTxh;WOK52I?LcatcRm8&rrDN~EYqeWh3NXFYsP$LAxsSuKq55 znBe1Wwm_03DdIffd`Tg*1dpR*j2N|>Yz$$W3Vqz=M6>ybr>@Z`YwkK5;aNcz?4)@!NHTrjJjpJ_ooSItz5#RZXu~EPzuGk<`-n{TTZbV+_H3x zg>>-^a7r9g z`T|ZX%XG%>y5K+Bi8RLt+>i=6?bWo*kujBI0}%hcRGxnf7s`$f6%Dss3~uFar)U#v)ho4d}7aLxWmB<(?%g+tuMM?h`yg# zJ*Fpe!IITxW-pM_v=dx*6P2t2#_VJ+eJ+zBk_Lq5u5Lkn1tIt_B9XSX{KG%r5oGWy1l8A6;z3D`HFMY zks>ZBdDdM6!YASn(i?ZDPtw^BrZ>FL8ui<Y&UY9o>0095!HaX9-|(y^4ZWYup&J zK1_<>yh}vCUmn=}h~gY_ddsWhTzgyV6w9Jl`;wJK8IoRT*L=+JaE{U3gBRfomvN5U zvq1GDUj);E)VJrD_U688u@}M7)=#>|yqG2LV4W=b_R20eHHG*O7*{6Sf&w`b1AL|% z{TB}lmnCE70xcH2c{y}D@dt^KB3d+u9!C-HhCASY>Uu2f`O}Kejvh!6k9)S))LvT+ zTXO<2(2C{<)QE_ijEsarPF86SY4Uwu&JpXvii2oaEvajV?LOCpT&=M@w*l|Ud3uHP zxUf*XF`h+wX92ISee31*>`=llyEh%p(w%68YYT$KIF+Oap-Hz`N3(k6Kd!d9ShS84 zmbp1T>7TFgpK{~EsM}h*{&#VnB6boI=f>+ zM?K$|dm9~KB~vM8(wU3-<1vG0qp&)CD_NM;_XL)xO3LPvIg$P!a|@nw~B;O7Od~?-j1rGwCr=9z^2tJXs&) zyh$5e&yb&m4;$dzssJU9N=?A0-l5@5@?{ZhQgH8HmZ$RBisuX4mdtrip3_-R%w`Zf zX9c2c#FOo3@VE{f?){wkeu!W6ytq7H$SJY2Rfe8?_iFN?LK45`eH^i)(5SL#tcz~4 zVc6rk2L0LH9tjE(+Ieyj&Gud}A=N_bfVNoTGHKzdWO<$$rnLj7dD) zVVg_oexY<61OZAni1eDQ&h1XV)ZI#mqMjB-=1o4KAt0~Z5$Fk5HHfmAeJJL!QtE8%a_*? zjuVzfAcL6l)3DY4ta{UsRmDjj$E-bJ-p?dAT3b`39v3n z8m+ldgWB?W=M$FX(eLsFYP~tvxR{r84RgAzO{Mzn2!S|07PnYq{>W@JyLG_mr46Rk z!yv^81JQ13HG`3QfLsG#aNiJv>toz>l^5=9*`$u;YA<8mokAbH*+l&+Qu+U1oQLKE-o?3u~Yq5#(Xfprr9bpG)aQI7!SH)BlH}`>^6!oeG>(z z;Z?oT{R2*HGq`?O3(||fJhL>W!!Fg>-JjHb4!-)P=)`eQv@ddGhXT1c4e&4#C11?G zRFD+k4ZNZ^WYyj@NkFe+leVD<247DnXr|6GvE*=BDud8O6p^EC z2%+TZGlZy&L%R~Up*8 zGf(j`-Ui3S1Cuj4;3J~O5s)iaGlGZ3X~O4zz4BHE-mN!6UFz=bx+AWwTk8&wTmmVd zgGTw5+4CB$7r{>oaygEDRO2=FH^@FDswr)~)d{?M7mpY&PEvdzOUUS8DH zcb#>Dy^L$VZUCQ|jUi3fLTeRA1skdUi|?=T_&FiyOU^3kp`6l^37hC~Z38I#VP#SSXDa zPH3HXiH?*_7JN^|^}#f;8z=WpG5TEa1AP5y%e?jL=EXz&unBJ?MM)PZ3 zC-Up8q`h>3UFzw4yxFb#)*fnQfc?vVlWjxZU=P(bdLK<|Pv=W8UfwHpdR3>RPJ6LG z@he0rK4Zt~y6(sX-GJd$Rblx<3+0cf!J-7pZBJoD;mHYI+!b6v|c8pFaB zgE&6gE-g`>rhV*q|H4g}9-yJ{gcO1*wO3((@R5?|0$IV0ij&US48lZPh7a^v`;Bdz zD66n6VE4nH*6(+S$&;_K-+;;&r)}O(J!B_?nc+R~w1l<#3%`HlKu1ef7jxWe~kfg*B^mloyGSXOGMr+z43B(asvR=*yV*g-mcJx8!{t#YK~^j?P$RW27k#&sjXFg)-h|nMSdqAP(X)e zX;pg?9J0}l>{u+QK?2}op|paT&PoiPgi7*OEMAH0Cf^nxw?796Gxlf?Q7yF;QA}_Y zJ{?Q9I*zLs=g?*ZqbC&2V*4iore_ojFT-I1VA$fc(0)O4y`AJ@^f_S-QQN6e9soKpg;0slL z*r1~ea1``o`}s9cu_LgB29b_~0%_d+#K8HqM*eqb2Gbvo3K@c=_VLiMhRt%e(#;Pp z6T<`}9LJq?#+rsb@P^dEIe*z}2jHiM-{cYPoa_TRI^rG$Sw9TpC+wu=f*i`Sg3yQz zx3S+O9(_B~*u+vN{ZMk>4|lZf(I=xOE@#~X0vw(iHW+0L@TMzg81IHh7CR#O@Z~{% z<7kGPe6bn!bs4-!pD@kydHJ|0Dq=Tm>R}TxY<5AiA86Q`m!8#!$LKl!AV+W`kZU7d zKA+S!YnL^dO+7TQjli8W<<1?(VfyNIc11-E_eF0)?|xala(ho^9~Cb`NQDUsljIo>R{6Q(nv)x4%I?fu;0Aka`xn`4+T+T8<0*Po8uLqOdYJ?&VX=}Ir@*u#bO z&UY)YlPg_2xi|?G-Q&EX@SUQ^1UQ4{gC|UdEs}X@9u=f(mo+{WRx>QfqRr6^#0Snp z^La0|k#_yi3p5OHeb3G@db4k~HQYmz{l*V;ai}Nak-2viLKUgrnAm44{3t%19#RB_=eyW=}@!{pM=6jrHvQ zBRR~kdA7iM{v%1zskB1SS~&9hdU4yJy-$;o_+`y(`~DkSf#nrsWE@J3U*pbG4qU^i zFi34oEY&i)+BBa)0jfC~oODJ#(8ICx1U`j=#chaIM|{9 z2H~JG zouy**?rWqV_j>6W>GmGg!Xb&a9aXu{{G*;k53LBeP8+Afza!j`JuD=85J0I_`0VU9 z{hXknzN}}~sh9&9-2jpCEkN!9t!?sRp9kGUI6CJWz`QF`#~Nx-N_%Bf(>JWTH&Mi> zW-nHG@|hr6WYfsVG=}<>nya;&o;}M0O{!0QGaPgD2Tz0=B$gZwF$w|k*N}(qWY%X& zDom+(z%iEtP=HWdT0S_)KR=j)>(irVWj^M*y7xBii+7)9sCoSE+Ej)@sw-Z!zZ)Aepf zo==MXGEi9@(nE?XT%SUBB@ILIIkC{Eu)i2Vng#M98LL4*xVr>avBZ8kN6O1l-t(Kl zT?z$#inpbnlS>5;fpPf|(>~E#0QDV-g;bP`K+AkSShrKKKk8J zXmE3jc>r*nDx<)A^DK#*ee2V-8w#XQoJHe!b`nY?2p}tcq0-lZ-ZiRL8c-}#7XjTU zot7lDOBWREBbrB(*pBuqkvE5s#sGp|(a`iW$K1pB+!$h@j^w-}JaE|^SaK>R{5h#r zFjv)l=#A6f9Q6QP#M`VtYWJ@)WjHBP@4q2iWyB!sDCf7 zTnnXvI6U%P(jQ3HE&WY0LD~MAxdYhiTGs0Z?MM1HwbuQ-sb6LrZG=51S{VjALs;Aj5RzgQ-8^$! zuqo66`8`<0N$$O>>1XmZ<@R3ibKE!cFZcE(& zHP(?E<|7$Z!1@z1}?u6=x&g z8u!)SZnj-uXKBEa|0U-L2yng%feh?9B*vM9MTK&c4j$mh^Vk+vpAj+k5q}EO3 zVuIx+C*)x^hUuNRd;dvsgDi4vf9e23$Q(4#-V`_u5JdQ zV67>B9iMmNamw&pBh$2@Ma@)LJP`$G=mZh*xDKRpy+Oij7qW}Qa%QUW^ zh~jGCG&f?R9;F1VIdH6*7`E%8>QUo_D0+7*X0$52UM_SgW=9gphhjp@oW|{xbb9F| zKqvMJC7Vwy9Z(@QdE;L;3#Q)3+*B9Oh-}JZF5V= zy=>`fzo6bqBD8AL{{g3c)|{=az^LQ4df(c|7hcheVzl$r$G|DG>q~gP78|fR3z05L{WA z@#;c35F~lS*iJn+2>8_b>qE#_XZv4_-EYlL;jP4?k8ab22$5LfUg!cqyiTjEOThaT z1IV}MD*jR^y%Dqqb)T{p%`pweDz`gd+z|T-L%~}M2Grs(2fo^ITiF5 zL0U0w3j=mz6)tUg<=4Sw*lsv53ZQlKJxqWgiG&`oV)MNmI>(?hJ7#NqER0Me_!tMB zsdX=D7_yGh3=I9qn!-*94~4yq6vM>UcCXw$4kZ*am_@kn*r|(NU5HuV5kaI4+m(@r zOAKl@x~vZZ_V`u;Hc@t56UHSeMa#Z-gI7YbCg4)K}Rw z`|GBeX-GG4KbR*!UxhVd^sUl20MqfAt3M;IBs&)5@LNr$`j>*hhz~MfYxj>P`D5Zm zQ8+whiBf;eC=kptY8PNrfv)&-CQpNSfJO-U+oW)x_AB<8zz*s$I_es}MQsF5Ai-l?=!nxUb zgF3jKRm94|2%Q;$hKd|F%Y7+rDj44D5MA8;CqQ0^e^1}MY8mE9ULsIoV4z2>HN_jO0-(A>a zs-xRt@2|l0siyL|wt3J46@RxLi5Yr|>?J`!&SU~Pb^j2pO~0jWGZEM1x^!#T>4WYY zOcdDeztm@*2TyQ;ntydBy=PeuTgnT(2`HyTsA}?Sqc0bDeHru&w|u>jXDI=n`dAs{hov67_Gn%L!5p4VL;lq4%QEeI8J9WO_Qeeo}Ve3}8G z%i=AlF9tZX?06vLT0;OAAK#I-TXT5DmSH!RhH4DY1<_{B3(>K{h~CkXtmh2sr#~YN zFi?`dN42}zVe7C|y0ybTO}(g4s)00RV%U~UP&i@GwN||Arh#Yug)`!P#uOm|+?i$%4JrFR(zm%YYj1vV=J!61)ddi!YW&siFEm(nBKh@Y@uO9I4OrV=XgZ5FF zBGQiYd9}JV7qb=4%~*yNI~6w8@5s-mY-E!az^xy|21 znuDKJd?>j@BUA5f@`FE-U?wtY&Nw{bL*)2fgK3xqYBDld|HOfWC*Vfg?CDuzM7`en z!wDb?$zy4diY1v)`PxfUs3Jo)v7ZnA;kN!4e#9T)e;S*6YzT>@%QQpg%4p$7;ekce z_4sUpM=DQWk>QKK*scF9+rcm%lC!Y7-j$)=;}4yuNz)qtG;Un3llMpzQE{kn&HL9o z{)cyeR0LV2&q4{9Z8o(lPGId4nGvj4GNQR-AP&I$zj7HO|Mr3Yg9P&LU!{})_DFW_ zY)9w6F}40%3d{dJ_A%7{e|`JA6FAsFMLP97*Q%v(zul+5GzOAS_4=#C$EUW9B!gKY zv+XbcO|c;5*iyyEXz-cu8jFPfgER#(#FQ$A8J(^Iwmh1~UOWCb?YFtd0)8mhhYHzT zELz9?e_#ZS0Fi(2UPxJ4yhW~)M0%Edla}YR3VQfI#s4Wn-shIuz1i#i$@E^HmF1(^0hK$7?5|-t zptw*g9=1P>^&hmO7IK))%>Jtdvi*1yDD7l{z5b?^VwPw3xQIKMiPoH}?u`Xu(?7d4 zO9VXNRdm`&yZrcr^MZ=Q9jjMap3?KI&UXVT`9V*Dx(VU3oT>ayaFY7)leE|4-=xg|N)hXBmUzhF zQV$1lu`Zeiab44SzdRDr;z%O@NwMQALE=M9p;t_Q4lCC%^WfyoqF0IgHr4Wn>t@}& z+MX|H?cN%iw###J`_HcGR&2S@IsQCyyXD`BqV6jH&mSIB=_6XW2-~hm96cPbcKIK25J-v>ZWc^SZHN+-ej)WjOswkfp6k=U z*#qJD_XD(%9f%@dfjH~SA72f-B&Y0se`&$EIm1;utP^P9$nSY9YR9dhkEUeH3)aeCj7+ zQr(GcdU92x(+r!Sr(efg{SmYETm1&8I3B!?S2{W^({OzYaib#OpPu5kApCQFrQ-U= zjs2FziW@^D_)(S@mqOhCkfo)T-p4oRN0x7tawf#releHfSqo?kJ!x6t$v&$}?(4X3 zhUQS{qBmQu-FgfOza4=rMa@EQ6s11}9;Qx9$3&x{`QH$Mf)Obz%Zu-3QTiCJbYP7F zSaA3mPD%c$=p525jWLHUzOanqQf08Ok%4u#rTt3hII> zkH;I%w9et#FH5D#uaN!s<&ybee{AKC%mT%_LtXs1bjrms$t4Y% z9xZ{WEKLLt0FkZ28EDW0AUqly3EjH8AD94GZ}su=g#;#W?;4D1z}lw;(LPi%{P2Qx zsW?h!sBjsOovm^R#1D-FfM^SLJ=HwksK>JUr(=^9y5}4PK*OM|zHv!MzDX>-P-gB* zMx$n6l%gkFd=+KGr@<yD#0Z99h@Q+)I!Izr8Tg zo-`XiQ@@t&-+UL+SInt}f7#5ZTHORtb2TM*j17SjyU^S@fFjIR>xnn)F4O`rfha;( z?=HImf4h4wp#_G`K3O(?96#qtfkjg=x#^Y&Kojh4~qU&g8i}3qkXrZ&f+@8 zJ14hDG?q}L#v%c!aoDVR)-pUxcWrKY!D=WR_XP^~rw)m2CfVe zduWn=bDQf#_X9uJ)jdnVh_yA+68NJKwG@`&(9c*)9BY`J-UqW4y~~78KVK);;Opv7 zwBiD)QDVk6vuDS$`0P&>ZT+IzBg@1j70XShU7Q034^`JgltxDj+y?jI&j4OWCi=4< zNd&=!!V5~_B+~S?Ytn=HEDOTh8%^lB4VTH^I&GyzSfaps7~%ymb+L1n+aBwn2?>h3dkX4ssYSWvXa~3F7(H&= zK7TjTc)rAnpgkemjSZy4mNUcIn`_ENfs&tXbJQ8-fLy}I{G}#R@$dF(?P&(rUiaC< zc8}SySHyDFe|0;oz4>Y^CI$$^ z8EJbH9lv+p;rtMIb}isjesW}W4q%sIgL_h3K0o&UlPT}80ua69P|w73#V zWWL6t$-))Sk-~{dwDKfS0@*4Ox;L0z3ltPgJG3Cb2RNI^TY_f>#=FziQ5Yq@iNP1$ z`$_O&?!qrhV2OMcSpaK`GJD7rZZh-yecgSHS#}re&yF-=V9*#mjuN7VE)^nB8zrVs zUV#2Hz5Gne6y`RdDn40cpZ?0tP?iX4Cvi&Hph5PVv|COH#+|wTGQhmXFWF4Jo;zBn z0}_Y#P(du{GtAsUFa2d(0EXd@E1Aupy4lR*kf#14F7B=x(`wS?EqladwhlCn16jPn z{Gq!d7sz2O=;mNy|gpUFf`` zp7ER^GM5Uix16!gk|ngtyH$i-$LFe19t$UqMi`M!1z+lbYkO2;@xJV2uYZ6EwH~2U zub)K)o6|S*D@*$B%wZRK>AP>zIJ-ofX7;hUuj%@;&}?;$6_~?2)9+tgA~4WKa({%% z*zE=Kr#ASCi8nZL=2=!ci6*Z^ksQ<}r2p*GLGH9U!cJ&Vh75nA#cyO?80q2Y2TY8P zI#F@wMs>dh$gmdiw-J*80mvIlQmM$cBZt#TKgtPE{NVHrvkFmHjb1l*e?t9+vE6Ca zc$rwf=j`N~jxI%%l6N+~IQcl2Dn``qQwG`<(5aa4s%XRyx*%olP(^ts0^%uvIYMUV zHCsfJWYEGf!76#p2u%u9=zS33px!0HMeJsx6#p#Ttcq^v?qxgR|yt zT3GbqC5;c5yOZVG;*%=^7Q;PfTN~Qlr0%`LD75ifgcN8LT5R3d{m_@Xi0EP+qXZN?witc46}|Krq1?!7&Pwj!UiLMm?gb zvn4UU`|y5?u-u!x9P@rTSd_(ZG6raB0VC7x2h9_oN3YKsJ%fIW+CVuir%n^9$V++N z3OCVYGZ!W&bNY@i;y|GC}mp9F?hM#G@j2mz7QAQp`EjMW_s`+3o@?im> z>Cdi_muf@*ImMzL^k_KDEqvSU(WX-))}hTJvRJa{d}v61ITvOP{YOs9Gk(%5)%LUT zfNPUbw)08}VYY56vJt0djBdu+l^GL3NYV?;n!{6%(q-~^_`E*%rNQo31xTTw&@^8i zGx$O+6n4z*oPVJ=NM9_2{-ck!Oa;ibeEuz9V|7xw`3}^^u-F*U9Fz2uL|)|;9Gov2 z6*~GnVw{-u{FU<|qgZEw+ODS?L0jdIk)UN~Aq}>v=y=eC-3oS_L)#n@qpZ1aHCYk- zBIEjj=Swjl=wM=HMb7LMB7Nq!OLHas0?Q9x4=#p^P~QMd>i+TA2vzU3B~EDbG*SWl}KnSL7tY`1yyVJPrcgi$Nlz$#3O(J%`)~H!y!p|yEvua*NHc`CBup=xJdJ}@s zzvvcIU!ut*bphcr4lg>J5r!l$V=d8Po(PlG!P|PyVWzVLk6K>r{_*0TS8P6DP7@#>Kjy3%6+FS~9%j>R^_Ga{=rV%g_+#G-yD(8aIXi)hJF3yjas zW0b6L@Hay{*AILl@zvo)sRM}%Ch-w)_{3$QgGbZDtlXHNgW#SMD!YiNg+OdBJoc#c znyC_#+Rnc|hT0{^?Y1X{OgviFSD8qgckh?hvDN0opLeBdq@EHPM4aCDKS91KY^&5M z(tvc=kg+Bb+2aO^f*1YtLhq;4Q8KDb#tEX}d?Zw!tJ|>cx|7@d?F$*RLc!4K?FrH~ zZC?WfEYRU`eMc!shyDCJ7olA zW-Za@{Z&-=WZRxAqNcS*5R%G`kzn$MgUoBi4Q_xrVH2`dM0j*TQo6{#wo!uPeL}yk zrgn3y=K^3tzl3Rkps#5!>yua7CKcaTAjR=IcQ2*P4On*@fRw|p?1EfNht%Jbx}W(q zGB-Ey)32FvK_U8n>CrAt8o}!nbKg?lGk_5=wtnq{gDb21eodk_23CO_%Do189~w3r z;4$1|dLJCqhSR8$MW~7Ecrir=K>#49ZO@d@IW{F5KlMD@js`8L1C&9yLD?rCfU68j0Aw=8dR`jFPmX1*Z zCvdFXwf`{j&ci~=qi|sY3e#c!SQ`kPa~>>bDGOTlg$pCF7e#bmj`o2rrpLc6Z~91C z#lycViN#bKml8WdxYV+WutNcJEZ-s*+N$1jj*Hrf2VDk>O5a#sNyuQ%UOyZ$$Wvd`S19&(wFjptQ-&f)?)rYLEv-|* z!G!_oQ#8=XO)~CekX>nDO!|bBf})=g%qoC>frn1PVH|G0SMV#=-r@^F!%#a<9txK; zg*OE8g7*NL@5_gF(xBuRdnJzU@dWPhv-Ku^~Vx~5R zBSvC6T$K`wv_hkVb@!_o+K9#C9!3@$Tf5Cc#ut$8dLOi5##_%fWn zApJ$Bo?0F?e60Qp4cDiWGV;yi>`fIy%a~!(zllAU^ll#Wg7Z26y4dNi@~M!n zU=GC(#=qz!=0nSU%l^n4!W-^xweOEs)L|3n0!ql>QrSf6o$RRsWkx;Y5n09RDOnsI zbMYx25b=C4Q#Gdx`nx}gev?8upN?2ulwY-O#1QXv0t$qkcC&7lY*U;sL>G+ExoaZ2 z7s2CH{h~A}aUtp|y+upkj71cq*KEqt3D!OS{8Vj+zxQltom&Yyw0Vn|Y55<0iDtaS zR|p#cE?>70Oq^ov6nymn;pKu=@&eV3Cmb1~642Mnn)+n3{;JIWmI@Az&W@P42 zS^f6O?_dnH40Uu)?8l#MtGn^q9~lz_Bg#G7kR|EHW_)}r1q6>fw6GrR*Xw$2`t*VGc_U=gc#QN?a!+(7ta(vAC_m$B@Rk^N zi++wsc;Mo)$FK9o0~JO!+XePdl)@z35Bs)#^4a+YfAlXyI1YkGpC2v;-kl9a3=EwI zN8?ndA3O+LQh>POh*J*A)wKpiTqZxH;iYUcGy2@G7G&|_TS4g(4iGa7gHA!C+uG%( zsn~y*rF>9i)gl?b)(z9&v^wHcU(qVFwE*ggy+J7GqAQ=FLl#ylni8LkI&f;^xiE&Z zV8O45i`Nw{*-riIGa9No18fz0noN}FtC_tP*5xj6a`(7k)-kHM)z$0ntZ-Pqa)g|a zd}cPP`|%-tr}h=fyY%AgT5zp-#`O1qXp3ZJ&PiQ9{{7SuU}iz+yyp~>fGT|lD|}7) z9N~!TZ3XAP@quEI(gP!3p}ugfOiyNf=AH?RCOMH0TDQrxrjn3KKwb>@v4`%ROpwVB zgNOi-l5#}X+Qe5+4YSUSnh_Pe{U0MCHh8JyH=?Dx7}Y!U>Rc_m=@v@lhhe8oZ>+Gl zVE+Sq4VEV3zF$>-z@NK!VWkdT4)&ZoCb{a@)C!IKBAW3jOIu5!8ZYFn;JvHF1P9sC z4NwY6{DAkFkT4f?zpUGuvOPNrOEwE|@mlwvn<5~{D_)XPJhLSE@>%*TZNCqp>rJWj z&L>5G)^)p(dg9SLZS*q7L5L?yS_0C(QlujEqSK$G{|vMj|DDzR}@e>;Ki z07fO&A#ceqFUi9+2jdT_*$N z9nxmgn6G12%1ejtdN+1OlKQ^j*%~!?p8K@1IA<{)NP7ah%tB|WgfB|D?oUHw-t~J( zY{wV2;Nnu~yA`+;O$VHqf=?>p+-PCv%>Zc^TI*gmC?+D*hiT{)-?(f3wt&GEp#cA% zihRN_Mo#* z<9P(vjsz!V?ch8HV>jtf+UfXK@(4m&$30yQ(hi97gWiFroh(jzdm5l;@95*S^h{Mt zIwDdEC`n)aP%;}jO|CKLoTm*+v>-l#8(4oWAk4*dMe$he7&EI3@iHPD;T348&m1zp zzpZo`K>gAK)GxndtAsFqiAQ~aHg&;kHC9GYfx^Lu0kBhuJ7BhKK29L(Z^zo1G1Kfd z=G2rZU7oxotVz-*tRcd-RUJpAkbZhmhrO)_b(pV1I&;Ms5R&<-#jDkPx(1Y!`5eyQ z46YMm28n=Ls%bM}QAq`eW`Jq(IskIe0mz(YylPm2tVmSCtRUwvII>3l0poxSE3p8bLt4DBt^`G0~x%!!1A0WGzn^@>9k5c#uQ-{O- zW=zMg-QTwHp;j40J`nWW7<5SI!{DklmDag8n}*{+tK}*CLiT3V{W74DKE7)UvUgrGw+gzIYtpH{p0C$V`UZt*h_g+7bQIo|X zf7>roKZP*WHG=h#i6}x~Gx@yP{%GqSTL`p1agFOvXl?;R%*)f`zPHDI0?y&U=vjSZ zC6a6`zc2blOdli3?ibUud^IXmsBTA2V4)LY{=G>h;W|ug#E9LLe(Q#JeK2Z)cECOO zc8?e)FoKBvcKNBQJv4o4XNE969Vl+VO;j*$yt)2fFZPP-9$YU!qM2N2<)<$%#vkZU z)cmdQIG5g;Y*v31Vw+oYS0~icPsOP}h@<1)Y_yoTEEwv+OpOs-P#p1(7Gb13bl+rp z7ZK)`5HoeqE&qe1{SG$=Fz<28>9k>m`kj&+Gi0uIQfJ&>I4~TxTiTU+hG=kf_SWb% zgWi^kA1~I)_s3&cFfSVF$7ST_CN`3dR43>^RkJ8VaQn!=@*yK;naBvb3XSGDHErvp zzvq@Ff|;O=+eO%U6gZv%u&XpW82mBys{7YEGV^=QJ4xQIJxc~v?ZtsaSbYyC1 zDIlxugEYRx2@PrneeQPrJ&6XHRla^ro~K^t+GO0!0Q>QmYlpVjMH?6+)l+)-*Wz%S z_g5%HVuR}L^2xwPy*1dF$W;Y*II;(_jI71%p+BHz@2ngEbZ6>H;9TSw@7)kXa;TZ` zK8bDJ?_1Z}!L^&r&WY91Z!G41P#i%4M?-r6S6{`9j=1-8bkmDM+5@$SAD8~tpQHPU{o2TnL>*dUHA10^b(LpSq zm^mxn`i;`oaHB<-7A-Z}vPS3A)-%0#&@H$OBsZ7sm_9$J7m2tU9WBJ0MmbPPc-YIs zZK7m=#kN0C@Qgm5)p8EO@^<5@%Im|qP52z!g-Mwf2Av~A?YLZ5#pdL?OR^NIve2P zSdM)4vD18d{ik=g9isQADp7w^DC~TN(obdTbZfvqJ2;WZL(bc5o&9WZo%KTfv5!R(OHHxf=?M;u<+#L_PL%AW91xe~FlA*EY_n4*HdTlv6ZGL7mst70I zrH<13vu>g@_SeUf`0358xwxX}KQ(v>Xa}OO_`Yrg=WLns){yvev1T z$r`&5l0tn{U1fdK46)#q7aaD3y*9Dt*ZXp$IXjuUnEFxhf)-x~i76T(pqm-gl`_!&KCs>I~FV z=22Lm(E6{M1ixukb!@@nGc}7>EZZb+xR<4D)E;YN@xB)@R&M)bDUyO2rt)LM9xAXl zTyEl!R!d_V$tLfWlR}W7X-ny-oPUG)e)xcgCy)h(Mxf8zZFp~;?ol-CZ#bS|eVef@ zQ&hw=k4SY)Uy|=JwvMvYiXiv2@WX|fJe-F*pGQ?O3evaMN{s%`H)AWH39f)QG~tx#wu%PEf{1_YIL>ya&Kr$xswe7~Jc zvC%%R^=91DQ1h`aTi?^&lV+JPDUuBe0rc(_abbYmU)#Y!nD?xs8Fo_^q+9!V;b1U2 z)9H9%wOGr2&%uR#WP8f%;~6DmjS@Z*7gkJoq>A%Zl%KV?z0Qw+oqqSN{JLwYY;{GA z>3O}pz9CuI&ir8G8QKfRfRe+fb7w@%6l2(kdejX-!@5pa>c^<+$n^StqKw?3kiCUD-Zq* zHV?XRweP(a@WbMCOO`rftdp`GSzv-B&(6*!K3hg7x@>tMu!fkoN^@oVRfoJbv4Juk zv9TnqA<#SXk3K{#%k{37ANt+|gQ`14(yzx zl^fJMQ2(Ry)REV0m|EcS)?)b%^5LhGI;c&W59z_VCx( z{y`xo6tev5{PQ+HA6x;7o}mlft6xg9hYkw!@RcB6oUr*tvLoKKtao1@uLrZ^Am=k~ zq)e<1GW&JA1u;#GM&&tM+Gx8AxZt1;W7If~srG?-@A-F!&G<;R**7 zu1T%Rhw17g%)?W84d96jSneD;cl)T0ROvr5kAz>6O>}>-7`?X+l+W=oHZJ(C$q?$qV|73KX@(fo?rMc=RHIQIHGSLOW`4L8d#%Tsm)_ceNMpY4{vqnO<& zr)c&;GTlX&uXb5IdM1#m`P=Gu?bXG28NB}>v^lZUe@OWVN8AF3zP1`g#*X%?3{-(D zEFLlPsS=d}hMo<51U(rxw~)?8)w@mVV_@3)VDmjcA2eC(JKhDcjvNB|uO|IF zbc>=bTyTAz(qbRWL<>LmyhvUZTf|=>L0d<({`t7Yn|`~3165L!dbI4PcDe&H0>)4$ z_@tK*>M~*-XwHAHSR!5swJBZs)uBe+An5A;ZG>i+$2R>18Z;(EmUO6=w;53>ee=5g z%@=iguY=oBS|$c>dR@ybQP3e=qwg*sl0FpdexNG@d_S&M?@3)0@!(q6QuMS}`6@8< z3?NMdqX)jo;2CzVh0Z0gR!THL3aKH5aIo!CrOkAz6WEpEB~52SJ~+@ur#kTOaSP3# zRV~^89{9Ksz1;ylnZ{eykRtWUHll$TYDm5Y}J~Dr2Xiyo`Nb~j{iH^Nt6+`TZ*V^Ov(Ou z70c!)%+J%LMsbSaMBE!nsUBJ-=16X8&$z4^AFztj)fF_0Q3%)XXs>!t%1b3=v_M2C zMsHhxZ#LgGGstBYkw8R>dQR8%}^uS0WdVTxRnnwiG zKC^mp!{HR}g}lUTZCr3dbI^Q*cZxyhef>nPTjJde*8YQ6Pl zzffyqWV~tJ5XygBZF-zJ-KXxD@l*q}x`4``&VF6w)=+P>C1BL?nvhrcY@-k7!@#Q% zZlg54&j+47cEu_^t@yb|wzbP{Y9R90MoEnKSJVYSPy4IusG35Jw2j(n2anp#Da)vA ziaL4q(4ThKBij>z-N6E-4tjUCDDzPN%i>pRT@YX<@VvO9$BPBctiIc|RabFZ{-*uE2frTDRKg1_lv zcMNRVnEM;H5|s4Z1_*R_LJU@mbhk7iKPO`okCR#C2&RY9Ih|hm`edw#B8B|>hPQrq zI+~_qp#dzW-~l$ZCso{kuLiPMIQe!&Fb~CyJw2_`d%_R3DFidtKyVF}pX0Yfz-a^~ zbP?Jpcy6^2`@Kn{y2VExq(p0-^rFWq#ZreIQ7708SHx$g`eb|M|Go?fFPkXR{y zDXBYyCd4TV*e?k-9SaNB&}L^SwKam{zT6lS6Caz+%N-5DvZspO1xhbPR%GqcZx;K4 z(9xyr9V6XOb|KKL?yx~3sTzXsRW#MR8n5p-O&{oQRTmwJ`Rqjq+ulicG)E#!hqOF= zBq?zSSr%<&#afQ#Tvj_6(Q6yj{hlSbwwy}Ld)*&o!x~y3*bmjV-F_|`e`K+rG#tR& z1b8CQge>8fV7~Y|4sDu(7#08g;=cS0Gk@A4GZ}emH zMkZ^{y1v7M@BCG&TlQ6!#}L}gJ6Z$h0PFp10r@a_lVhf|#h8JgY)5w`^$&cdMUOVx z(CDa{egU!wzT*KzJi)>}(wrr)PVw8Thf5XIT=T!};bN_lAp5=<8%(^*#%5G(-M|}e z(E*T-J;7hLLWYr@zRpzWyWvFeta(jOzh3Pw z((ANiEVRA;^HIj%7cP3Ez^9mho_n<-RHTl%Pj5ZfVpW^v8&4HZ5n*>mFdwQs=*&A> zazfW41g(Ts+|hE@(4RD!EzpsjQ6LML)nPByrzTA;Z{mj3K5};#MtM7bOlj=%V0(3c zEV(3!b4Wu|lTbBj);r+qSW{W*)3)cxV4BF0#NS5)B9|?$4%1jaW1tg6O6#Y4_z0w`U|w>Ni}o&(hUnY;991xM$a(-V$Sf5qp}?t0(Ypd%H_ynQMgJ8 zRPp}!xw5k#T0yv&-H!5TGrY(gSyG2Ob`^@_u~Pp>3t*u>=5VpjClT9toC^Z9aY}af zAO!S~LTjYY?^C>yoFJM`1#F%-ov@Onc275EA+3j6*H@RJCvbS%SwxmAM-#zmG8gW= zr|2#Q-$85_ySrjUuA7Ni&=JX}_v!8wSq^+4%lXy%D4!Sq$`S&moU1&l5R(Gr2r@rw zZ%9EQV!z3hAe%=7dk3M>7FlFY`hcHK)(Z7J7u6dRnXN zv16cbW3oj~+daMd3Q{2e3X)=qt5`o0O*Nwp!DeHLK!uQW=$_5;>ispemJ}xLApYwz(-P0~=dqO6a2ZwPU#_hxzIDHKIo`a^UaP~|NK&8f zcZ*qy5%hZKsLHz^+NG1|LP>1)DdA({-EN}rx;&r|J+76J#pjc^^`Fi4Q+l+th{2;% zf1i<>rfzb|_glkoJk2iGFDJQ@A-Gzx%u(t{vo2dCxV@XFH!U0LvtlIQVCl9~#)XBroaR)DSpxd%k$ZPAO zPR-d~2t7sq3g<&GS`2gg&Pkc90N;@Fb1Uqp3p?Fae{YANm4Y>sYIk8UDPao=R{EYf z6~oo%D2L-_m~L9cbbQtNCU&oK`f>J9ZMR?Xw}Wt%YZ4)Ff5cN~J&$H_!Vr)g2rWBR zibyr?mj#-VnFY?PNe=ZJ`Pr1w$h)dXf(7P278BkX|GL}Et$6rHRA`qBJ@~5Vyy8uV zc?oXkbYZp8@$lv2dJcZ|L-}-&y^BRNQ29Ut0z5JeB=D&3o8#NPB=K&B1)Wrf&8o99 zT=|4eSTm~cGn5}(y4{R0KGQ$-{g-|f$pIa8)T?`fb|jre{8sDeTl_Sg7cy8|(YGhj z&GVx6KxNwG7I|wNYKMr$tDX0(Z3Arx^r@pcqW9e-Q%uZ6g_v>kZg$nWt@$-cpIVuB zN~{L;Y`!5n43bC;fQ&48|-eioH5=h}CL&n{J zVQ1~zNS1%j3sJagp^QV(+f8p%=+~JYa&cT9hS^k>%`6eYna(9ecq@) zAjukNnK#a$*OQKo^we z-pF}QV-E@~?PN|kGAZBrZ-A6CHbaF?EE7S@hvJ@t_lV$colj5OMivW<@mZh3`(}Pw zgw5A*{Z^=cp-k^yh*?L$F48L+ii)8QR4PP%T5o?xpUU{K1f* z5-vZD;`k8cTJ0Tj^iZFR3(Rf~4Aa@JU0-FyQ05q}__ZKw#(mAb+6b8vRvkbv>u#^d z*=Oc1#iAt*QVyJ~CeNSYV@e*WcGTBY{52znxAv>)tk_@c!Z=(Grn?W>!s?sH?3M{hdB^#J?7 zzIc!Ay&!TUpb1b6+u!|sJ*LYsE_<2muKM_>SA#vcivJQt{pUj#g(Ol)foEX^+;-X1 zxX7A)-?6r&iN6^&1jcYIu)X~MF8i04jh+MmaQ=iQ_yRjxgD8^8Z)SGUe-XeUIz2a7 zY^MKJ`I4wQx=y^Lc@0`ikl$86If}lNfwQJ?`hn^(P%chl#1YNZ1Br7y!hwW=fpC!` z-t2LVaOO#tsqRJ~0$SdESuif`e}2?Uj0XdoIT0u`DJZIOu=wy`L$*JsmQ-~ z64A%`u(!*sE*0WKEKmLV=wEes@YP#%tHW7my3*v-x&dLIBu>J*#G9(AqtUc z^V$N3d)q<>VM4<1{=a9RGO`J=Q^Sp8sek{Ht^Dy&^xf$?K{#@x&8v%~{H90~S{P!s-}T|x%CR!xCdy6LzC z?+8w1tNlwNe>Mc%SZ%ys+^8*w_5aucD5p=1+}vvYKk4u&9S%eq2AZsv1!%rDyB}bG z07MC32;eEH{$hH;zFV;z>2~E^(9*7H!MbRZZZ!o?eQR<*?cdBZ|4$%d?na=6OAAOW z$3EU)OU+gocJOkee0e8?U=OgB6tcxJXf$ifTs8;d<$&~+g4=fELn(ddfYiV50Jy3X zfhBazK82sbv>jxL0lMcCe^~VYUcWP$^}~$~1&El(z84W}*dH5wtJlDrQ7)iAhO!g= zPU-<#zRefz9Sx0y?e;JOx7}83j4V&zK6dxtzm^i+$JsQn@-Y>Rrp-}^43}Z<*rBHN z_hZ1&JI*A$j?!cxVlL~}Aj~q6;7uLgjQ37_YK&^R)`{h=m!|r+U-IS2eL%1%8Jcyr zpFtIoweS9A$iY&h%N<$+TlSzUy|M>v*-G-j@4tV9Kb3blwKhOG;Lt-ZlKX)}hkMsa zFYSZ+UfWtL(6+-tYj-TCvP{IJ;iBGQ-(Vz-ANmxb^x^hgYN^pB3&Plx$?P3AJ7RBqKVuynaHxJ;y6nWh}MXE>p%T`3szonCeR zlkMSRg9?Y$50&KjQ23Mwj7kWf&dxWDXIdN{h+T2U+wYIA+^4EM4FpH@EyPqG`G zA=1V&w0+V}ir3ozI#b|z!E&uNYRnjP+HL9ySQVlV%-PXPPK3bpUq z?kJ$E1hd-nLog^1WWZZs(hSCyvknogRV?~BjtmCufbA{%l&bHJAKn%D)5@F07SYp> zx>Nry3XpkQD}PzkD*vOg*s8zA`TO}0cd)Y zpd(eLM?`UP%rvuc{oo8F)xZo6R*KB*wSV!Khv0xSBX}(Bzb7fLv~ZXKHr_N>WbSny z94-nxN$<3z?0?NsN|bGNoFO}#LCs(6nIajv@C~RX>))J&dTpbgODcw69KvZ%ey6(H zqyE$>%+Eqh#PkJKekTe78FN zDJ9E}sy_%FtsSX<$DLc0l#rBIa`110FChKkT-~Iv^Z+GQ)1vtUexe@D%?3$g7s&6}XPnqirpFp^Rj z=!Ik4KzOLCF{NrTo~c=5eq?b|ZPf4l!d%uqCK~w5lTe~NK-aKtH%6+YoI4vG&JQP_ zyVkJoh1)fc9C(!4X1;1$V{LG6B>DGR1!$WXC#H68U4}+|O>?E@UdCI*E7fDdz}@1S zfBKIi_uu5$|Nr+OU1)$jZZYRd_WiW{+y7cI7+URPhGs!_QLTAWY2TA{Qq(EzU*`a@ zO=sxD$@E{K*pmoMy1!o1i*bJO*)r(u-8o%-Kq@$YxA{mVcL#;;qlfAOy) zyjYaL@ToFA5Nx1buVlip9xuOd) zcy`CA^7r3+xQb!zIG1vNWnRkVMqgPo9G=?1S3o>abT+%sHLw$Dl z`DHcHv}qY8!1#CH^_>43Is!9eet1__x&W?eysdMp`QTKwX(fDI;%T&F69Eoy`L0Yif#Nc4lh_sqvjf=er#tO{YsFD13g$m{Bjh*2cmj ztiLTf32_FMum%9woVNfjcW?QVF*cM*ipDur%=52~@3#K)bybGL8+vyG#=HqT<^a=r zywP^Ee{&g4Q1RPo1#)bj6{Z8aYUrf)3niOuV(TL!` z{e}f%?R-`F0;*H|&%7918A>$9+SN$hb<@_!;zL=)Ozg-(Ufpjnw&t7f|03U-sCFR|8&; zA+Cw`|Ccdt3^2xNK4*$Z$BnDKH%0&$<4=$GIRJTLn3B2Wd5ClQ4a7kiwn7=~$dHjx z3PSn&Y($bnpH83}-}4GXlBkWz0RvYl*v(Vqru4&w9S` zK@GC90;6#HU-dWs&s5QuzXMBST90f<$;yVKkaszP$u)tgwVRBh9RkAeJ7S~9zfIm3)7p_wR|1Pk=tl!g z{r`=B|G0Lq`HTqM4;DcbYpr$l;bLQBMWA{eveFW7a*G}vvgkMtt>!sOX^a&GxDTn% z{uwRupPM-d6UaVZ%q<7OXIAXxhRvtgg8Tc4dOy7zD1f{E{0JMCO!Zln(;AT60Yty- zQrufio7miU91CofyJC?Pg1K``UZ;B*rajczgoNTxh%xd!UnnXVAN~T==LjHPq9NF9 za4-6I2aCz{(@G8R%}}}v|9fePguR&L;%3-1lB&7ar^Bf5*-#o#j;)!eYLoVq#msvp zCay2EDxpmA1OU)@Am}({(^hs4X*=3vuyHAyc&Gp7ApNiC@h|p%Tyh1yfsV z+iYGst%|mzW$(SM#v!BaM(<~C89_Rxu*1DU1?AFNG5|aM(*(;Em~r|JCS3Gh=0kJs z5s-&&jE}_cd(6`!*thtfHEpz4ZZz7_iZ-YY0_ika7_8&r5_L<_^ME_r+I@%*>|4?Q zN&GyrKpWYsO=wX0s-r?@#Wik$mlh;7h_GIFe(6cM7QGg}j*jshXn?P)dE9ci$3^e_y0UbL|mk1I;ji;H~Swt0#mN%^bO)`U!{wE7uG^lB5%BS?y ztU>q2^{g6v<>|IKtnluKvaS#uN%nCk!5f8cX=ma7SX!#QeX^CUVqU|cIl=5F7>LArU|}8?UL{qJw6hi& ztj-pN5jYq3L+^~)iPYJ?Rq{qkC9Sx^65rW6Q%j*#^n$CU8I4M=Nb7DghgG}{)^apc zv$n;WVB)=E>#2?vDiI%LjJC#mtH#GfaE7bodGpN!>q8&pc!mL(h{Bu+>mTfgfHuHH z+fPYN8hdoO*I&%3xg|$vYP=9)h2Qq-77${iYufOza zL*y_{bF5ejY4>%rtFM2BKT#PB2*e5QbH&Ypw|0Tj*4) zX^V4LU!EYK3_=KQWvHmArH-1UiPnH5f7p+a42@-VrU!A{vzp%u$%|z}@|Csgl zr%bX_Z_rbO1j103L(IJ&GMN3ieo9)zHa?>kS#Gl*zQ%3Q_ zpg)&D^AT6eXsbB1N_B^Pre3vJtdY{v#*MYj2}yo3m}Tk8nzNLYYf9aIs5A@&I!NF% z>eK|#?GyF#4rQl;ZpfeE#{ZN6+Wx2d(3N^<#Y7uS8qWZmu_UY=H`1cH>%uw0_lWJH zYfn9*kL}G@F7l2=6AE!{&`;)G$`8RRKG1A=jcLFattC2Y-Kc+H#Qx`xyg2sLgu|OE zWYKhbZAK@RQmQvI%kl1H*&w#3%{o(@3L$Oh-!ZD3*u90qrN%k!eTLa`@$gh$?x%98 zm(9{{kHXccJTk-F>xw{!=o4l*2zEa+uNo^|MKmYysO7VmASZ7CXrna6EKi~VgLZ-_ z5+CcnHaz?AQ14OtVsu38MgJf?`Po>O`S)4vudHy7Mz zxPi_+Z#3IFb*cG3RPJR>>GDp9wx*dKB}*Vkufy`J>( zEX1K~cQEBR$U2%XVO~RD(}4nbi*#f8mJZz42R~R02HEr(Vw2#|7fKfI#hD5`t}kie z-)uMD*MW&#ox0^)3=#L>@q8eQ&WT`uU+)*IC?qY*e_pYu(C;BBnAXz)Is|0iw6pigMlF+4;dHOwH;^@nU|u;`ljl&2KF0qhB4Lvb+5jb8MRS&a6|P zMY_F3+yPy29Zi8L(+QR7Z!=6-J@%+KDVS(Tomf7D2fd=vQb3Kb&-=pPG3BSzImncMKP?PD zE_CEH13tTFT;utYf=Lc(=Ko>tEu*6RyYSy(=#rtNWdH?~4gqP9ln&``q+5C@MG+|} zX(dFuySux)ySxAQ_{7iW#Org`;>E1Rnz?J=-`M;5T$`6lr*ZDR+*^*thq;S};IEZ{ zLycNQN5VWME2RGRW0^#pFzQz2XIw&s=f*mbcL$M9x3LGq7w%Ja<|+IFtH92w$*-XP zPFl~yr9QvSv8c$x&tgbkH?8SRjnYNrtSM`uvm?dZyuXR4&&$QPTy{V+Es0ShtkLGJ zyG!3-%?WxA!9&~q+?CXTxa1L~3jzWHihE*)nu5*uF)YOhUqx>HkQlFml5gV{BvE5^ zT1te5$DKnzCHtU!C~FSPV&TZ!$_E9Z+M#1evw>ySUImHD_1|uW?=M*1-2r`Z3yg## z9ebZ)3SGZZ%f88*(AzX6ZDlQz6rf7w|259KdE4{{;5I`UdSo^7Uel(@?fs)Vp#W#K zboE0#81%9cd**FylKHHq^<2Ho6N}k}RRo7d5+A*bEdFuJ(kd>-j#;WWQ0Z>X%j1)3 zeCF-hjm0}!-4!|};EkXSHq`^itKT;*m;WO3M@=}5jX>1u&LB!Mjhs`;%F%53)%Md3 zHSv7sAR%3PW{KU4_9-GH(K?7lPpD#bMx-a?L^mcx$PdOZUJ6-TOz$}ARYXWKz zV;5QKt5HmUoGcNu7KeLwl+X(#mn6^$o#%UeZxBV>ELx`YoK<=!D&UMecG+# z#ok!&fi#b)5h-40?pK|3X~`e679(kQkwYMc#_b0!{H$jzuEMWJ95ID?R!UgvV&nr4pXm$cX%F!%EijMDbniKL`yTPE6@5w>G@V~E!YZ7k zFd-0srJMqL6nT;lK-h3Et_Wnzs2_-di#m2Fh{Jm+#O6qmZwR};H?5xW!h%?TEEqd# zj^WqmS_w_7+wZyQkr_RH)#gw+;~!>QscW(Dqg*p!Ex%geX$1xpA_GMu9B#^WFL)HG z=_DAvp%-t}tzBiL=|paXJ?jV5XDN$iaLoqpRO>a$wIWB|SZ*(D2VLxa?)L0x?rS;A zbEL!0oTi;Ur&m#jTC#j)8t#=IQSdTC3iB&Zb^PoO)yKfA8R`o!^?8Z1vw2bG6e%lX z;sn!TQ~8ka;R-zkzE1tFTFZdhthkFN zB#eSqpp5K#F;>|w1lo>fPOSW-GLOqo=~SiznS+q#bOKEel~x}2#iQyh&Yjo~q`F9I zF@Tmd+e`dOeyFrxuucI)gMGo71?7rp>Q@YLE`tw<$wl0>7u=@J2B2{QzZ`tB%w8}3 z`MblOLfu8=6->;Zws&-K|AMg#$MFFc+XY4fu-Kh?DcV2^86S!Fx3(|nt7-IUw9Wfs!i?&QMTRmDtaHjaqKdp+t$4Rp~A-&E2b6kd5({<_b%dH-^n?{GSL(A|xPY8M1ki%f5Un~Q4B7SR=SKOBU#|2E)c7w% zPEnGW(BZAMZy@g#e?h4Xm9t@2EWDjUz?N8Vcn?gHd%C$v?izV9L$rboZ%ns@yowh+ zi%oxewoa9c0$n0gWtB4ycL@hxnfQ+z*zsrOpX)0>`R!ifK=xtUV3^|i z2SjrByPw8Sm(IAYc4|s|AyyJB8%$Z9R+F;k>U(-KvzD_Q0aS0S)3gG$ir^Dxc9Lt0 zwVrR`>%8wja%v>l16!FsvGx#W@3HfNW(P?PD! zXCND;2^)KsulHax6a8GP9>M&m7lzm$VF#;fy6qN}v|(SKOir6UJ9I6Q64$Pz=~$I6 zbt!CTr2Ro{AUBLS&F$fF=({uep;}EM_xP~x0T3Gc;7}0T3D4wthiLiq2zYEH~P4*l5*Q@Gvdz1CR`AedZ@sYCfXu5G-`K z>XhtJPm;g$v^hbXk7B@Pd{KTR3p-l9v>RZwH9mg8XRQxGkKV$z_RWgb@k)thLQX}G znvWr|^R_0%8t;Qm*pwo^r5Fuu5^z)Hkj@2Y=jb{>t%{GTw2Yb5hLsa^b5J|ps>ucl zP#_Kzl?nj``fX^BT9Y&97ZSf{rE|_4%qG1Q!{-?3H~HS~OFY|9;KNc9zUv>~@`Vr| zWtPn#OvK*G=idJ_q6KWGx`lM*{zKg}_XPx9vaFIsy{g)xLD)0G?ayruRG)*$W(9*< z2w_mwXlBjchDTv19v0!g7cDI8S5x>u0#1m}>lk6Ijgw^$C(0@ZGcMba{jOROl0hnO zP)!huuy+elAi2%6P4i9_m^``9Pu94FP=bm3odVDa`Z*^%aOCvMv=DNnSE^Atzs*^` zuKTz-QGTQ0E_Oho(>Qf_T`O)zi^MAomy!|Chb#D8ui0@rIl|02+` zv_GDgb@L+XPFLK#VSH{PzLyR3HK=a@EQYn>eTBNmMzZtkoWsWVD(ND&`w7Jt234r^4V5&Qg)EqCur6ZZM|=6g z((=uB2IlbQ7mZ$*fKpV(urj6o=z9qAJgMt}JQs7W-I$;Y9ibm&w<@mmlXIN--F$~W zW?)EchwIUW%q*~8!jo<8iQ=n>C;lYDdwK-Vd<=wbTO5~1At&h}?Vc@OlY=MpyFqF~ zKwjWH1y!cEh^3ZlGl3@1iRmU@Htg+_YY9vtJu?lz36Z4JPoA*~OvQUhO5a-UNU(u% zWWIQW_4lu23fqP)UuCfdl|kQj6XnyKJsu1sfiHH8letwXQhw=P&+|jjE4lSOKP7sv zXBX?vWfhA{L~)=1m-4cTj&RjgUN44bLw3c`0-D99_3zdsjECna`;9)x6VXI>{Vp_ z_Oo%YA~*56&Oku6*nOR4!YezVhje!O@s(ckdH%t;R+9GuL(Z4LycyT{4VgLYOE{k# z9-UvXlE8CDadOq@^nS#f!h8F?KMV6w0{ictEZAv3H~mcFRm#oJL)lGh)suP0xk}3% z_X7X8E(KKd+pVI20Z4wx3fhj(oW=aijo$`E${f&TdYT+(L!Y(&-JQwtcm5Rt{p0%p zK`*nhq)!Us68d`*-rm)&8az`R*ytuQ;7glE^RqEc%FCUC@touoD_`*3(Y5Z~-hgkf zyWBLk8|_)yK5u*L4xgQ_QZn$h`drFgPW{-=hLQ?^BL42oPwvI!Ob&W{j8FtaJB?ehQ zl)FxFy(m8LVfV&9RZ+pTh!x^ElJmvWO#qyS-3R7PMj8?y?v;IccYk#tj^tNF@DQa? zdHGJ;A1+cYB;mMlTiPVvT)n0wJyXGLrafA!AsgVmEISJf?WqDcR1oiwYB9Nwh+~k^RAxF)zNG(F<&_#^Y|sj_IKDR}$h}Q7FdGxMq)p z)l;f>%I&;)k2~L;x@Q>+i#N6;*DY~M-Wo^&GHTN{XP#H23?7i?JGQ|32)v(d$M%M1eW=kL+tme*E z{`s5~Trb(tz~7-y{$*(D$V^wO^-IAuPNwn4y#r@q8JNnTSyPY9iii%-sA`int=VEU z2tyC?Ee~qIvW4}ez2_b?t%CV$ybe}H?6j74b5)Qi4L%s-xRiVUoL3i-_kFpn>rK3C zy>oe_kO0c2ZGFQ}cROS#A8}awVTj5G83ITjR{3GC_U^RjV7>0U$xLqFc}I%x$(M>x zQ2NveoFPelB7+E|IsI}f75i6?SEhaA%OU)-e2`vzpc z43-FgJO|ZX9I-FRc`T^l8`O5Mer*XO=SA6cMzTs=AxC@78`5`6zg)Kl9%2Lf<3UrY z!UoWJIe^J);295Jo8oH1Z2{?*8Y?nd+eFT9>RV9d2s@&P5~0?U8%O0hVYbIQ&kx z`3mbP?HRT_yOd;VE@8B#!h9(Kv(8d`=N^9vuXxK?75wPV3F@$$q!pZke zw!WfkH}&7&4fL56PRs9qjsXQWd0nhBcqJds<&|j29X0P^DuOUvxmReOETC2%#Bpw7 z#gU_k45Of7S-w+{8%VE?rpP8&2+iYKfMf+r;n&B#eDEs%K4F;N2^%YvF03speP)&#=NkIal86D;rhT%!A) z$Gr^T$%>ZIVuV>s5Hq&y(sGJMP;RgEj%2os_kV4NRhK<3UMmDchBYy9RzDK)$R!`? zBa+6D1xxz$8<_K^4eb)vkHt?ya(BPbCS+bC&VhmJ_BJ~15SDs}Kp}UrOPTN_2~7h5 z8FL9J9!e=H6vT@e1Osh;k1>YuV1t&8CoxJQU<&(Q`}dV?jRY4hIfDH>z5L>HdB|ZW zadU2%!oiY1$pdg~5jyAX+~|fLAPB{7P$QxLf<=thwt*ZoiT2(#Dats}vhJKlX&*?@9c^7lZh2&6lpFJixI+<0CS@!=d-PI`v8Zyb27fDZE{s z_bV1-YeH6B7bmDWCZ|8G>vkV6$CQ0;!m^RAEk#&(uvn<F5QwNWNg<{c}8 zirqc}8t%pedc7%8kiRtV3$p#j{0U(~+dys5_TY|Y6pI<%4D8X%{59tg(1dcZ(<9p~ zNl@`b<70iTPJC=xH`q&L(usaeb*YtSpzH_JpRBK?j*GFH8j(?M20?D0Us-G}U+=e< zGxg-YO1X5_MY|bqaKRLg8w>Wz&7t%aop{6>A!TkZHPvFFLGvWE#Q+G2=2-4+tPx)_ z1JfXsdcMXz2 zT?P)FQx2YimN-yl$aaVr_o`?PK|o;~KK94dElnGAL+83# z$hli*ep}eIZqEq&GpaeNgcV%4{vhYa7Pfb;Pu%ZUWZ=Wa!WWv||24GW&&Q>U-%&~V zdI98Zk##d+K>bLJFsp}Lr>9!I#Jk8~3ihLClHT@PwgG!YQv((}6WyKWczR5O1$J|1mNYL~T99ZdQ*-5ux|v!7f!A3b z%WMd_3H^$%L&o~93HM?K<-!FwOpvC3!FMrL&aC_piyh9_G9%C(VZ-$XxWQ(g<1)7* zYlfl0=Sm*59KeVMNb|OfWILM|-3ST{z=P85IEfv+k{6>pMn-d(^PW#bKm9VUt~9B8 zjMyidF%Y^7-AL4=GN~%XrQ_w0_)lTh_ged%Yh={vK>)Yp_eybnFP7?Y*y>4X@UuyHdV!Lj~qHWywo^ zt=+llF#y5=0-MoK)h6$S}r8U-Hr-p@06=e*qV$ApUDtJ{3c4a zJeH)UfN*0H(5>|_0HuUazp@*0^-dBDBHGncJVm>snGBB|?^ROzhtucKwkGzJs z+RxtZMl2cYM}4mbT^fhY8%OTsGFBD%*kuFSq-^yavnC^JIHH@S_++f@HO~JtqWekr z%dGd4`5kD;uR?>z8D$+f+u#+AMLf))8K5YTn(oTJ>UU!sUSnI+&G`GqkD2i|2YsJF z^~TqC=clkecoB1HpcissE=qQFypQ@P9S9zt;noPoAINlSQY3F_j$`5tjWiO+kgj=L zhJ^W(@3s3{Xb>N+{k`0TT6AXE?3V2ELdwy22fM>D-*B2KMG5`y2iX2jCpVft~sMe0YID&nf}y-$1W@*kPj8#}>xVb9R>|TVqGKRm{rC^OHdOdAsVJP7C>M|J#FA4Vf zZvDc#f7DM|jA-KwL$4ucru-m=H}obxD3&gCXI&@{o}^1y6Y9Cd-OK3KBsA?(&k>V5 z>uVl@>k_uUmr*|7;j;dgeN7g4fQxjSzYVL=G>3>l$hQj4VDWh$W0(bzW(^A ztKR!2-RtT&I|=WYcpA@`wwAf>OtZ~sqGW>^kD@Vly}FQL`WvEzEE}db&i2%3DTk=p zW(1UU?YvS}^=BoTjYaLA=hvtM^*o#;RiMbMENTc5lgadj zYm@ti8`JPtU-$gyGl4@CcUj9-sv(M7BN=|PK*b5Yf(7PEGM#*W<9&KLMK^t^W&2^0 z?yx1S;Z4nxfllpdy0h(;DbmF&;RX)Y!@Qhk}}`pYlb`PavZs;UbU5Kg*Zin_>Jxnt zfU9ojpkQBb)&2&KyDCpcX{nvkOiG0(s<${Hzv=y#wDrM)X?5FS*GhQ4Jn`I49}`tC zxm;M4H3}d>gd+AA?u+G!`pejl<6F;!9^Pft=B^bl#h&@T3HkaHaav`QrUGD$w{U25 z1Jqv2`ko9`yG@RO%XM$$!kdGRZ-iR<11b+H#{H5p%(|m2jiU~Dhm-r$=i(EX1b$!*|iVXXigk&b0aq-oLOzyy(Y#h&oHW%wnIj| ze@@-Y1J?9{N_4y5j|UQbma81qX$^suSPs>uMyem|ks?o%RVv0t)xQDcUsn|*gA$~{ z{1&akgN29}5HHsEXDb5@G@lY(uCtYEHK>N-<=~ZKD#Ncnnt<$oGBM0IL+0_@qaT?% zPHV{bJRAK)J!)O8*(Ymol~L{VVE&KhiEO%9F44$-!D#iA1ItLEvsvr-ibKqeY%@yi zOZ1<7@jIzX`6W(BMx8^(9lZ|!C6S-CRdE@OUTPj6nJ;uTy_R%7YzRzrlYd5|!+3v; z>Rvg{nM}gweH}?BS!Tq1{(W1}qm!|}(bfhiEF7@E`>RS1W4xkwz zs_J;r#Io>Q#spW|)39ngo?yY6OB1fQAl72? zdsVpZg>}hd@4CSRCeJEDuK|@#CY_uvNy2CDkoraz#mHql{doUbK;~=hrq6{U`0nHY zEQ9!&0b|+ZDR^4(eHG%gU3Fm{-*j!m$v8tF%titwAwr1!3}fxWS%@|M$p5t^StQTGr1U&{qk&88I4AB?eY*h8g)Cb@79c`3RiJ!u#dOK+ln$m5mT@f#;I!$go3 zXE0x7*ygE;tFX7*@3E+Af^cQ}0E~=(QyfztK|ZL|rU<3y6QRRylML*&c9;qxNPEd$ z91BsX2Q_TFT&jh9xSt$Z2GLoF!}~0L`ScK)p}1+C0E&4vqoV}D*?QsI@$hFIfgG}z zJJ*-5+qF=KvgAOx6ECkhp*+;t|8L#)x8!znpK6tN|ld*8?VV57w zGLbpstzZI$4}at+6Tl&&eMEwlA=#b@wn&2TL}YLwFc`s#di9lqtof^-$6f0Z)j+a? z4F{jau6`fSI3j(YxUBwThQ*S9Cd)gf@D$3iKNM&2B6;Q*u=2qdIb?u9E^c$F|&jSVwOnoWrSUm2Gh5Dz`13spc? z%0a_r0limhQp#?g!`TB)!-r`zq0tJsYsGFX?PQT(peUHnWzPdmcab;LxE`wqs}MOK zx3Avc3ufI^8;D{;6%F9-y>K8h3agzp!HxL&L{1P^z31L)>=&*3Nh`7HS}q{$09Ip& zIy?_4)k5UUg_n!m)0M~lQ;V;Ya*0ak+OhN3|HTIPhsWU>*nj6a;b&LtApJ)?VfEFFPIy zA2=FE5N9J7(ri1e?SakqM|I_I&j0ZO80#n4b2B{tjvYtO?Yg;=xeF;>yuOaa9AIaL zM4%+2p$NQ`6M)@jQY*=yx!Qjpt$Uyq@iyzb8x7X8XW~xvq*=k3SSQ=oBEf|%3K#`y zac=RtvO)Xql|g>t7xu=V1>zn!br65h}dC1ad{kTajyyr=4szqvbDcXO@4xnfm>! zu)RBpA+{*BOO4Rn^xvv-C>YLyh6oQS_fhNh?Bx<5L*W=B(25Y2fuoaREI|C;x3Hw3 zC6A&WSoTdu{jhUdT%>Az$9wazN`bJpV#5*rz-=HlQ|%X5B{$(6Kx(~%4}Ogo&zR>L-)RYY8wSYECe zwXHs)wfP@DSrN?@2m-&zHU$f`MbOWsAH4$5bxe4$r-FDL+|f0@xyI_Rv=BI+01A_ zQeBEC(|Br;j^pW3&2}f~FR{>oP!d5Z==@Q0cj(Vz=0C8)Co!lNMU@4|XTTmq7y#*11WijXv)2RV>)VJ!#DWL%9F_ zg&;DN8@C=O7C2pdY105_i^BcUm+v3#U}Y@x;$wuUv;QT62l^4KNit9OQY-CC7cdc2 zMq<^~JDHIr7-#WGm(+ZfbQxcK)5F;Tj@zJp(W%9E<1KB<_A7_yl$E1#(Q z7P_8v^O^*g^~8={K}Jc+9eyn>u4+lL8bQPrF0;4>9O?bcuqZ1QP)g`NQi^mcZkt7B?7AR4_f~_pX~|QPxE1GY5Kj6$A3G zVwW4{sbZU02P;&oR50?fbdslS_;Opbn*h-%HUFPy=O7Qi)gd6O+PC?N@&G^WwPH5h zF6l_tb&g`8|@n#h`UF(eh{{k3GxvVa}aqH->6UoQ(;e1SpW3F^8 z70q`WDK7YoFEzrvoBSU@1giHR)$SR2;s2kR;s5WkX?wTT$f&3c0JIfR?vO97nY}sR zivpT0cE}^N(4+IE8VVcX{e#I#DT4RUt#c~?62kO`PlU1;DfcmQDpGBBF5cha{2pmA zqGe}u=%?f1qR3Q(Z^JjBZDVaz3F}*GXJH~1Jw`ap(j0`DpkD8F^;995KaxiB(Mh*x zZ7t{DKl2T}RU1y{&eEpDQqT=zuB2!RB%TaB{z5w=oI+{q?#!kAIDN&gGjo= zL0bo7)+}yi0TD84C<46GQ=qeCB!{{DtF82?T6shAzYhb(4bFm7E$F9+!JZde{6eE@-N4G8FR@piKG?UMOY%Zw#o3vK=WO7y*##t8z*IJ_!qe0$O&tJT#=qY`jj_C^AY@ z=xrw0ge@aaxB&Z?iD+hBUQC&9VgC-TDl*>3@hwiiq zC<*?@SV0QUjhYGgPsOHcabe{LT$Qmz3wqN6&Xg;A^)dO!KqGFKBcbz^K! zJ$*4b&FfC#l-cvedq%n(V8W4#$Au?(+fp>>?aWx0UIK&pfB){SO4_ygW^?+w&$&}J zHJ$=TdVGM<^PeT+p9|kYvi^uo9kvIX_O{+Tl4a2d#{8k9M==P=GQBqZHdOt-Lca+Ot0YLEkF#i#nirTR!VAn*HbMguzO527FHanC@+_ z!pz6(_iA4inDhPI=h?XFoxgZN*9~D5Ea~ak`3Lv|#9mJj3b-oNbA>Ik``=IdpU-~8w2%|OG-+Yh6p40M8$&moY{w93mpQ#d#>@DMV=HA0Q zUVn14(xmw-*#9%S{LfJk@Pe6v4gb(u&+Ok1?Vo%7-DibTV=A3pux57$f!#J07n?Elb_ zx@bJ&_#5?k4eE&kF`C+0h0Dwe?>g9Xyv}+1iz`=ZzfN(&_0WHx2JqSB3vec1j#j4r z=S;*JF`iFW>z9A$3iVEj)<&Z*QcX5sOcYfB3=?G8TVLrn^_%klUwM7(s^j`SX4-&!W zX6q+~o71Gb4tQI1yi>Ycucry20FPgziel(Q5i)6U z@)CVE$RB^#rS(A0V=w$B`UPPVgP2fGG=usZz~Cf9*bgj$<}bs8X7}j@Xerq}$lyrN zl0*{xNfa8F0XV;jWpnR{{PW}S00OOOhQIMY;J49d*hnOb=o`5I$8RsaAYcmv$vX$y zBm=f8M{h_=92UGzLLIct#i3TnCKKS#Q~p^+25O7w<#NWZvhrYb@kR{6rgHSRnra{Z z39w=q5cG_&ewn|YNb4g6G~sdhu>eyO<1BlTH(<6V40Zdg500_Gl1plP@{3-zb2hLAI zxyCY{LEk?S4y}eS!D3mQ=>8gb02c>W=o2u6(Gpljt^iG_jr>by*Se!a42PeGoL_CE zkRhN^LvmqIP!l#eir>-a4cJY9!9f(^PGMV*pc|Ji|3tJYo!=>5p(sxIxf2e;i82iNYfBCMi8=js8!>g z)H`)pep_l4O8&~{sT1hr#Zw3>lH*1vlQ|*3%I_4%ghahlP8x6v9t7Soou|@|k@`Oa zhHx)F@NWBQmu{u(6`v-IIc|4ltR4T}5Q9pZ;eKhH_r5V5Qy^O{4jE9RJgV}4CWGs{ zJLz!s{>W`x>uG_G=Nj_!uQbaa$9C2_^nHLJaFb5+msU=ClTm3+c0*>)2dozB29ZNpa1S?SxdYfOnJFB{5uWBBgf8+A=Hg8~} zT~6C{V&CMsW^JL)&$W({^+z?X2T?3Bd6C$@w={s{G8zDtV@d{i-kOgD9f&@2e-P1Z zF#KtKd-44v&7OaJ(YGGb`O zyemsat+UGfEqlNN>LVj;Sb$?k-$T@lT{SgPtx z1tgA{-e_mKo__IOXuC^o zonys?2@nfa+)vfM779@BZgH_imx`GJLp0<(Y|?yKyfP7AcC2NK?Njr*Szm zZ>GQl_Mc?jHcSjeO$pssv1v+)|wNJ2RL{Mx_qJsE!9tYBF5Um z-7|)II=XTA)Od6Hy@AmK2fRVRBlA6ZND7019y(49VP47wwU)v7Q`tWMGunzmI@Q1s zlrSjZBvL6vFn?! zB`(Z>>GU*^do`-9XKjCbtk?VF;l^WFCg3<*d)9Bnad=>=iJxm!vcVL|avJY%5Uxf1 z7mE9Z$G_C~p+qhy*THe_Ezz5q^_MDq0&PXKdlFK|_lWGqMwAal>lBjG4usYy7w_Kj zO7R@R@wB(GY{P&#qvHU@*sbMAb&2tYp8GFL-zw=mSan5?xjf~=FuBNG3d!`i2I&+( zRdJeE#k_sCP@aHU1q23%=gqUr26ZTSeD;Fo5dzxdpM#k6t^@e*niC?q6>Qx%@5xM0 zok~>Dgh{e<@;EAS}$=%D=k?xkVID?q6H4#DFq=WjPj)q=L( z5+VdX4N5z!E%=+Kpborw?~|b*L*49{keT7@4)>KR1pu#E<$WG|%9oR+iH}9C2&4qQ z5JGgC9GFs}gT#M%6j7m>U?Ny-02T?OCk2=&o+qna&#t15v-$G8mmkC6UJ2jJDXD@m z3BzsQ89Hoz4=m+Rt@GgTtdFt}paej%=>FU5*E0D2m0I6fkmn z*fbUmQgN{Ft1x4T8L&0JH{Rjome>nXiM~hHa6Kit{>=GXTNi+SeqfPs$Z^@tkpKo} z0v@vO1g9mRWP9iE+b>}2O(BIQu%8G_9o`5HS6H>ATH2?|ulRV}y%}zp`H8S)R(n+> zn{?+_d-TS4Z*~=(CuW)~_*;}YvT z6^$NaxnTXkZa(y3>UCZGmEBH=FQ(*XF=CVZ)Y4nx)VJ5Wv$g`P2#79L5ll~3@#VmQ z8UTD0Bh9hlMGXMCQ(l5g^Z9ZkH=*KBP#SqpJ;pV}ntpVWNPJ1KWP~SrYdEGGaB?WU zghqW2xu%MY!$z&xSc5>{BC4-hT7j2N7%cgfJX$OCx?AtgJCP@|Br#1b6wq*nAuYd^ z`UEGrBg6<{xi{Ro&s`Q95iF0~Q_rfR5a60JF#2is>}w1nW@|C120{~|j{M$*CPQlD zsMlY!`%}ngVZe+5s4LN6Yh&Y9w!Z`H@mI=s00Tu?DiF5MyYGOrHMjCc@=nIvs2kCu z6Uqwk;45=A)``pg8-1jS)@z0&Edl77Do?TD$ok&#Y>(5Lxo!st@g(aTgDFb>&a7wM z0>-kqOHiAJ-w*rN!iWz}j}vxl5^_7$2X>u@DU zM+!@U8Ww;dtfPwJz#zn3bMAh1!GP%&KoJ zl0MBAz-^GGh`Pa7 zm^;%)crb%cC-xyC$GeB;5>I*A;+9WzTFd}ZOsUS*YRnGH%U41|mdWJ&&KlDdzw$zE zo~Yy`1|&0s4}Oij-ETMnoPjsz5T5x5$w!s9z>U(ouT`B`7H(FpvfVdava2fs#Bs$~ z8NB?^ljp7qup@5YX|7;#EL@90Rr^xdMoE&^z0qjkBnGE@`iEC;{WU2)8fA%oXYxRO z;r43jRnRXLg@dl2E(eDld~UfQq$RB7ZUh$_RQ2~b{1+mIYl3)IfHL zrT>24^7u5oqvbbMeU#w!u6Bvuy!>6g#`W1dipegfm$p^Il_NTrL+qMaQEY%ekcH1{ zz6y6mI{j~)?gbiU@&*wX046Ina_#n==Y|h0HV$qly0CR1u_Su6wm*{-zwoILqfKlo zS-2MCIoz-Lt~X6L`VT0}Ye|E_;T+g>ZBqX8$VDG&lG^0UiYPNmSltgOQ`6# z8k%W8PoQ_WDuRVS;iKcDoOE|ouuc}l3j!~g(R91eZcVWWPD5 zcE@6?G74^N{t7$!3OT7yX}u$wk$aEwX56yhO30Kv3hinmFyQ4*eSLSP!n5_;dV?yJ= zJ1>w5>vFfJzA|Lhhw$p_?@=4?`F#WWylnJO0kc0yTO#DA8H{9{Tgif|D%i)1G{HL3+tQ zvif>#1JL0UX2c9-ilJ5N7SAX8ehVorAp2Hxk$u&trc*V?jg@ zq=ukVJsW-LPvL8IZ)6jde?%z=v-)pkjamt4C|nq$E`d1yz?b2b&1oJL z42DbxRC3!tpkM;Ao7Z5SV#dYqhg8hqXS>GtCW2bL7t?kTn87Kar(IAmT=xIpzw}Vy za$H!V20*mAO?Wm}0v6S5So~8UZhIH2IctpX#(yNyD?ryTcX;ovLqyzl+k8)ID6wOovKJ>N`Q6slEW(7ROI zHb|i$3W@;L`kdojM*HW@EdBfNXra3og`?Z!<@vbZeN=}IgPz+ZxNz=%af@ndcwmi# z75g`(JIvjUOR&`Q0_gK1gS*o)$lS4{L23t;+nz^FR{VP@O0X)E>ZSkKq?F5+SsUx> z$q3*e1mzjejWs=KGY8hoHgLD4sb2)zfHy`!p?~#yH)d##P%WUbcl%AKaB}Si*H5+^ z`u>_K49lN!io!iGKmhPBDY%EtD;1^*h{09q;m-nOup$rOnh@wTqCH`s-@^%?>sbwS z8rBSe&0lcsAF^`SZZk0(@oqt3zgiRUEGM^`^G8;W;JpR;qak5Ja~}{9e)XwS4EqOJN&}{do66;NMZmvI zCJ>4r3j>zNXFr>||C>Zjj{=ehGC7w+b80yym||7IQ-j35Kb zomrS11tQN)rhoSkbJ$~rP-yI9k==ZMB*ntPJe#l5)Q;j?4VY=FTUrvr3^bP$U9QfX zS$d7m3mYo|g)Df&S%U+aTJ=|<$XsO8vE@Xvcl&Ro{c^pO(FF}*In^T6odHfO``A$x z5iA5XA{GU01Ek`0(lEt}si^kQ*;#ee?`K*NRFORZ7y1G_j=|7v0yBy(qU_m~O-^S% zmLdLznqvWgq76yc{B#`p+g7a{+RRwLM=eJOG}P;kd(lG}JbSCed9&#Kjp0_$;oW3yi3cE%4RALzvhG9b(PcvLU%wT33~ zq=SD2LPX1HU&s`HW2LgvEV+wTixkileL2pSGj%D{@Y8ATBI)MqMUwkjf854p@9a&o zXMfd(&w*+C)!f2~SC#!2$Dv8efqjm30a(kUSefBC3sztEh z{gOBR)2Cu<2pDK+i$u5~#8ivNI}Mo3I`y1{U{PT-Gz2OrQXp&wJs4zy2b*>wlkOZk zx$0>7^>m|WNSsfLd<~^o>zk-P8YYA|^UGeJ9oK?rr^@l1!i~jpXR>OJgsA{OOS-~J zqR>s~^(=-kS{E0shhUApE15iPV25QdbxULHjP{51scv_*%oFVqUgt;6$LTt<&w@c( zpypWzZ2L(5;nHLbgo<~2uA&;nxa5!XsQ4@t3k|}8ZVF=MOK~Nkpo}c@;SVgdlcQFU zO4~~F#Y&O){DC6@tPs5ytJUbjk`I>&yceWat4l*pO^@x;X;6nqAElVF9LLQ}9jw@# zKj)}VEeq+GDQq_qW~>SE2P3M+3Dk;&+1n1;HME8ZM(=(sy27F|XIvN&DG)Qo4N-H5 zt3W^K7`%SAQW7YxQEP{Yf?HpIcDY-GVSNqN>yX9YzQfGCnTsNYOMWGFNjRVMm8?tZY)8YW7YK4M4a;l2? z^5)jYhnYYgWN0h2- zio}1_cK#CK{B=;$ee@HjwrOcKJW7fW)`(uaE6zJ7Sd;gE*t+VlD7URYjH8safQrD- z4JzG@bax6!cXtXRB3;rcDcvn0DcuYq5<@o(&G&lF^&IbgzW?UgJnX&SmA|#t-g~KG zLZ5%|c71-jeW9AL`VFOopwloYO0XxsZcre#=3_z?I(pV|CBu?;Tgrp*WU1pkjF&N~ zIyF5%zZW-S&xNNkW0UQockBx1UOR6=MIQP(CGi=D_K&YxKW1AX9Af4QqyD&&)g_}> zR!8g=q0ZdEd09KH)_t~M5oW$Jl?Flnq*g)bKeE117#6p^oo2XT1A#KyvU_=h$Z(o5 zzvcH>Py6IHnknsE3J0X1HAMY*4y%DbU*13dIcujBiVYW8;>PuNbSm*It;7d&;v!wA zb}g|8E_L)L<&^C{=anRL7X7;4-_VZT-`~1{ZEuPkH95?A^7bB%;Y&Sy|ERLkS9en6 zpxMsEVNBF|QAr0jE=OasXI4FCB>1<_|?s!c23ZA#7- zUf8|=a^5dxaVBKr7*{nidng8SOo*$-1K$tvM-jRY*B^*S`$vTCFp+TlWx*r;>`P-o zSVj}slf9gcwA080RgI@9nK7ygfQp43G`uh=4$kpH+N~0QwxdhUU^xkUl3Re-AC;x>J(0_t$a`{CyWsQ_!aG z8|F#k7%a@soDm%%?f&2}%|Z`DcTZ{3jz_b6F>HQ4j7cE*RSWJg(gP?k<8yMekC>>r zFz%`v;64)85YBTUb9Sfp4!5G56)oj6`E;5p7$gu`X-?JT=WSjypH zU%yY#JNpt!%DjTQO&m0g`-X;5X8CGWm-G`!Mb%gD8U%+hp9TqvmeT8$KR=P3uAAXj z5nVGLJDr{@UAo5O?G>PSAE1t2zi|hK-w#ZodnXDPW}LV`l6>~dU7KTecbq)~Xa9zH~0)OJDC5xK)}|8apYT(y*t z%dghANha8F`@orXYrZttGNPQvUG%hq0E5d6v&VG3Sp+2!x#!X1gIDb-rJdbF)Zg3M zTil`|>o{$eE1-FX(VdHZ0Rmi8c`GS9l|`L)5rQ44Us*CJJYRc*OYNmgUZiFbtMGw5LroSBm=JF~Uo zdJA~v_=oCb-Sz_WZr__8O$VH|zV$1(Y6vbPji92We-%JP&&;f}h8IYIw!)&_ngp%> zKdWi?py&|5|ee+mbg<9w0C1Usfews3vfcW!H9(QIb?r3%I>2IZf&)_D`B!5=(3EAX*0ugHRLJ`-eq!!mk0cw$7wKfeIit+Cy@<+Ow<@`sAEew+wBJZ)v+ z%bFJ~=k<*ea*7E-tBu%c?-16J7oFUbGSxSj7`}%)GW^R za`l6-{4+s8gQD)~54o@=1nDxL$R>UB<2i-P11J2&@AHIL*oJv>-r}f~pI9A-q1Gfv zO_^WmAe?wXM6WKKe<+H~yg*TJ-;ro+*&4LOe67x$9f$J(({K=qvJt3gr7lnDqiaVD z+$q5fc!$6i06F53%v&Vn6#;=-(XRO9GE_K*Z^ z3#SMpBH_}-R>S4olm%pHU0?VN--)a3<44)}P75mdo4}Efz^jnwGVZR^?2EYRQOe$Vg?oM6?(peedZLGIi-B2{ufxevYCt-!1E?kB#V-BhuVhQr_fVFR zmR7u4ZC4`cI2Hlv3igpIBtYNcwq{Xd1K2|aaG?ey%|?Kxs~6gk#N)1 zzZ>7-IXXL1W+rwjV1BhC{3$T!RR~m+7e(sZ>}FL0;SaKGvD#(@?qJ*ofivf4KDF*G zXwitDP9-*u6R!=y@HX$wm%;I9uCyqh;DG{W!{3UzKW(>z$rG3z2yr*TtYQUOR>5z^ zeNtzQbib2P*4MKRsu5KS4kJ*%DYZVS7OofMauSh&%;|=0ZRFW;4b!#KNK%H|2d-y} z-pG-Rr-%=x42C$?M8fY)-yeNA+;>W^nJPNNJ-@&0k-oEjXQyZ7>J(-Cw^IDj2cOSw zG9ztp4lT1f?=X6rnY+mdLs;0qYt>7%&@O*@J3BhrXuN38+v$-41HR^KD-cX$DCnen{nblRza7S~_3EvuW z!Y6M{$j)*mV^1G@IX=%_J#HKdT4kfRpar6n--?Y6}RsH~ya6DC}+19G8Z( zJhIaeIavY%1!{-9w>+jG!9~@**1{0=1ob~~!(I@{4TWx9>+@%msIvXZYxi~Iy?Hl8 z^8z=gSr0B!ua>eFtU(m?p|ORyrf^OhqZRR8;Aypqc>-RG#}c#%-#1?=629l&fr{^G zI~eWa%(eI4;UVvf61b9K8G~WgA@_|k&yDZx^Ibu=s8+7}k3Y3E?Nz;-y$fUpBPMc1 zjHzxnToaiMvhU5`g%8>l6*Zb{jeNblSJ2rHrwzDIeuaz29Vsx!9yUdyRff6z)S+Y@ zT(Mr()wVg%S!~fc8s1H22KUEY{?>t#bxwa@Be!Ewqm-MSHGYUP<8v#~g958M9>lkg zVJB|Nj3vB{0qUgsGW7y+^i;M~^V5slSEGmLctu0$g7hoOEF%@@JsVXm=$k(pmNtgU zpR#>f#-3aptuv6nk?y?Z>^OS1p;GG(IW3q>qYqJ~6tojO716amqSN-Y=Ux^_2}@U5 z|17)J*ogd1?;|T|o_Gm-d?Jn2{ARSwW-ILm2n(umbJKpa3y3UJDS$Dj)LeYL^B*fF z&pLha;!&~Gz}TsbJrO-)c{aSX6#TJ^PE}Q6nN5ixT^@(SPzkUlB7!;RH}iTkAV~C1UVMeRs}gcF)(-V`tSbAQFIKESy(Cly7=rbuk;y-iNIM*=2FzSqFS=S!3Q&r*W2 zc}zWJL5Z%enC-s8Y=wPXJZ^`4gA4qT@|3A>L-a7z7Bj8+sj|b**SAhP z#9bI~u?C#xA8AH(&kKGyOIuKo!DRj{hO}cbN5y$ZcT4!?IZoy}4L#B8Pefmt z7a81am#A@$w+{YC|Hz_PP*yd_z7Zcw_;!5%&an5^YJpL3skk)4s>bE49z|#n|(+BrN${CK>!@ub`# z)8Qc|6ot!xSl@WU5MDb{#Fjm(sC{vz!3LjJu#VWo!b@ou&5s=(*|1w0RZD;Y?1+xn zQR-3Cv*|vjUlkuuZc_*uBF(OV(G(aD4Iy>N!z>k)^3{e_ls#hx>({Z&&0K7&pUnSo zDI(AgpDWuaXyn56|8j1;r;;q6&A&76c?U0CR~e&-*H(mWDU4Pz@W3Wk-cEAsH#apE zg;=59m3~6+DyFfu5*_xr2t;!TF}_UluPXNVW;)?BD8q3Bh$dc~4LobI%i`}wm9>QKCKh86XaUX-lnKcJYl{1?7<2dDF{^LyP|CRcrR%~~9- zVZ3>h+mC>6xXW(=SO&F*!N-vSG#OSPbF+Q+*(`n=P9ARd!VIy=&jyoE9SyrV17n+x z7CFk`SC$ME+0rvU*#c5fTxXswNc0n$WiCF4FJzJqerT>?KCS1zvNzqs^?#0}%h^#P ze#a#~*fY!h@p9N6^DrT+qsXKITSeRzjIFDRZIG>5AoT{PPdc?P;*P=Ps(htDEMdf_ z6iw)uG({$C_M*!C4tM-}+0MJmkg-=x0=c1rV&hXQFxAiL< z5(YT_4fcMDX^e3T@eRa-A(i2ZnPMHc6&7y;}+|5P9t(%J=@~fmQG6xLs#4 zhtG*oTRcd$>4cxHpxp}6E)iljAP?G7rTCz&W+|n>Xk+Dsd6RXerx%~|W(;e8bP5ml zOdtgyj>wJ7$U_27)7zIR>J_Z0-)6bR1lfeS^LG|FFSSoNc#%*pBAzZxMDbLGp?p(A zLJ8EyAr&OZlGqwAEMWjEj&_J!Eap_rF0%toYi#PCyz=uGs)aA zP&-s$Yf4&<7+Fr42G9^fFVVG!jZvP$sJcsz%vlEVyV(z2VH+`cNE?MPl6({7GL*-U z^PPn(8acBg`q@v0&O&$Ar8GKud6DalYSYdWOmBEJ6b2lDNc8CR?6X_S$NUx49xc8n z`In94ATq-7iI99+4$^DB!S(r46d@cWlZ_kG(GzO8>190eYjqRPV=QwbINMm()%RhD z$W=SWq>YOL&p^X_MKMriHm%Da5_Rwlh&uUt^oJP#l|3Y~ABNyPTxD!>#Qa4uI-I+O z(ZGNdZ5g|1ntm@CQ8VPk9~lOUb82irMau8=Y-zQ4PR!+|D3@LCW|%x}$svBnCXPzv zX}bQ+S1-Trvu1Ac5Vvzw75u8rS9Nk0RVbWcUZG`wQ))NSEqC?fBmR$r1RW9!FKY^Y z#rPZZote-MFf!X>jV$=!&`bWka?2DvqynE)vkkEhsTJLJ@jz5&hI)PKyOY*%=-T&Z z>($9pR#;^@9$tixjkR}$K%SLbD<>DX;@yVt6%g?@UzhikH*HK2L~y7G5IfV01#dhV zI=rW3b&z0~ifmr?g2Po8#~%O>s)h&pFg8iB1G5?nfb^{rSk|IVw;_qf@FHVpe}B5Mbd*{ehP zIl_%kCerWlp~Hdjv0}m%wvlB*k+6>?BDbrWa;v2^!!i*MiMG;_VXMA>O3Tej@qTFM~I$FE)J;rKtFVk0?ERX=ls}qhoU$3qpHav?BZxQ1 zz>z~!RGYQ81MY85rNe}-$oED3L4uI`-`F%K|PdG2ku6$O2aMfEEZ1!O3V z8$RgQzwj!pZO4>kJ^9L}Sg>TCc~`?Zd-ddvfE}^GK4`1_h+F!3aPj!PZ(>6^k*0cSn*q57pUJp9t{jw2LLTe%zf{GQP7D0J+U2A zggb62N5QueD*giN27 z?!qNNfjKMAM;4c?%Pc9e1jkH8UHDww@`nzOuDm_l79dD*FjP9yV-jjdeJ(_fe%Ew$(o5i z#G~Z!l3|T(pFU6o-~rwdCO7?=lTe1@9WLAJ6`@ypsx6ZZI2|^QEmio>c*wX_>Qn(2AXIT zA+6`q!GFjJpR^DJ8u>FZ_TNl8V58h)ev1hL=!yK7n9zgbne-C~PiUgmh7vpUgEz-t znT>ul5Xd`y)uxVDslC&K6oi>F66ADO&T4X`K>9e=jS^%rJ>->YXGRf%=k)oc43=ma zyd(}tit2*MhoF%jMxzqG6RN>^c9>~FvLU;em^>mWznJR@nm5caH4ciXC{O+-t{@2Ohi;X!oPu~EIJ=ASxSzpW@r$%jC{=cu2j zO<3s(u7Ow$?j1b@rm%9g;UD&hVJ;F?9~KM#dh`nte_Of98)b8WKU{h8G7|pX_wGR8 z(X(f=!otFju(5@JYkFj)q@+hjN6H36AIWTfL#V&hsT3C*X@eep77zS~L8pBU*$Ixz z&84JQ$&*dbe=WG*)g|`f{rleIV-5wyf5=F-s>K_`w{l>yel2*pqdBi2E2B!*t{5^b zpuhMGY#m@3n95ZLS&)sctIalQMLI!4XPRRQdVuY(e$$#MIUiI3!I>ml;l@slr zPHHyk(=xH>q*UY$$|jGO%q9igolkNXUIMA^ApNU?Zz0v2kxcL2uhAhD7#%RNt=~pF zA6%1tm~E3>$ba1iEz(;jvDDbL(Dr%5B)7z^lH;_2M&xscGFjI*7;dqJ34pNIe}XV1 zEIe$a-cC*-d6xh3j@)2u<%9U>Xkp+cWiIfZD7{`|HJo~NHOUTwWWGIBvc+`w^{Y?^ z;o=G6^mK6A?$V&qIrQLj1`Uc+OY1&-RnMhWOa4AIh*`9r!^w>zGzSE_2a*yKR^51U ztfjMbNh_}I@v-sCR^FU2D3Di-kk5@qbBD|Qmhd9;=AhQ)`50%n;Y3@le{NBV6Cs4_Fi6% zZQc|F1>UMY>ua)?>s_wNO;;*FU^Qiqjzhh5=GtwxlWUeYz8S|V&8O{XPlwK3xK+>K zMEi}oAdx#mc`ZsBl7)Vsd_+s2&UGA!7DmoyVK91!;_XBh#g|TpzzfR_LK9wxdwZ&6 z1ACNJooVO zA_Dt2`1sD;)j^3kb>JctcO({#ZJ(aW41e^G&e^{Cm4fSa z(JH3jbRsY|omlpf`Cs}Vkwx{Onomn;YbWEX{GdlcMeQ4} zdIt((33}SujMK=>$jGQLJj>SrJY=Qh?ae2A-L5g3E#5agJS=%uD(atfwHo4c!>G59 zZGRCE{9OrUH3(RPJB!-q5A*Wl6EGGr+l%uD{6+}?3xPz?QrR@Cr z#}Jf?h>9L>sfK${QBo>eTQjB!`lSPne@(qNuLXZ#9=?J=?-+Q!mE0M*(mO=fCWU=sMk=g(($7M!c{Bi8mrs5l&-MN|)+l5B;GJSrlk<0)S zGqWpk*y;8(;?y#SN+m2Ei^($~fmzQojF6=kU0^e*u(RD3<%=V@1$I(yI+(0Ao+nr0 zu+n{7C6YmubURz#FYGA%YcGFo>wqPoC9-+OuH|6kAXnCs3`DgiLiKyM*#1K7O2n2VN9cN6r=*iF7tZ<*$_$)yWQfvqoX6X|A%ZttbfR&`W^&U3@&%T{bN}da%>1* zD4%v9XH*x|CM49SWW0hBPp;!r46)9naTJC&<-vx{ZMo$6sH^I=6tWjXKMfyE`#oAt z&gr4M^9;^C`Z*f7a3(d*#`sxwJ+fksMEC)yNQyI~LgYUmu1E#J_Ai1kr2QTM81&|8 z0Q8eNt{{rAq0Y8F-$FN4(@SACRInn-8s#VrieURO4PLj>+zr;S6bcnB(nfP6GyU$P z2w@Ny2b@^FzuDPF2T6rwVw1e|_7zL6ME#59f00@70T}4I?JtJ+58*)p$->b0e1_x( zSEIYmInXQBm%?1p;iFXamqC}#D?0jK9mnKY&|s`M(8S8hqI!9DX0zTX379>?{}`iM z!0KiNX(CrDwjT7z5oFQ<*9~v7c=-4~G!3Yx@dw47)uG%Y`&;aO?LnO?09s^Dyr3V? z5eL_GW9{iZF`OIG8&seI*n=H|M>S*I=hkR?;L zDPyK-3uU52ZCYM?#T{1vO@g4=?;-{b#TybxI2atmk{^vB|W z3`?UzkRaA~Sf8^*)7u)ty7 z*ot+PH!pZ}+t=TJ1Xg6zeM23I5BrCjU$>x9XW)^Hs$+Fz!qPwZ43InbMSyEb<@gv+ zs1Vu1R5569U@T2kN4o`+k|dz8;^J^csGDC!+!`91L0cPW}c#3RG`^E1Mj5ogr{+2iP7@*Nf zHD=Z#<#muvSH{x|&F32suo4b!&|7q}r9gP3yb>?k@BIKA`i24pseL3e12pN+J<&!B zsY1{KBO%cEa)Y0J&-{fj$gstr7@Y3~MtOi~kD$+Ge=AhcN5G22XMHyQJpcs-Y)M09 z_sWJ2<6VrLU0~Ykyc#qEMa`~148|Ji%jA}@CEdWw$62{9jX*`}pLL-NsKCLeE8-4; zKW0*shssWEcZ}}7maJ^Fw^l3Z3@R*5G*__bQIGdo=26nY-Hstfw<&`09Fhbd0zw^8 z^-jozQgLkGSL$>!8p$~D9~V#@2m+53WqxP>Ew+Gez6=!l^l_TU0reh=O~>t)u2m&l z+74Z)xU;f`VY;d+p`J8PqUXQc`P;4(JVt_KesEY}{Pzz5mJ(3hpuJoZ! zOc9xhDaE?RT}&ed$uknY5aVK)L@Cz4hUpi-GI?->Kt=Lyu@%3)0x+ujUI<&ZTEErX z!<&wg@uQs#in3D5#@zmwpQO05Z(eDAOM87x&%tVIfbC9sIZ>$6!{V|%6>f+8qTtJl z*5t|$&wmK|H&0(i1N@<0WVe3z4<)cA>5xzq-1D;Zr%HM;%|gD9`K z(4pz*_?QW>=m|_ZrdnEB=?*^_acEJ*5AkOHb*O)@5`cyWe8F=b`JjI%FTjLQ=9;7a zjO>m}OG`TwMbFx#)RfZf>+6&9N_iXLwEq2&T=DG#j4!@7m-;AZ>B85LI80Ei6{sskL-NOc@Pk)#WMRrIaA~NVvFtydx7p+NM+TfN4WuJfC z#3m-1m1x&UFXI=r7SpNZX|8mKmnh{(=%Xl0a-rIx2&oOHGyK6g;3xZAVAq57!yj?~ zRi{iq{}i60(lP1OGG>`vs=m4p4ON#(V65P zbKpAof6x0FTL?sdA2Z|+$t@B@LKSL{l@O5zB9n375i;wvBLceAx#r zC;%X!Y8-)L763#vP{4cCj!f!~BuO6C>`w`zdzi}SU#${d%%xL@Wb}K`5Zy3jwJ<#KHd`4WR#J&6Uq@xi5y>ZfjBT+A3?Nt!;R1>a@^2 zbHid#7`2rstvA=(1{^EHvu7b*4=fu5rmUy?&z6AKqh`v5 zub;{!uET(fSI?FrZCBYW-dyc9_*56ooUydsnBdX)?Uz-wd+u6q4|0~fOwlK&2-*0i z97YnenhjT_Qe)VpFmaUc*3Q;z`h8YgW!ADB&l@g(V$1hb2i3}Gtc`~o6Sx+ zcj<=rtTt?ze+qNfZ<_p6ttH2c+oB(9?_#EV%6zuEca5HBiaf3S9$=e3e7(LOwg4a8 zuayXU27BGInlTXH2%?$R1c!nZ^W4YY6fFQf6_9A9-~O+N71AiKKr*R326i?Hm#V%s zc(`udHsPm868^MHMLAfeF_=9uxI2lRD48-hEm?5i>lGtWCSVfxVJ~k67 z=oY?1TxZvv;k2{r^v;8f?nS!K6$g{e532lN=CBe2k7Ek6tMREkH7D4JoV)b=#aW0! z$$$g$%o7^oXLf%2OG*hD2Ifo)T?M!_0})!KyN1YnrY()6gm!h6;ckMTtlb1pDl=Z& zOnnI*bS0mz&M?zVkm>avaN)OJsF^!wX2XFM-`VWBt+LQ~mKbiAE_9{rW=cLa589@Q1T zG!A#`cmOG)L-J{DIhb?5dy+MrTO>O-uo-?gS7WL5op{o(T#cl8_vQZD ziW|A$r??Ma3w{nBp$HujG>(SS8289kc493qEV-fj0qReo&|(6#gdS439tbObDXveM zZ(v{I!n!V&Js$LWb2(LZ%!k{3 zgo{-vWep>>bKW=Zgir61_}_W46pV2A9xP!1HH=ir+R$SH&qcXsg032O0zaHY3pBCCeoj^e{U%VFQIEu)Z zjcw7=aw#!%y*Iw@6>8|e_2`QighNZ4r4 z(g_4k>mwh(plOv$KFT@HO|n8OOIQ(!=)3m{&&RX)`a+|}iv`0Ynf5wTa4>WI|8;zLt?FC-B_x}zlF0ejEC=!hMuU?GhSL98@aF)!E0IeW+ARv{)Tw+a|W@; zf#2>I70hF}<(T8wnw;HPc}@=4>PJ>=YGv6e~8`<#kK`NfKrBm~LA1V?;MAKxbJg;YD z_J()}ZuR1>rZQ3=0cY-8PztYGmdhST^8Z8oe`8}w{(z@spc{U>thBisOPF`8 z%Wk$)1iOcX_x0@bH%Ml&gn*GR)3Y&Gq6XbY-k)zPW`9K$1HgrTj;81Nqp71dDTQ%59t6k6H9kY7yOj%TD!-nxD2s zV%eR@WfSlT2PXma_UYq}(f2eCG_LGQX6vt& zuvH>0BJ$qcoHDGlY39nkqQLsJl2#{CI{;UiMs-%&|5TqP#Sw!l9BfrJL$Q*Oc znuIHp+_`?C(aqcRbjJ4%Mkcj?rz%zOnC%+LFY;{ON5|k|w5x14Vw7cWLx=fL_nX{B zFs>+-)QqRkPX5&42*1xcS16(2^-0qLd$vZ!wjRf-?S2-=7lUb|9+7WE#x!#WDWr9Y zByDqE^+A^(zEFMKff;)79ntXFsRw$|PZ&MUZhi zv6WrhUD9wO=dh2y?D+`;js0|$=oxG>3-SI0?=a{RmpuQ=Jia~2KaA|Y8 zY$~c0*Y`(mN_6el!S?T$`FQp}7^(kNrUZ(RNOs=>v`d%gd@mOqC8BY2a!ktM{exQ| zv7e=Fu)16Ry{2D_>(m%3Emb@ewCP@)F!;qLh2SjwxK%Mdwq&7eM($4jBa>F;gglqm zdq15#k%x*W0E6_1j7`o#{TgFAS@RL6G<= z=Eg<1r^Qd|rjrM?{Vkklhk?T$K6cLFw-;x)M9$u4OUG#%-3_&e556sp`5s-k`7Gw_ z>|73s7PSuVk`v(ibx<}r4yXF{h08k^S~c-j(5&Ceou60%$a@K$w`r$5*oE)QTPe!M zjU%1w`YE0K5<-<#z0c&eytZ|POUwgU;TL}LlF@Y`(jFpTsm?NFK`|x#y7FWmcd{3r zjqt()zhry9iWLEUXB3e^?iLAmyO{RGG@N~HqgjOD?K&dAv9BjF?RCvtJUYs;;A3Z& zcw-50R`W=lQ-OXEaXRNhl=Wiok-fewiB%$%RA8p%if7UaNV(_aF z>UzeOHLlH(jEb%;-a3!f$$K|f^|q)mmmm!_NjJQsM5|G3g(5x*9Gq|a;<~+F8kZe* zuf)ao|Ak;Z>&?~*F`Q) zvJ`G4nd?Y_6-~O6-7(I{6w^>P+*I^aaGemB`+hsf2%}C%#6`p511l>0QT8+Jm+~;& z$WvmXR#MgJk>C(rzcg3*>)S!Vv#r~pFV$Q%+Q*SC>7SFR_NoUTOq}}N8P_&e54lyqg-QzIIsi+I*0dIoj$ZYVB8;{KISk+N0Tg(hI2v zRAqoAm8^qW7Pi^7-hG>Oj~!0LzntFh?=^(O5>iSCi$0X(fTJ+CE|xkgP`UYW?`Mjp z5f0DlVOCLKUD!ppK6;?wkwCfFcW)1X)`&%ABHm&4c;udD+p?CMLMafv48Xdv27F>G ztx_2xlsDu^a*y4(5%fAa;wl+tU*yx~2wFumyvz#_j|Gw;Q9f-51yR=K#pPt z5!zG^q-fBA(=sXK3GBfb8o;S{5hBI`x<;zl$)rk5HLB~Pw$+w4{y8n1?Q@DD#Qm9$M-xLLTy!ykhcJ za*xzGq@I&nFd>JuVL!)$Iq{uqWk*BHwn%y(!|EK;Ld?78G8}ZD<(m*s)@UNuDkz52 ztn56$%SrkvQ}Ss>yVmJ%iGGgP0C{el4TLqjT>P{my?k?a^fii>Vi<2tHX5a-^5spU zpj^7bF(LDrJ_B0a>BNTcqRg5t%^BZT7s*=f<__cqzK7mCqPJKd%6qkx%1M*X-W3w9 zviEdNML9at^>x00z7QP~VFaJZqX)ZK@Cx=G4@L`ReIsc#RH4SUmeS^hZXnzXGh5U| zh{n>V$&;ENl{Vh3`|*`_RK!dfA&yc=Jr38vgggq1idhA11@76N!Er<`a24MpRj{hj zChHGO(x*~nx3~i?ds@k7#~H5h_vxV_vdeeO4rHT4JZp+9QID(?yFaWAXlRlWx+)@s z(j}B4N#s=ZUT!e&XStpghG>mbhuiF32QS`}3{2$9e1!X4OX`t-989X?=Zc%bX3K+Y za8&bW%&m#(2aUIsjuHb;sYfVZK7QRIQ1-ppWobI7)Ui%s;&EuiH8)(F zmrZbUGC{A2HCD_nD$fL^{nrk-1g9H+L2!G9kwC9#u-*SOYEr<@0&R z-Qc~!^;0kAHj11%&;vf0vx!x!*iR`$KV+ygEz zx0xa4rj6X(ERXI6J_fgjx;)X1XTfWII*s2c!9aGs*6QJ)UVWJ;VreqO-|HegQr5%Z zU46N^7Z=u3e7PB`pSNjniAOm6lpy>^jCFMxEm@L2pe%<0RtXYLw8(sWPuvG{)t34u z;1M7EvroMY7R7Q^=$WhjyzBAu839 z{>j&qY+ffrKNs1PqilBfwAjiNPUgjcI)Q1LF1ZJPisw5(4EUlVxHi#ZQ3yK7-nFhC z=3s~6!cP{G+_$Ck)9dblicr|DN%5y{`;pOtFZnFc4n z4`!e!=eZB{_?R*67B`?(6X~Ty18Z|u9YdJ?)?`4KN2;XHknI=H;LS+=lwuZ zed3vl{%_{&S7!J18j$`X2akwr%o`cVrm0(t+_t~4o4JW>wM5=&$;rr-?@G2-mbN*0 zP`N0E3646dZNAv+GK!t;pClh9T$@uGA`X7)b$E)UM_Zv{O-eIY5SMU|_Hw<-?s^|< z?b&75n%(G|83sGwolY3W#)>2R&S8c*Ya223=mP`K#@#KEKDt+fbPHu?9;QGI;T;-( z9yV20FT?7FD1sKZ?6#iu!G*Ud3%_d(7A&W>KO{HQ$77=T;Go>z)4N2`VxxPg!HUr!ad245qu39r+oYMMtx!X9_J=r9l#oOn+Ra z(wn>2^6IknLBrLj4Tnc|n(r_?T=|bybzD26oLbzI-1`gRdb=&F%x&knU80M{7CZHQ z)L7+jsFXm;DN1g9Yb~v%L^CDtLAiY;_aZipr6Hsuhrt58ZbV!AT0l#woNTyqL&B=z z*_sA3R{WKZujk2YmhNCDX`Mt$kecb2&w7!@K*$HaOrO|K6iMi3rHRW(1u@xap`PpM z***`hlx8G@=uX_DokgS!#Uf|(Jsn^GZ5|_Z5nAs-agrG~F(|nrl5~Z3Yu%p1!72Qw zLBQh;>Z7hZ|ki zm|#U{`_un%Ka<#+NuW3AgoIr&OkTteO0qK41cZ(Ku)Suv51;l$@-I9&=}4VoHE(Bt zhsBM}&gzGz1+TkO#OFfNumqBSMq)MGwkxhpw_Gl*Z|D0S#{!+?4ySK!nGk$Lc5uFD zM##j|oV81%Ek+Fu9<7UO`-i)_n6d6nvrg+*jVi-U)cVskJ@y6&*hj6y#uyHX#oJuF zt1c(wF!oWGg}2z-?v{7tJ-WWgenY7Z=)e=gI&`&nFItTKhV>o~4CoM)McV4l@2mTz z*9RSK=GytBrNp7DiBBy7ch?2?s0BVuG(8ilpq;i5yjzCdTwZ-A-|eq-n(PfR=4Wne z&{ToT;f=RQGv+(D(G#q-yk7qKX-~sKe0cChKE`|Q!?J4A+nE|pfu}HdL3%}SnB$GF zuWtZcTeE(C-;d*hS8%GV?W#f2pmFJP!OQM?)%_->`Ex}T^HY+hgRvB&fdu@nZHCHa zvHqFXSBQ_{cjtHe21bGlo{QK{+}TEz+}dxNy7!xlvM0rBftKSH0n-u8%0LH`NUsNX z=T;GSdhQqDsD~>wE<{hAV!IE0Z0e<0?N*441;eMCMuOABjsrh%x7YTzgmKcFtV9aJ z&yQ*3F0v$x`#;u5zSSA|I}jA{nrhvDaYMaHDm;1EEHZj-_pq6&A?w#^QKO@ zBx1J0f{m``f`@rvuA%NAI_EhHxWV@EwDndq!=ovYtl9NJgVkgByq}YS)h4b!O+uw7 zXp;#(WQKL<@h&d$)wKI@%do6IE%I(tmEKVq=c`_W%yOHi`Z^JyvSGZ`P$z;<{VJgtmP55R0w!f#+dEOR;a z79eeXw+n8w{aaIo#`{gJ^J2k3?$GIl0%lr{F|k6+n1|Nx>f$rO2`Z4m>v28z+9ruM z^Kd=MI3CoWEDh{#H>M$w+jRKwu^}Srq)R1WgW-P@> z+n)2=3Hj!@9}TBZ2l;>-o^Tj!U!hWC@p9%@puYa_OI(|fGE zAGf71@Xo&5F0p7E`ecz~>(~E?W4EH|s;Bc}Gbtfo<8D#!!7WO(0(BNQ7CcKkXzzY)b%D#aiJ!Krrt8W$wM@>OOw*$?0>iEE@t zH-_X=-6d~Vs8Fr->`R?y_lg?3rB>w?ce8oQeVpQbW{({Hi)$|dyX+&X{!X)l17x3q z1w#$zaKFSvM4+0Xn}8O4%m3`Pvtp^q5LzZvF!1P$T;XQ-=yAiB5laDRy z5EPp_n}v>F6$!7KG=%}+z$&gjT!s-a@U>Z#{khBjORk^7Z`^5X|6;`G z7pZzWI=0#4vIvA>52Yr%)znPwyfeSu+fTm&X}*W^gKmN=_mP$=t`4_8S^3-nS(x6<_u1p!(fp-Ki}AbiJu?!Mw1{&9 zuE8)!qp!92_fY&n=|_;CH1!yAru7Goi|@=^G{yM&DH|E>AL?)nAI8#-*2d3b8p$7< z%Jt8O2PL)L>2GwKE$%_DDYu0VZ`Ted`}sAfP!0Qr#@S7Iry4m;8Y25HM_jPBy&D$1 z2ZQA&bXl}w(Iz)9G|Qh)hnafH7ZsU1+OF?--1=&)xky*}sfE|D5pg~E3-T6HMWK;V zX`Z|tSZ4h(ceUFH#KUOho`uT+FUqb6srt9tpIyr9=6Be7@D3c;R*JLKKq5DfPJ%(I zB-+)$V|{-Xjum|YhOL>Y8xPkXC13fxFDt%usabHHdpSf#d77XR+U!lcDJ2vNbHM@< z1dtGH%>$vJ9w z+_1@=nCPE)5{ifNcg_~y$o1j~-=tCQv}!B9P|pJ>IK+n#*!-`p$^hcdi=@eOR^Uj4 zP5MB4Y=*&WCf(9h;26h*J2mKc>I}+?3IJ3sVhV-eg&b0^K@#`t)0ati^P-|Yt!*#a znT;tRnJ)vgpZpJr&~O0~LAm7L{>!9ZZ*U>DvO03G>P(41b=>EPQoSa|!T!kyNCmp} z<`4fn2FjcVVxa!;k9dEuxClK!6@|{|#c9f>>&1u{eYnZ?;B=kgO=D?whU#p~7`mF_ zJxC^fkj(TyMESe=@(c+ACAs(SnSg+&GEg;<4|o-&#Ef7w4Dh5kGQvvs%q`x ze-V+CkPxN2L+Ng$8>B%Rq@=q`L_$)!ySqW@kd)qZ$ELgQ+MaXX!|^@u{oQ;2xnnSf zo4xVbYt1#En)5TikG_);xr$>cm`lY4?@=Dj)+qZwZwY84<-s-j;7AiG1w>0%b6@2V zLy4M|AwT#Kcu8~$a{wjDtK|9sZs|dT{xe=O;HZ&5!I>m*0aA?|3t#S93kIPj1UPXjS9Kuw1>@11Sje5?LCwN=p8y*+Wz;GCA)D|tq8wC zHUdNg5R&T>xsB(+11lge)9*xNgaSAi;K&WjnX3F5q@1*KKcgD3}6NfQRXiV`N7VaB*?biH@KI z&@CdU-aY9fEktRoioL?pmZKDYnFN;C2m(h?1)8-C(DL3Lzi+w7fh$)*C(3=0^fP1w zTGjxNsOGOE`%&uyiMgj~z+|ql$u(U(>`)9G8)TlC*>!ud5?=FRNhs()!hxE4#Lws@zXq)L4MW6rgw*|5hV7`lx$ic%=5{rj)q!OWJC&J0UsDp{^K<|nM ztLcLZ=21yh4xl3`@t6R=K!n(}j@;ZNV0w85Bk+$8vFX!9IVFqQ5#{FXToq_w=|Q+-M&Hi~{*b z!3Kdmsmo5q;dF~AZ*=WK&GBr5;7`2YyUJw6+{zLD{=#3~J_i`3`^LC~=KgaXP|yH0 z8GjH}RMLkmVe4ac9@xnr2n z3L|6<_+6<*40xIY0H?6+0_MXH9wq;-(v zY0k0P{%-y8VBi@#G$ek0^0#r)gkXH%Wt;+`rxhtZJw10#p@>g2B9opbSoaa{NVxlG z_FxiK%j0dXeZP?|-5{B&q zf!-lCA42j6Puq~FXGrm?UtJ^(8`4Uj7|A`RMd9W3Sz1JskTf!Qzs6)l|?8H>yc1|P@9CaFjRr-nbxDf9~>}l zV-$_SzT@j}(1Xrzxk0t$-%k7iB*qLC;Hc6N4B|7ajz zSQiNw0k)bf(S9@c1(omO8H#hDhs?0s2t_K|+=)haRP)aqok9PI| zsaJmKBSQS-QC^=s_Kq5mo2e zr}wzh0kCvC4B$#j?q7Nh*4!99nP{2Aso75f0kkW!a3%U=h4u34ezON|(d1ywy!&HK zlj7BSgqEB42_Cn%jBMs-?vqY6tOI6-AglT>mUW)f^Y9Ge#eH$FmVTD*UYoT}H(iDl zLy(ZeK=bK6Zp$t8XqGg9YpRl?^e}hOcE5jpj#Bn~AQ987!K-`T@vy#ohWL6P0B{SO zJnv2!6Is^a&fP5x%&ROkR&9X<@)y}DU{<9OeRt>Exp%zo1f-C|EX#DJ`F_$k2+Eec zh5>TZ(uY})l{f11e_we+pcno+gv@=upf#RPDn3d%F!rrfQJgxuqolaWQC?)li^v$2A?+0&^U0iS>$^R;sNKCFowKOe zf!Sj=w?X&Bv*V}zwi031^(8^Yjc*f8Yej2*2UXLq{sF2s(zSkjGkfOW%Efa z^Q}j-z#U3zH_gMT=JEM7^?XfLXvIuH`T4e2qKuRDq`m$`!`}I7?aRuhh#UxvT03FR zBIQbrAb>u2m%urR;y^Mak$grwYNdiCu*?Fms1Vkd9U*{puK9ZCEpDe}!4I{Sb#f8X~Agra%FB71#fDNj~gE&s3| z>ail!7TIJpVLin=h_ooSpp2n@HB`-m<(g#o8Q<}nRj~Gj2Ss+Ietu-%f1#-BH_hLk zZ>(csct^f;{f!^}QWQMrxNv+|-(YUYxPP1RreVGF)~ecJ!oooHjFWuJr636cmkv8b z?5;F3S`iApr7UM5-H4z>b6=cV+hnkXI?bBwid$+8botDT+bOO&4~Xnz_48Z&sfOPba_xouEj0Ej1b*8tqtxX(O(eADqoY$^|&Cs$) zZE=lEr0<=JrGEuj<_i5u&ilwjx<(gu832{JG+8n| z5{KUVy3sWQutfI6S2!*sTQ?K z=G}NLxn?BlYZuILE=$BVqq-R10Oqhh+3o6!1m%<;RMm9X&eVEhk$I}uVKankMa9iU z)8C!B!$6dtys%qeaBMuryBw5BVTACh)#huICEWOzt##NPybM}Xl&M|@b3;5d1fL}5 zi9F!um##G0VYO`&8Y+k<`8V6GkR32KAt-myACPKG+;5O-a!2V~hSFe)Ibl<#h2B7} z0>*As%gy%??AAJjYpfE+08XW;isV{GPO(ubYv~Wtv1(CorYCynw}l=27zx8T>*GRyIGs7n0@RBcvG=m;V7 zyqY`5;FSZ#A=iW6b1O(s-mjE$Ix5!c*_3amBBZP3yEvm?YfgJ+6r(C6 zx!l} z#btH3>2`ghx<9>z!5vi+>JFGXckRxb(|N3d2Dgy1-X209q?Z^lB!zcO5xipeJg@6Ft zz+VaD8zOlQLN<`*!Xm|kA;LEuuZXX<{MpuGLqK+fK0PDzdG2JtV=9tdYS+Iq=%O|o z%hSq(vPjx-Y5j!Ic};N3^@txsL-0dn+3|vQ&C#UNMTM-DzBX~|n+4n8Qtq9M9gmg- z3abh!>vx5-ZU%&m7fF-nZ`fgvcN5oHj=IS;o)X%ZST+eB)>Z*IEbZ%qs8Txa#uirl z5ZlHZ0HW2oL5KqP@!A21JJ(2d2{y0(bGs-fT~LM5rOwo}Q4oF6iXOf+$%ts&!g6=@ zJeOrN+Uo(C&HSf?&SATCb2*-kbo?yq_w|5)NEWRed|k&~KMmBDr`CrpnY$CslHF(f zZ!*G~Bw%$N1J6mg8#j<8L{o@*wCvWVMR_!r@#{l@71j=~UIq_e!bLUqfAG^AYIGO$ zeic%}!{vqDNnNnZ`3ILTtGCQ+J6m`0@(i>x2`N)Hgp5hb6pUg_npg}r-p3`1Eo2C4 z^wFRm-b_@UyN1*b{HgOQXSF5~P$u;7n01aO>Uj~5r<~V)V`dFd6C}4WM_SIKW`?+yDDu%zHRMp?1RAMFa^}SPo92n|c+p9EL zL7t#d@J1nC=@4ADF`zsP+J_rwwO=Bh)Xh!CX!SZ?=pSjaTxxXG#pYsO1tBf?p?6^2 zDNe>smi8e7S>oN=xen{&$I#i&!~-G}HSV+V-Oko*&JIl$x)LqQx47mT*>x29-q3(2 z_Csu4RBEX17c_P?V;XT`(rigrVE(aBgMwZoktYR8`rxs(Z3@Tj=1IdF%dF51+E%UY z*uUKVc+Ng1F}Q1$(4i>}F`tPDccb8gbW7InFbu zcVD$z%N$utywr`*vy;H{NM_##W4@(j=X;!VI*5UGm?czIKp`+}=k z+co((Z^ST(Vy#bObt}XDwO)m_3cLU!QjSF57vUjh8NNWKP{$_$Z>qq6N4o(h7x zQzk}de>RPNVNtMe-3#6v%Qb}U^$s!@eeM0WtS2^a^fvZf4@6VNgqK4*YJi1eIKR?} zCz-hC-P-vbz!zTxAlEro4XKRjO%_{vP!bH-rti(aJg@g^IVr-g8VSr9BjF`BJrK|GtjA>oWC1G z5)hT8)1!2U{fy_RGkeUyQoO0wehDm{=HN}+>iX+dFXi+5V&3M;T5>}GA2P-F_a1a&kCP;p}EeC*Mx(y75m8N8jRjHrSLjaLNl3)Q_aX>M;$59do0&{R{1 zg4^&>K|dKdb_h{*y@)*pfJ#sxdm_2%nmvAmGqE3a*D``6*0|rK@~m`fluIqyG0s@k zCG3BsdcO6+5NX!c5ye_e?|cl=*I+8Xx!Fm5%`Smu_c*-y_1Vw5%b%Wi?cv=4rJp7P zklDZQh1c_t0*jxSfV`vbWO3!JyV-eCkLHqm+kw#VtK0J~IobKW+E(}Gt7n?0xP4&G zV~;riw8ALR*LHh)?!aKL)+CF|*u*tI$^DGPae=h7gYU-h{f~e|+R>aW{Ebr#Ty87H zA2L0iVLX?J3qt?`p;5@R=?L+O^kZ5@BVAV#i<0Lfsy~XN#TjybeQ9q)4vIR|X z9YVy^X>C|ACu;E&fT(@iT_{(j>l-J3bbfoo>!fEkN5<>;CkDj1RW5(IwXx83##vwj zE;pG+QIVKbYQ5|QEeTa&ar4%7MVP1G48=}Q75G@Tl9G# zftfMqdZ!Kd%EbNN!1dasBx~>bX&ous!OX~EyHy*lBY5kvenU#1r`Y+Y=9?~<)@FP4 z7Q#d%xy|!Te%aQ=nJN79*2O}W2?&R?E;8X96;fJ9zAgf=8+~kd7j|K-6Is;!m}VOB z<My{jLS=VR@1O)m*!km@&gw5WQVwU@g9PHP9$bTJcjD9qou&v)uz9KN&c zF!RA>%DuV&c5NrhW0K;UAn9V?ME}GtN4y&>K;+}yBCJIPni|cPGC+Dx_i^CsYtnSl z@xAT+4pP|$l8OBSH-k7-nY-rHAHL(3PE3h?n&XMz|ZY` z15wu;ryezS?kev^X3A~t3(GV!ttU7)v*`fUlc~}=^|~dSF1B9`W+cwuR&2XzyEG2L z+4omtH?Sv8vaeBJUQ}9J_#e|vTkV97b;L=*&TZM=x#N~Hmck7S*Xrt!fdhpqwCz?5 z29Zn(P}4I*sCWkF@oOsk8Mvs;OjB$ahCX39FQ?mK#!^~uF=+zDyu7Tg5!Lpb{s7lS z#F!7^WuJJ5O!r&68|t%n1c-v#T~hP##_~()`x(oeQgEN0TPZXPf&T-|HizY#&La9z zSYrirD{=6;;8mNAyT>>~$=t8WaW4dTf)?gBLIyd{2s0%w@@pOB_)NZrz8IbR6>}*T za%KuJQPRmX{enIA3x%zxWmGgXn)(>jOet3}i_;fO8r$b`T!}Qf(Sg=zCn6VDYqm?G zyxp0zPfey|?lhF);*k-G&g;EuG|@{QIa!%+ zLbd&f43&(X+IZx{$8<)Q8>)`wCk%2ieK8DV$sydE!Y93cyJdrMoYn;x&E47@xm6Al z1duAF{{hgt@OVcP?w38jzeGs^s+@_ zXOhvw3-p1G)*CTGWQ=JSKHH{KO%B4!UdZQ_W9!}zU!C)Hp4nuCN}6$cXJF{I9JcaD z+ihaRnZJqBD+P`nMIB&H3-2E<>-NeRxN-a{0x5y3DMTVyE!ENH&b}V!8^nY3>f0R8s`1fQOLjdS&)J7hr8QWd7;M?LA44WnGBd#cLWI)6)CrS5;4ZfFpd`d zDMYS@SDT|zem1d;k5U2t5vA<%>8i(L%h1?+uW+I3(TVIeD8;&T2s@j#;)myl7F(Qk zN2Y=2+LoOIvxeTDVKnQ{*!<#kR{(Z28u};OF`mDeTA~-*QW~^ci;aZRHI#d}@4$&2Yh*OHPjs%c`;o;Hk$sQMF*w zL@vN_;t(!GBKiK+u9MqP!25@IE?6k8(2D)sX;M8G&-hP3mjlN#(SjSxQOa;!@r&&@ z9qy~704z@SBtIxj@v`4C=zwy>TVMwjg7yJX#sR2L=1ZEo#%Qs>*Ok@LYI(VjvUar5 zfy;pkLs%kZr3mb=J(~C$zR=NyhMn8${jg7I3&`g=xZqzJ#e}BBJoOs3@$i@`oSo&- zE#BGT2~b7K<{@5e;~Y8d0i~XJ@Ey)hnN}}MIAEcduH@f_;MrZ`-*6JcB%yFwXMYuP zr-sqgnXe$(Z4VPl>b*0CZ-gxQcQBtbs$br9FWlI$A1bVt?Mbk}Xu*$T3qfOVjn~0Hz&c#_2kY zTQGKI7>uHIyw5C+b(uGbMZk)Zvv?s8|C?m7NuDcQZ{?mQaL8&U4#F8{(9N zRO%;HjGXF`^E(jyy$fM@WPhiVV4)KVM-+>Nzx84WSkv`m2oM`*WXBwnC~Q(=xx{VC zNOupkauy96q&(B8`= z!^zg%?h4K4%M#C(5W1J_mhDp5fZ7Aw!rgD@7zYz`{dx<#gP&5!JrA(uUb}b&-rWKP$7(@yHRbo5;q<)y zl7_`5Z_e*$e}nhJhE^Eg>m*czda--6AKI30nYC8lhXSjllhQi3g6MeOP(9Sd#C9#+6D^b0nO*1 z0Gw65B(aap87e!H!s9{K36f2w`h~NOlfWcV?6Gnt7ZpugA-i3K(Jlui#f68~-AX8h zVPgddfqu%^>Vhe$o8JLuGu7Vny2gwW%%gl>4I$sS#D~*Zi758 z=ncTE>@KCdHN~1!HJX;DrLD(i@i0U(Z9DG40&svkT_^i|e8TmvnK%UHerX#~TH=?$VcG0RV72tSS3c zWu7#KkD*Ejtsj%%fWm2L0groxdD!Mwmg+_)<7@u$czEBtxVVD$R+oJDPD#(`Uj-_( z^<+WB^2HD=pb(^<37a`|sf;Kf*L;w|do-r$=x|^8X%$+Tqwf1&=QFG|4ZJN|q3!+5 zPqE5Vx7UP~D_+NM$54`UTXRoqHC&K@I>%r@&F(?k7Z9Jgga^bY^4!Jm%!=#4$Jljb z-z+s_X`Lk1ORGO9l+bHc0UVgw60c@tGG%!3-$8Xe9{*#L4kW(;9&?HrlDr^X-YM|ZwyaIA`D_O-(Y&TY5{@D1 ziZ1^`WUhUK07sGHH3P4hkIEkp!e;hClPpAhz2922#^~_KDjj3#t;KlaJ@-EY!u@^OW*~{-;J2= z>w^NX+`Ww6YjpvMTq77!=HG&ISY$wQU+_cHAFT2(TSr{NC=zECp&;g$X?mDaOXUO0 zTJ?mOLS$(9`VS+2DLMYhF9_5_AS2wDl)uoNKS>g7NEoL>f1Ca1?os-=GEVt7F00XWd--*jabAN~IYr2eI& z^8Eh>kIokR`~G~M5(A1}faaJ+qvP)A{FO)}pu`MeFXY@BGhs>o9nl4Zc!#119+;ZR z-`gLPJ|G~V29UMXgzzhXi12Q4DGG3ij?GFDz&*r9MXkiaBIBsNeH%Moq77m+^TK$A z_Rz?{n3w?YT$0DQ{w@!Uts?cV^Ana1ltzt3Jht}?fWuZ}OAGD;aqb*v2GJls&zp-n zsIfX5gZ{z68h}<(Pv&wc0>H76oZmqfZ-FO}@f->#_rcRuCWJ=!js*m1(Y*m_@Fpr^ zIRNVt1a#NpSL-pq(iqOE>Uym4dL^)!>7#qEK2ol=(uPTc3TFLnZ!8G`x5)%#A4G0? zEP$4)RyQ`>UW?9C4kWV6?Fa&8_!+|v6`JWg5HM$gO)T{Ku$$0wsV#uwP)w>l5V;s4 z&~X9zE=WP61A|0Rdsqei8fP^4h-37K5lB660)FKykz1i0G=5Fq3`)UW_<>+KVxl1G07RuN|ahez^- z@-OjRSQ3bM?w3&Fj>HLc58nd}9xo<6ye-H3a=71ITmooJl0^7H!c8EQ6ip;0ZxLfo zmYt|Vl^nw#WBP@5MuuFT9DB>}3@lL;0@HE9E z@`ksy1ybGUSZ=<7%gN11|6LFBAAekS z1z%+z(|pA+81vvtAXA$m6fjLat&{lrfa?d|#sn^0IU3IGfsaC7q9bHALq(guy8i9` z|4fqq8O*aG&}cHM*l%C=(48QjCC~+wtMt`r27doPw3ucO^qTDBC8~$__37EiPW{g9 zw+^C!oMoSV@3^J8`Yzqiccu?0y+4d&L;>87u`XE&;IfOaxM@653uDsYyENEacymF7d8GwSk=KL zw}Uiy#NS06qg7@R*h=-8SI(aJh_%T9qxp?ut<~IxBbt97SBeTRN(tOQ{fh_#(j}I) z+l?P~3G@g75+AYUa1la;XQ$ia%)yvj8t9f*Q)PM$2{^U1{QL_*-G(KAUB<@5ti6OA zMECKP$K>}x{=1DozUAM!en`azAP_D+Jw0v4Uxk(+>l@=Ala!QHRm=H;!( z_}lhmDIg@i_|hL+KJCA%9{vS621dh02~y(hw6t$zl3A4-a^LWIVhxXh#6q!X$nz!n znNp*D0GG2m(ukaTj^%RzEqM(J^7)6!1p?6kuZ_SBh=1_5iZGG$y|}mnz;FyCs0;)s zPMIHQAkOmxMH>?ZDutLj>D!G7Oh%trt!AfiSl_P(5C!;09RxxtpMvY5A9^*BJTowZ zj(@)XLG{3CB#F>Y0rYmX!l)lzBS+2w5F9nFHta<=2gKN~UZ}xJQOG83wog2HM&LUX3Lv z2U1`|DgE`GOd?w~g-n9898ti0p=xOgH8z@ycShF#qnXoNAcIoI%F5~uJT)-ke;`LC znw|rSt3Ef761pGwV0)86_accyN3?zGeJqaj0K)!b-k}i#_5!vy91p;LB0mHXK*nO! z@KZ!26%@Tr)6`6@b*h5X^6mM;`DHAi2H%7&@94+|E&pat@bf=IU8BjsP};CS41Pdp z=P^MC3n4<3o*5>}0W|F1|J4V0flf4q2Qv|55J^)6I&5FC^-_kG+fMUPk!U3z$UOW$ z`WU@%yio6eWh`8;nu zVgVsTKeM?sPhRtcpHmasjiI|0?duYym8OU20Psk7)}3l+5vUovN)K#CSSp+6n%_^Z z-(zqwYA0Biw%po`@&W8?g~$0U-_~oDmdia6&7{P*lM4VUeGe4YT^d+_PByKVimU~` z)WHJa%w%T7^o{l+v)L@Ko+chnE?7xd(tD}Fsr|?XY-#lYeH8oO^D}0m*=Z#e!@h{CfP4i zs`f>qTzbH~-17qZYVX2x<}YE@A4j%5FM3$VhJV#ZnK6y;Z5jmr4}DG>=m;1eAZ9Wo zw@BDu_kqN9fg5C2!1`wwqU5;Xn7%;-%9#&W(5#CsGxOCvADLMDj9p`e%)RQ zqy(>}qt5le*4oE=obK9FLR-F@)N!d9FV>{=yo&-VW}WyCGEz4+3q5H28L8|M!s2FJ zcdDv^TrWNlDuR?;y8&2ZlKfat0%y6nP-q8QaVhTbK+S?9#)8)JaPP*`O5{mvUAiuJ zH%d(8rqwD4n#v8xI#y`;F24!I!OWi=giskoeOj-Ay-|(X zeIW5%aWzJJBo=}|07LMJF7w7ry6h@v{!dTQgyQ#)#G^DATQ9%N-7fY6=M{`$pj7>o z<)N}tGU~$F_Xh@FB61j->t>C&q+MP-;~rkKJDvNU%?;6~EHi@dvj3{-V~6=4{A4B& z42ul{|M0yTLo&QL;VB%FuLk(w{ofJVdqQ z+4aS-L$L80$^CS9$nkqN`}A=CP}!W*Jk6zOzZ$LHihqf)1yF}yWnRM5pL*IZ#|*|Z z>DL^Ze^>o}oW?-zo`w6QqaR#A+b{zJ=G01#$JB(zAR)-(L7qO8d#9qcy^iEpPV*$} zIf6Ce$I1;=VkQfnfR3vP`hZo-y3oD~*46E_i-ttiEC8V{Teg>%#X^nc={*K@xFGS~ z&p5k*OYTJLiU16Cs2$K(bkfmfP8N5}<30NU(uw|r2uUNpU^WNFfg>2jL%?{n5I%fv zO9yf4kUw#HEBRg*jbZ1q%1=PzFAJVO`X3eq%2Hj9v8Q{0ToNGRU>|I>qtOnAr9g!S zDth_lAwVfW1!mr|nwJdNrciP+tWF7D?|~^7$dY<8O;&g1To`c!)R5*G<8)`IUDB%P zZ68(h)}D*wzCPgr0eR(5c(~5Hru$pz1~hS~&E2KsnAsVC^dWMxV;`I%ahW(y{pskj~>*u(->Rgcm%vU3-yS>+N^&4fL>ax`3p(403JFt$&JL&8^ ztL>z`&LAUC{;QQ6DXR)FtU8<$dLdj44RArQGM6+ zH+sjIeR3)4M=j{NA=h);BDwg@&KRu`pckI~FZP4UN>dC)hk7&j>KhM=!fM|1=Q!TP z`+*m49jTWeL9}QaDI9(U^0JQHs@+v{SYZ_9{w!{Fh5H}XpR7VqS8nq{+|@v$a&<~{ z1J9g&;kL*FkTEqv_vX7fp>;F;f$JIB2+=NFytoaY78Ux|45)~n0UC3B?y>&pw?SGd zm>Z1>PS3PA>)x}b&#E#XaV6?bF|nYfZFcX z*`(8o3MbfGJ7n$g75~}b=p1as7i6 zuHG&%%tP+^ARa~iIH*JYL&{HM4+^9OuW98;Qzn6ex9$SlJgV~+D_TiQOBM^4sU=M5 zQv`0Cj+!y=T^1Lg<&W5>Dd&G^t+Lb^0E-P~NbJ)^I8itsP=yFDiMpE2og z`SKo%kj2F&f;&%W!0HW$kSD%BWq%BF(Biy!;bKePq;1|3ayt2}Vs7U1+6=|U6s2@i%)b)P4?E{dr9c)NA?2y z02vu-m5h(&4l6m9(EF{8ecP2)ckw1$X$n83^Q1lw9N^u`rYc;XT%P}kI+qwQAw<~0 zo4#x#Mk^GOBm?VYdD(@u93*4Bj^>79e6Ku=?Jq=+=`BRwbw(xt{f{3s93Lbg_!!CS zMHmrDlagjn$N?NB10N-W@7nzSN}z{YBP-4IJ6P znAkW|*2-vvhm)D|oDCYO%WBNHu9wyChOI}}4}n#BP}^>e+!(spU}yWNJ$50>V<`9y zSO6uNPMxXWG1*Y~<#cQItc~VNw<(sm9-rf3k6P!|B?Jl>JPtwc3X$H$T-dS1yy7-W1z)LbK^p$#3gBNGb)=eYID8hmE&{YD@& z+b)=;em*B>>=wRW72o(FV6`VjM(5?s8^re)oR}XS%hqNJ8Cg^GdMOn@e*|2d{e=zm zB;pexg>XTo)ne6#uUaCDpU|wns@;J5tv^$$8=v|&XguNn81iCBSN(cd&UIggEhDPd zDY}*KZkzZ_u*yb<`_=aO5s1sB0rXJ_zPeVo9bYI(xTNq-8y7)@R=1NH%JRxAsOqQN z+AaIF_(+?HEvyTY0RDI*Tw5F#_G^m0T;pA_zA0kHzG8MAqYkOr1tjCA4na}v9#bXV zF!AZ8XK( z0F-2tILw4B!*EAo8#`1{(y(X3nHF3w2wGM%^lc@N`45uXZe#cmSEv=S^4Ts41MEf0kl^8yqF zg%I)ZN5B+@;l-n#U~;ub@Vz;&my|QN+ea%DjF^M*rBTwNqoWk7J*`PK=n`19Y~OFv z`!CKkIRp{$U{mDF>wvjrYi@3Wq&=#G;?taFHm#eb?j+p6x& zHlCv?d8=lxv3S(-8}3oh<64OY24rb%M&x{0YYJx_9?iqgQ~vCHVx}cn(h+(YbW_lR zFqisC5_Nvhya6sg-MzuE_(w9=tMT;M(mNeASd?obS(W@r5Z;Q9n^ojaNsBaZtZZT2 z`1w&LBD@@*Yp%VyCg}6)^=j15c_Ud?PtL!3-QqKzj{k&xk|Il0UBLUksq_^aB21b> z>0qSo`7B-SoO|`NJ)*O3Q9D;mag8pESJEjp0u3$7{Y?QVaz%lRC{c+ejY4~^*KeqG zs?wfKz9J~OKACiDXkUzf%z5W?VJ8$ofxA9fPMf9TUtyl!h7A*~fHOL5jDR{>uVb(^+EQF6hg(JP%~hzD zMBPSuK9qC(d1_aP1Py_hyt&D9zUC+Vs|%0E`)^ly?OhjPan7}7@Ow(+&lI2#1D=B- zAG#0lT3{1q6o~$AmSe`14zD?qGX3tU0q^G zsyN&%RoVp}HEpV`3-_}ISNVCyi7s@k(MjdkJL3y(V?hm%*b+3m19r9speCDzLai8( zm(~^KSg}hO2UpfqjqAoT`>I{%C|ZNsZ!@8j+*)OY>Qk&|hqREC-_Mgywhc{wy_M*t z{83+^wr*vuiqpz6%eu82K0v|2t5lmS#oG$DQVr6<(5B&PsP$9-zR&|=e(u78yrIPl zy9^6F9}#$%Ypvvqb)Mr}pN)9>*8LmM7^@|F+uQi;7Bao<`&r=h5%W!n%3b$67IXH| zu_f+_*i&4fqH}Pcr7>Iz{Xi6Is4$9Bt|2lqQcElh&t`YN8KheIw6NX!W1oXt{YfWj z+`7QiD~^qsy)wjz-ur3^5WQ_+ZV}>`2 z`Wj6KG#Sge|MXS*!hx}gCbGwq@K`~dk{|2xf=wqS_pX)B!oqj)ODiTgjKs}p6yW%l z7xeHpu=|NesIjb^rV$HoC>gSrSI%FQMvy0&ep9Zm`bI_FrPYMT&c`F{uOwTDLn1rgL#{ z@*EpDn`*OS&}0#O(BJt~i{a#RCs?yaqs|ljc0>5-F1W=^rB{tAMFY$fUW@7APS5*m zBdT(J=UN*Od?+~dvyExm)k>} z0xW_h)^RsB25W|uUGTp8uE#g=sph3_%ZkHp-R2;a_qZwR@U(cc>A(e@OvU}~MxzuF zv2epr=%I)wk?*>1R?Y+K+FdWvy)96Dedcicd7ZSZZF^Zvd4G`Q)IbmSD&=O`ChO3+ z8#1lPd}SK)zbX?7E0r>$?0e6zs?!;9M4<{TltO=DQLPCj#E;825cbF?bGDgXD(LOF zj4AUNu!#P)ig0DP{t819&Y=teyM7c@h+o2ct!zLxtCGy)rTr@3aitw9;;7~L)Md> z@$kkIyT&F<5B*E`1jmU}2i*wWi8JA`^q$l*ju7H$4M&@Xtt zZQYHHql`Y=2x4(Bz!FQg^e7f&)8oMQA1+@@ zMAno^W>!v`3tD3YIVRk4y<93{+gN(~-7gvb^z<-8gvP3H9KM<(7?cDG?Wtmpf%s*C}P+>NLlXkN$SUs;LA zY7&m5w~FRUFJDd!ep`GSUTVp&TTf&$|6N$jQl+P#T({UM#;Yc?fAYey>^Z(zBibM; z8#j`LSBU~Yc3b5o?L6_~uvKFqi@Wl^!a)A&Gia3S?V1;0MIZFSJ>RmxvcqGe`+&V` zgAE-!)O?c|gq4Yd&bYZpU=%>(@%bS1Z8x0XqJ^@lm5}S)ZEno^ z^3usbyo`8p>~YV@&l~6OsZg3a!!S%V(Kv0-md~GgnhHJcO) z=@>`P5#yr2feZ9TdQ8yxUuz3Am>1A{XFE5N=I~NEmxyGQUD}Q3CAdG(dtINEtT&i5 zFzcBOzf9+btV4M;%absBQYW47iqww%5b7t=)-m=X-Z?fEO6rn<9Kp}&azF2Y&o7j6 zJV==Nv}l}SnkW28LAR<+MYm^^E4_BLGjpxxq`mRXjnWg2G|(^iNy$duR9HCI&)T-f zSXpE5ko()!@9*J~o5#{+xCBV|U^wqNyV-uDN>+<_k9^&S5k5;ju&pwdH7jP=o$A%U z>hxS(2U&^9$`qFwWrpNLHQo0z>o|lpREE>eFs?|w@?D&m4_p)?%#q^p`}L+UJei2C z%DAr)&eqMNo%oV41g!9#fv-&Harj8%$LzKXV{na9T%&nDoXc1ZrNt(9iI+h zA6>N9BDGYAbt;oB1{I4Fb)@>8;wG}u0^cYo2Uw@q+VuW$~mRaz1) z8EcUZr)Iury(i+CBRH*c5p7Oz$MQ!7k8QB#A56`4J??Hq-Xq1skQkzDJ}y7Ja~0>U z>CC60D7+<3m0a-)>)PWZwc-=Zf8GvD7)VE>RY?XVviYx)zC6~TSSWv<1O>gUVP}P0k7H2urAbf%>hjH|jeS(ntKK6~};A-7>NBYq7 z`HT@}wNv$o$EHPrOIJE8mFB(#SkELSU*ca6Xva=$ z+!u!xoHYG43w74?^3_kDwN6f_LS>~GGa3G9G%jKQ_5+9l@aF6~?Pv7rHS+`YBIQC9 zbhEp)S}S~PZ-)t2(ulC-IS2f(BoE1<0_;lC@e(b8nZlP;;1LyvtOZsw3<-VKk4xwJ zCiV@aW6e#C+c{&Tw(rPztOFJ#x~o&)850)t9li4UferOdj59ICtnY9O8>%Xm!T;P3 zF^E5+QkMP-o`^8Wr#Nr!o^ITaz@$!ESdo!+uIN}B*d4LIxQMTC-r|Z%!m$p>QTgzH zF?ZHsQLWwI9|n*C1O^aLasX)wk&qrjLQ)h#DUlRuq`MKNk?xRgq+3F|yBq25c=veD za~_W8d49k5{qObS@){ZU-uJ$Ht@T;o+sK#mWf=T?;bWV-nhYnWt%`<5EWdAqF3w`bM8)@Plr)5dr_ zZ$Xz~-da>l)PeXvQj4wf+>B-g@nA@RQ94TFI&<4LK91i`F(fo}X|zyVG3!OJ7v0Jb%cVbxEEhzdDkE9E8fzWhNO!%Zaq13xT8@!M`+8SOz)!n<2&HAxOb25 zZypPT$tj6K5AyP4V)w_zTTk#@D4MO&0_TUCVKkb_D#+iqwJkAGY!(>I`WBOV+*_8K zW3T<0e6(G?=gTV9&gIw5{^O;xrIeo@B6tbO>kkY7=gcpYwu!|}62j13?1N@kUbh{) zaH<+uGIbRY8hm)il|6)|l@@rfzy081!1YgA;F1MjH^ng;b^QHY*^19%y87Sm#I%TJ zI93!XJ{2|Fc+N@+f$y&U^f1%=#?eF~{Q=K4qI)i-RVFWGwIt|e@E_)M{i!VMrB|5B zw+TD$XCRCIS>C_=@gUU1ACIseQ#BgNB-Dr*zHm~>61;@hmo{uPU0>AkI3+ze zG4hAq31@~}Dy21E_GcVDKX_26P=5d4rDg>eGNuCc9GWiImiaur#`k1eY;{hO2m>BW z+rIm^-4n2xst*OB!sN*xR(Xvc`UKvWY}^%VBZ`m^s$jTi+89cm`)jt;{{+A98CI;D zz5h74;5;tslkVDi0*}vO)m7g?18rI2%&niJ!btRFy`x@#-=evqh`@kxh@xWj^v2w4 z=8Ws>8DzoYE7oZ8*-f5}1~{X&fdexW3pFvwtnlIQHRCDc3Rd{>W7O@&y=NUM0e1%K z1Fp*wQF}v;7PAi7kuTwtH8E*CGN~JK`|x|Cz(uzjmwNwti9bIi0)1t;)rzd;v! zDk&w!!+?f!iOJ}*}gF7 zHJ}l<*pr|M$h+wa@>nj10j(gt+!@tHx5f&_x>;Sn<{x@Gpj)-GLc6H{-TCo;sDRVu z69B}(!lDS*)sKiZz7Xwm{pvJWA;*@5N8U;8<2TWNX#oKGWhO}Urqk6SOv)^6;mk2Q zZ6QzCb=wz>+O2+@2M|6uD9{J64K_q>yo}iufNy+Q8)R?;g+)dhh@smii&++ep&J1- zJv1G6DA&16r;Yes=I7^8z)*fyfIoSP^h0DmuR% zLy#nJV06s?Y>_jF0_Z6P_Yu{NDtjysflIp4VKv=ZM@5mpv;GAyH~pPJM2?wKx!Ifn z=aBJ;(Deni!|~P^Q6Frncz(M{ikNG(Ny;k)5TH$f@%xZSjR7XH4=LIbH>%KW-_3O( zhL(n=nHfw;N-7BTQosp@BujG;FooHEqCJW&`0NcnpB+PXL$h;+oxoOb1XX;Fan|~7 zQ4rp^%^!t!+X1>2Mgpb8%>nFd4(7T|TI~vWc(ZRc1`@a^;2{0LVCwr&IX#bK^offv z7ZgS#O-K!o$VEkr-0((Q~Ixs$)&j?a4Z%!Lp_yz9hV&o*) z*dkCts7ns~jxjm>sEM+`mdHV__QgY@6{8F z1-|3jeM^_z<3=Zdt)Z(lpil)8xNq==Tf3CRK{otQ`%t*Xp{6P#2ZfD+hZi5c_I*p& z?o5YZ5N;*YLgJbpr0{RD=^t3Hc6yu-h7 z!5<4OBjQ%}O9tFGzJYt=@6cSFWObN{I+E5$7`%M~)8Dr=&S&M*N~p6>-&CW!(HsA0 zR7oM=K0oza<8QXXfBp>pf|9HO8ZQ=l$k@m~>U3=nSfYMvXB{KNAC zgk2HaYkubq*&9zWBgZWe!3dwGaELDTpAw9ahS2xv1PT$#CD8#3{MUPHu@o*&ujFrB z@kg}*K1aY&)aqYzvue5@f#n)j4t@@Zt2FdGZ(JD&8Z(tVHvVR{GH$|j1PAS1jXJH2 zi<2kZ==#6vx0?=w`2Qz!$UtIriyu`l&+iK;L?EH zP{24%6uyE`DgX+kg)$_6vz>_Lv)TGJn|{QnyH80e4D6X{9F8`;3zu%={=j&$g{^k8 zuzwB%ZlJk-inj*by#ETI3(Rn$vWvD7#-yB=A;d`%jqOh0`Ni>c?@3M9aQ(@NEj4(l zGwQj;;twrP*!n#&R+#ih=^HKnt5P6=z&yQkQuw!#`We3G;`2H zl$>K6&pV#9ObUtc7E*fml-Kz*>zARCk@NuE=|79h?wmp4a`9|$xr@neZ}FiBRHRYZ zHU$*6yJ3g&_p-ZTdH_YoT;Sb*aC699K+Wv?;gSq$iK)ekpz%m3^>xP8PSaaR(~oF0tX$Yx&sGz{w2BN%8%I~_P z_0Nv-9~<*Fz8MCf3j2|OvRM_xYrPQ}#;C;jHLqzz)5Um?832%bY>}4QzkC8K$fyew zOW$ZM#4@;v*ruKnBs<+~-h1Nps**{dFs+Q|Sb+Bav=Ff9q9(rZRT2LC)o`#DZX)Oo zRc*@k{U1afbzekK_u9VWOcKA{-Y+}7&YhuB)-(^k!xARc70U0p7@4C{CS`qbT6Tw7 z4Q^7MR}xmShUb5LrI``vwCl5EGgar%<9xcljT|B+QInzb%ODfX@$5+ts$k#E-|dtKhSAxhd?J@(lNcNp)k^eyfh#-)j7 zuPps+-QLaUS;e}jVOFi^o1_@gaLR0*>vzZ__b~fo6EN#eH_Hra_M?-{8Q-j)qX+T4 zGy6xsp^~3<$9D~&qwVm)4DTrP6w5q0sj>LkoJ4baxp`cd`mQS2Vu~}O?LGB`#Fbyu zs_#<5=KUJp;ZTW=DZ-uGrB`R^2jpi$)Gu7M-YOOuO;?IONo3Noommdyd`~!V{jE$e zWSje%_H=(K*wMUVY1_$1CkBJhPU#|wztob^t&#kIi2(Dfj3)gPjHQ7fD)S;uuhh-D z?eL9g1>1RFYm2ewX4!!$hxs%h9E=D+XTI%>VAN1Kvw5%MbD*-N#8-Kca357kr7fSr zykan~PN!!Y8DYQn7Yv7zZx-$XRy+HaKaY5Cu0Q2mZqZ$mGtl=4>?ca-Uv%YN)KTb8*{JFQ zu2#S#z^*t*eyccFSd|92o5+nTgu#8{|1#h&8UAY<;kmxdozkJ5U%(-Kcwh5n!`#+H zKITV8RM8ty%wJ$ApUOmwb9A_9J^6F102NK;TVT6R*WO4!2)=D-dATC37L5q zpl>rcTv;cmi-6iw=EphLm^oZ_8KZbA0?+w-myIysq*2$bbRyAH5*;sFWZw-iyG`aU2pab54cvJM_LxR#;&0L+C+iPNH2G zW|>AO<}voOq<1`ZJJPiV;~8BdIP&(xa3QmOkB$y`Xctwa4r2?ohV890Qpo9P__19} zb9n>5JoL~En=q+t*VeqA+rBH0v$3zt)}{jBFPVyS&UQ({G%j6^!=khBdwq3YudWL0 zstyLUyJNV182Oy{oLkm~wG9XcKPmQ+4%is$G}|#*JKvk=FSkamsO{8cH=1-Pvbj$3 zg}@)X9`j(43#jLRXNfv`9rz-B;Om5K+m}aGpGkY!hXnZm%|UDC_)ucmvyQ}gS%Ic! znt*W_s^?VF3E_mq_$W9VM+>~KE@fjFN;=MFW+?88&)TRuEpcH6JVIe>3IPpx|CNQHeCIbuC&W z)${P4b)8}U!{Jf9Fi;8L5@UB7UeOETQ!ULApB+6MaSnu3UE^YNpjFu00jZ^>>Ro}k$)?~E?%^ku!vJ!e~_ z_9D3e-Aid}I>z`>mleM09v-F7r?%`AJKK23->+oRvi@cy_N@J(_$Bt0xy;U^rM3XniEnpJp*~Y%^I>L$7=25Bs zx`!yOAOASD9j#hiEq=bt7oOA_~IXI6bI19>XQJ9%IB_`2ClzHX7h?uoCH*~=(D8?L)ui4&W1 z`RM1&80MC_nf%MFDbaDRe8LZcl$yW#370n$&`;js*=nzWPi=P%?UJ0Ignq)Djs_h2 z{5W%8lwVpV`2+4$f#rm%BdPC1K2myzMHhpV0aYZc81WLHYE_tK3|1dQ7QHwf_2+|QmZ*G*yH)633%g^O1iHicu=IBe(XAH( z^hapW#_73h*o$41wDWofVRSeNw{a+TjRvO^Z1K)eHQM<+P8&AXv!za?gNBQIox{p5 z+7)4wE(Z|qt)%&0w9vWMKsu=<`uJ^d1Nts3teFZgaus=OYSbHc zXNI|cq`BgFM*1_>>-?Lo^geNo#f)?m1=OGzUke{pF1Jf}daAb$4=wpH*GZknyC2+vIs#V&^@ZdwRIG?T@v?A(l~r z;&e2m^sFih<2k))M&k8VHEY-LV~zUyWBf)lstuZWy9f87z6cjKd;x8o+jI`&Qnqqv zY7u||pL#@R?xRAef*@*afSdMd z0a#%>$ux>P;631}f6bL;L|f?5hU8iH1vl$<=-?+ktcX}*drZo5F4HNkMoabNkA=_v zXQYyo!Dgj4zXNLoSfhQ)9!&*#R!z8aF9xHTgx0DPM^MVZ42u16H1Rs~o zg(IIHCCun8FQTcY~U8_YI>xR%MymF;sMqrx@}$E`j26OEv2IDnCom7&4k zwXPh(C_iI*G~trPx$&OXWcLOq**3&W`3cOL-z;7C5FE4_I@}L{l7avzSrX8S@YmI% zpWwUp63Cotv5t+cZ0LB~7*%4$QkASECim*c1YK)zWNIoGWoi%elJev>X;WFPzSh_Kz%< zDr-`Gy{pXaQBRAOTx+pvmLi0fe7*Nxmwd>@^@r7lxfJB6$OrM6R=qTGCXt~kQDVhR zk5Jqsx)oLMcz=fM!H*Gl3n0jCS7c8_oru!z~C5mbWxEDbZ z0h_!p@*F6!Ho_LWS_^tV(u*~?OidiN(I&jsXb$Gw^DpgX?2dV?)=J=P9yO774vw4~ znm!VE8fkilksP;>M)9NtqCe7Ezhs_6Em8cACp;=C!%sP&xlPGqp>sUOj|Wyrv~0B6 z=6X{!>2mDfU#&&cMiHTmO)hB2?+UYRux$5!vB%Y`3W}~X_)>k|fM@6ORC9rO`Ht%> z?hXL%^=r0z&_xz=JeynHDjJCj@}5R$9pN1qE`=P}FP=9h}KkQWP7O?t>!J$_L0 zFPvp0)ea_u`niuhX=CH&RACt%HWDG$k$(NF;P z`+3oaX@fxf_n5TP&|6rcrjP}Xpf}K4eniZ7i$Ewy`Uly?OP?xII>9-$v z6`jnKRm7dgu0xA32tJ1@OO)8WJXiCv8EF3|)b1HEaprhFsZ0lvg+6S#ZD`EzfuPsQ zRJ2zYUJHAA++E2Q69Hj-kfT000a!*NP5eaG5(=8;gJ_=pv&Y59FN8RpPvI z4gEu?4;qv|2&RF(H8$ug+igC(YZ>a@AgUr7R!bZrr=E&Jc{Y1bIa3bj@N1TFleB&v zeEkm~zqhbF5b%u)A-RBiBbc~;0I3})l!-uaKozYg2WOczVq_-XbT5032HCWMOro&^ zcb_Pi!>(x~c&-7tfq3TCcF(0^kk1dzs4IJUf*3`Fah+*_f4ctN9&d-%j0H8(7HbG2 za3{J)FiWGUf+KL~*g0Y7p4PE9MOj-5h07s1Z-%jLv=7PKU~;%{k{8@bm9&lseG^$g72haWZ*ct>l$cJf5w*BE6J%rm8~iqdtnP zbpHu5zVp82;p;9X>Dh((BFNzuxdeb9S~!@Z8k!zcy5M!2_OAvA&Zv}aR!8ixi?CpN zx_$Tb&Bx0Fg$?sij@d_LvwVpsTl>n(UmYEuJ4(L;+E=PZg_Ya`_{7~a_l%_jP@~uw zKqUruuQelU^}ebHCF?mIo(T&-CRu#lxVViI!-=Dm|8Z}Q582;$(Irn%Au_lgO|;O9S8RBOTvO+YL_oe?VoGhOJD6C%8oPMC5X_ zK7a8nF!6{vftzQzW%UFybb9jL=5bXNaNJZoalWdfJlxhD)+7Q>!uuhKv^ug`)wx~b z{VkrO4kxaZvW~ENY-?yH;AEmS-y2;IOryIWhfN|%ZIPsCn} z0tmz=OH)@{2LTp^{`fog_ck@l-4CszkL#84m!Jdkp_*044*4A0Y~v-@mLnR+ zEQg5?tu!B8536(Ue^2`=*Nhoqk5aK7$3$jdXE>g*5?uBAdutT01IrfKb>2mhar#(( zC@y)G8TnahnZ(p1D~C!!jgt11ZU^L?_^iGmPVH_Y@~RIZy-F!kE+b5)j;qhMYuv1R zIM-JCVVBPZfkA-Y% zyu~<+_W-fI~k)2%9V)=jCSHGs)clxHk z?dG~*Nr!$y<$C^7AX8mRN-B?9BB0!Q`{-h7AzPl!iZQ2r=NwX39Q}>t=b6*W%$AAm z%LQ(?TGIJEOe>m{4eX%+vvF3<+R1O`3*sXIWPED7ySryqybWJMLgsm`7!dqJYG?~+O9qMPJU_yt<>)5arse9&=KW`wsY~Sa2XBXB3HPJF4a5^!`p`p z2R(o8;T`&2PF`3N7vB3BrK8vTNBdiK^Fvz+pXqQmwr0KsFLp+$)4t~;aQl4R2W(|c z_#F{`R#lU7#R9QH4Ur$;%o3J;h2y>Rhs5Lf+2Z-8<4IRNoQ8;xzR;0ZJdYmArbojj z3np{9Vh){o1+(LnX)8}M?tEz^&b(n)|Y0%6HIFqb_B@m25yLfJwX~$`) z@gtPO=?D)W569GZLV*er9Kj>4hi{Q*Fn2*)*1w{e8zIm6Ikqds)6I%X(T{-PjmT#$ zlB6TvLpxbUe(N&+T*}>Q%t-w{^WAXf5SEnRfy%S+)6M=&E35&6x$PG_c;`oFwU<7N zXacuH<_MTWG%-$&4r)c2F0RX1L5`;$iKyt*AMO4gJ%X)8wJY%1xfMTD=Q~baw7yZz zTA=iRy?o5Jk}ctU)$uS%eh9mALK{t3>gkBZ(hsdY|GDxXls@{H7BzFHk$lQ>mm0h* z{)R$d+86F@zLKn$g1el=I1tQz2>pu*W6%!`8o?#TMUa})=I`&}p+$lh_Xi^XtC^B%-mMC zqVz&dnZbdk;``$O7iHLe{~H?Bma=vHV^p>^VJ{%jJ6CcLVKHA|0iGLp)sc=8>!FG2A7%a51qP{)OyPYP4Bw5oeGg?k-)dTUG zzH}F$GZU%3t+s;f6Hw_R+?~3nL>?ZkM6v4-cZm*Pdq*qUQvJSVEtzsA2jscqy1;2P zgNHTZso+je<43@*HD^+{L|klqE$s5y!(9!&#F4H*-~1~#z}v#8lm-d!B+yj|3BpYH zj=aQ1sZ%*n*appaZi({&eQojnO2-0_zIu*B8vZh_wT<$!R%_QAPVyL(KJ{CkZO?u{MLc0B?Tsocf{pk?3tB^A99% zw$Dj~81N)KZHM1?Y7qZ=R0fcjwsmqwV z%L0LWs?c?aV;j7i#yA}Psg2y~t)l$|or5Pn(uiM{ZOofgERB_kValrU!LxNvohZlM z!?L}hc7KZ*b#D9n_$@jO&Q6aetj5#OOEFIfP-68Gaax)xc%QQbqouJU(ns6Dt&)*^zUk5gyt9MK3t?S&)-C4+YvR}jXxIWpvf4GVs;3IG=kC{Ib zBQruN(XwFEiP?Pesg->@N?fYv@yk@dZ&;yu)5n^r0dD4OTBXV}ovmUo#mVV{y7br8 zkjLr~fj@o7?DlRV25-e6G8(Jw;{>0pyJOpSzH7GKu-4f8WJ@E?f z_W<7jHA z$S7B2+)2VUcRFhrYC`Tq^n7Cq&#NpTfDsDfQ=sRXh`$l)FNgke?4wuVy6*z9O<{HG z#Y4XjfLH5XGAUpn{hdbP2bh(;MciW5AI%mglJ56O<-;7!?G8d?D4X9N8TOm~YiPI? zKLOkz?PEnlcCnJ9m))Js?1z^W4?hg0?7cejU z2Ow83M3>Z=$^hy}=5S<)e|m7gK~5s23fbjbHcsS8dipfu05? zjBy^P6hN!H#|4P>GBJzVb4y#sN{#_(2J8h$sEM~dJCbe^1BmyB11~*q{I`2G;Q59H zAzazmQRjRpe73OO`!Bw(>;>|=7%s%SoF>Oj-WymxdcIDK>cP>hZ;n{s==>~ zbJwk}1~pM-SIr{+4AB0X1S?Ee+tDcJsf94Ba6h9I)b~ES?D@BKc8dsV=;)~0;9ZNO1qo$mt0z{}suT=p_(z+i36X6gS zpFeP-Z4AKtzkprMhVt2%v;|Z9;JQM=0pxJG*Z*vV1Axj16ExYl3AdTWLtTTw^2ELj z!F@{QC?WwgzJe!j$mVg59$u(&OuZ`kjaP>a12cl(A(Q0!&8ec1 z_6;h&FFzM4sVB@4cBSG*KhE#E@lo{6F3VA@$AS`jhioiL9!o#hZ#tfXxHP(8~Ergq~?mIf{afs^LHF_{H^c# zANAATxZu}>?yH2j)nR*|$&GK^Zqv<5pX@3{%z2+Yc4h5r@`HCTuZ(MGS~m*19Lh~p z{A206ALI<&r@tpa;AR{Awp{)BPZ%S*SPiH|-{tKK_mpS9^_G?8#D4om9{4@Ko!O@w6VBuICjaXL zyc0o6Sw?)m^*cZR*MIrz1N;q8Ht_zO#@~u8`J{fyA}xz=f*^`r7h^4GWqkO}=Iu3| zQQzNRK@w&F+`wJD+52Wa5n&*u8_cVft!!s^Z}F*0rx(l{%O^VNcL+L=rZZ1l)gFy; zsr~asJP{z9?Zs%!ji=1=Q@!!J^PooPh{Kqq%84Zcs*v19?(!g3L>>V7WgiXl07tYG z-DH*j*n+^EfKibaNaAoaWD={766@acb0FQnz&-79^s>)!{H1~UxZ!&Dx2cyKZlJ}> z!t?HL{`~)BtmP47OkO#3Dm;fYjR*vMt?rfaKZtlb_uGdATT-# zxvvn^2_~k~5iSc;6nnp0%44%N`D-=>jx?79@+j`p(Mca|jA3ba+@lI2@S7wnzggHn z=C&Xcpt<_}1ITXPzZ=l*Q`{+>$LVGO)~#nQhFlzEC`=G0K$GE4=mJoAF`ZxnfxP;gFqWli9@Y~Th}#u%W23kA!RJ_2FtT#vB8N=4zsRk&JZCN=Z~L0J&K1zYJiicxP)0?%SaZ0ukQC}q$0T6 z19NTGzdgd$wTwgkAw6j3SObVY##IFqWVPo_mgHqND8TF6(bywqys_hZ13A^C-FHA(JiIJ7=9N{-w z3mBBJaYWxfViG&QIqkB4b}b7ML8AYzkWLKf)>pd#i!KH{6I$s0_p1@2T)=io%n?YU z4`K~>Ve-OQ3G2Opbt$UJJJ5R-+M(+|$XLs`BA;Mj=ak6j9@VeY}(pLQ) zgXItn@%Ck%0>v&`!0MI2-bg%h(fPJ*TR7(@HFG-#yVWbz``ImCIA3#KS4qhks$KYO zRWIu*+-43BTQ}#R^b&5zjK%7ypzZmZJc#OhDrS!U1wdj4w;z#Q7HeDhj z?kP5&@ywq0;wn{=sL35H3?`AAlQo(J0)O(L`JxaMn6IwOwm^mp(Xr&zTA*DC-vkg` z`$oueDip%>(vSsWJ8Wj7=BS#_1c1&+H@oRwYNBE1Xvl zv3G`yndkSO!Pbt+&%$x*9Bi@@aD8SYiRcl?(%T`E6=QLvA5tFwuo$3rV4^UPXIqN7 zY3IYx3;1af2DZ(Nv%2I92GD5>K@MoJdcoi@-Y+z%CZ;0v{OYKc{N0ppERfgwgi**U zVH??C)9?EH*%|hE0w8v!ia;D^tVau1KYe32oPQ8?s;8NdEU}NBqEqK50L0cAppixh zmg^o6@_I+Uzm7cM2lQBHPi4#;z!-$r(0?~sN^WXQdyg%=Uo1R z>!MutYT8EOD04+h5R2#5E^4SL^xtS`+q~BlW!wLbhR$*xe8?s52Q(%WS1R_dqNC0t zDg(J2y9)$aPmGQo7p`a@yasqLfg`#E$>8+&))BN;jH!F>=3I{JQ}X@_dvg=W_W>TV zTb+Cz@ELvjuytN3Lp;^z`EY;q-tsLmk}%v$)ySpUz5!X=)OY!ZY!lrs;~z-!rzOU8 z7hS`<4Ua1cT*trJ${BL}Wc9G;I)q9%p~E9D;tu=RsRtK7b!dyFb9Kdkxj5Z}(5CSo zI4eC#=lg8CAQ+VQ`GppCyksqZaWD39+wGd|@md90zdPNmoH5uz7=crpb4Wm-l-KxI zyz2~7zrGS?k44BB*<&l+NNo`0jMng}$j;-bBA@^E>H*o88cAUF{*w>Qr}}R$iJN@r zq6)x=c6c)EBjQc&01@p{{OW7Z*`$nxb}|KzGTZLbZKSe;_3g!MB@)?_2N0SQ3JY7s zl0I}J&j+Hu#mdts3R{Y%DSB&tZ#v}bF0`s;P3qD;-U zOUZa%U1GKs4yM*+6E$?xi!>tGL;#fVJ*v5RDLjN>SKpat{Klic-#Z{o#(ZRD3|F58n*dzqB}K z9*K0?7gA5NUJ66R(WeNL=80Di$#bY$E^TI3^KA0qX71^26Zn;Oi!HA*HN>3;J0Mz9 zCN;Z^_#&Idx-*ghFj_fd(0Z6V_93xN;^6H-R=3vkT+s~pNhQMqZ~8cq$&)xjk2D|; zeT`-iRPiQ78}k>=v#RXvFQ${Yy7$YfR73?AZARlu=;h=9_~4o1czl| zx4RqJvsKPNM`qgN%G8;B4yxYy3u;>~{-Bi?vJOOVs|17)#!44hyE?*k#P++54WDM> zJ?T^ccujoU>nWv|32BEt{L*Ot(R#J%lw8y$D>i4J+tYi57WG6S3WRtNZ+H#{LvVpZ z@KsmCd)`TRB2LECxwy@BAe#Q4owW#r9BCxJ4o6aoKwbghVF66pv+e7@;812og_<|m zZP*0ZfWQaZeqG4WI+t{w8qZ)Ay$`Mylo8vB?Vj_6ZODtf5owI|nEvAFHT{S#71gOCfUw@u$ z1~Jj_4WTu86?=G(+-|W(P?TQKZaTQ zwMJhgWsW!z5y|>Mvg3{0zLv!nA{%A$Oc=Yu9_Z2e@KNCz#Fotdc62hOe`R@SK>oQ& z_RsU~CX<$qpMcv~vF=de-Bh_>8-)&*aD3(p@!?RfwNlzsuXEW$SKQ}aPQKiW-x;9i zu<^(FWP7TI4yVR({8Pqal)f$yOEh!rio7!$((~vBk#)ZZF7P~Biinrz2`*a+6{Mp;mgFG568TL4eAFl z87JC34ZPKfG&lY|*KJ3${4Cc;5~51U@C!U%N6)Dfhlc+rQWP2@tKFC>KID_8o6@d_#}`QoXJDX9GrXL3ms)#L>@6*aRpSL! z+d&brWsy-#svfn9H99@Ti8?dHc_h4D(l=utaTTg_>74)^ZphU%`WGUa19wAzEYp;9 zKz#AFJ_bFWsOKd=HP@Vf4{r2(RD7LCddyNdRJiu8KHkR}MW7r15ku*6n;xaraq@)D*^746S)X4jz2=PWu*MM_Byf zCwH5#DMUaI6U9$z+tG--3(quKo^*QH7&)(J@f~LBy7ae6(qfm3=;5t}w6Y5zVWmW24inr?kM(o8a|k(~ofMhW z$gQ07C7ptK;0co)62v&Vlq~7(aMu<;36RVGGJ5JP! zaN5A1_m#6HNHMCb30W^&An;Q8*U|u9b(VUpHo!}M3^-am58CZt86A;E8&?6!qt(64T+g1f#=E1JB$%}>wx)hcQ*6I32d-IC!d&9;$%@2`I&eLHS^_LJMDYLm}8hPO+1 zA+MN;5xW8)sAl`y9w>jYuaV&+!00qNJbE}W>XST8`9Di-B6y$=zl3>JQLzE7P5O!d zNoy0pftUKrs?@Pw<5&r3Z$FRx)=yh5`Om8Oeqx-=U1;Sq1Byq{yF%xCB&xdMfPAVomW!^p z#MCEsO>-;?p@9^ZFf6R3<^D-t7B%@U&k+nb9ex61ZwjNa}p#H{K`VS?} z@vg-16B0W>v?;I@Fe6&wlYq+r5>vnJvSlvtJU#t%>(Vi5q*et6p!Y`y8OA7om`h`>6~6+W8sD32(8~= zOBL)0y`jq~k^MQlaYg~)fz{6DgR@s=xjh7XD@yqcM3FhvY8@-yFSPjQk?~oV&o3;f zqobqeg@uK2xNM`|4fzkcoTMl@0SnR#!s@ga3)0G2YU!UcHMm@I@~57u6Xl=<`4IqA z>SG2?ayS!*X{V0U!l-wguw+bAs*Y@s)9dAoW3D2Q@FEuVk!n=Sh zr(QH2WmWAx=8)9WSF=vfuNsPo30Z^<4Frkf%7gp@XSAzD!hwD``m^&pg6|D+qWOYTk(@V&T)nhk-o)u1 zC~Ls6VA6g-DU;tksNNIG0Mq~iUdby(;gM0ylgC`l@8<|({wxS7WUJg z>qsbpxnJJ9aG17H`-B#M?sd>zSl(n`@SpBBEDm6k`jcn{`Ul9(0RUP*E8}myN=MMF zAClYk188h7CO=#f(B@FKY-dyc1PowdUqRazrK|zbs1dH?y!{imVJAC;mX%vTGa?Y5 zgbODxw-%r!&X@Egc>?KzH`{}(4An&$1?;}&c5Pda(a@kIaT>d=(AL&z{!5&b@hw#joT*4+QKww?H+_PqLE)H%BtPHSfLza1k{|4y}1=a@bAB8SiQYHQwb zVf}KKNyflmlf#KhK%Q#cClvdEoGUUwBChw>xKW#BP!SL0mbK}F<4N-xz$>amTtcpg z_goc7Z5CeQt$A_sL#HK%O*bC>P|J%{GooECY{)sUdaw`2lg5)@b!W^F$aiw)a*QTJ zZWIG{`{CHRSuwF<84@ZSe<*a?JZ5K7YkJqX5jaEkwyG-PFQ_#XjdLZC`OG@&-pD=h z7C^P8I#lr@GPfr5GQ%OlJ_;YU<<@9?3AP{Lf9)Kae4@%P;vo!Q=dtxe_XkrpYf4h` zVULThGG=K_`w?8S4o(2Vb^b8#_tw`_+=fNFRj5hrl_i-B^4SK<>Os$cB2rXcP5i5E zqGA;$f@gm2g={(H4X}i2qbE%vB=`>2`+FCsgty3iDWRK2!YU=P(L1DCpmS>zKw7h9 zO#K~8u>495PHj-1$Lq|D7raX6fHw6&tbD}N9&D@Un+U%CRq5N5P8S=qET;@0vrmEB zq@VOu+EU+tb*51i)c8*&&-M2if_FTNg{RZ)25zdaA#yv+ zM1<|7I9KZk>P%*IozppU4pUE^R*ihr+-!MPH7++#+?Q`P)l|v4(5aXYjt!641F**r zKY9Che4LrN?!d_St5s1X`kKYjf1n5m>9$os6f&fdq{%k4957t=b}q}Jt*61gy9-il zL%q)}TNXL-C#=~_ksyaqx#0}R-kR?bA_-G`6y8nZ(kT8ZKzlJ_lTTpp7N8G1op7E8 zTWJ6+_q(R}OfO%l7L>GApdA|-ia`fo_1sO5^s-lFd)EheWC|+?jA~ecqm61b_C#v@ z_5iqU4Cp9t4rA@Va449AlEaxJ>wAOr3`b;XC4zH(%6hY`ViuJMTmUl2C|e+W$JL!1 zx9K4S4rQ+HaE?<`JKrQ+u(D{))%_f)BxO1JrcqaNTaUa8k#YiXBJuYZrQJs`P*MRa zUNZbalx~ZUv~HjuRky|K&FdF6F$iN2kzT5Yho+&mUVGU#)|s>5_-37%TP<^w5s}uO zIewDW8|0FlG>Jk*y@!0l$jr$aIb$WX<&8O%Y{2IOyqV<|jGekeuRgzUG_rd=8SMP>&!=~Pmafh5UfS!cVw%!yc zLb&1G27z!6D%2GkF0)S{sb@7;{+B*z3qv&QLu+`#HCr-L6RD?(>fdqIYO?LDej?!L zhbq(#guz_Pcs(o=b@UOJ6pxQ=P4GGv#FQGhO|xX zNg`Ub0gxY`f4I{49pW`sOr~qFK{CJoTNgADr!lGi`SM4JtuQ^tEz-vy#1J9|GS2#L zi$iQQ3*My<7ZRC8TnPFVY~nO?-Wj!L&aD+0vYTy#k)$ofen>Os%~nG#jL$Y`Xzx#} z=6^8v0difYBiMD<=vqGgTw`KAx=UMSukxjK<1-qzr|a9bT@R@Cl^;Q+UM8D+DZRig z*@xgROXB6tBh|cMyk<-w_n@?Go^_-xziu;R_xm{UL$tZ=Uc%Cbs#fK~_!k1>BeMHg zwwjc)unqVNd)-m#T5s+-WIhD{nz0hX8b~uK0Zk7&aO~%!Alc6M8Pv}1REmIXDNlYX zzse<64klKjII_D9>;?=@6pwe8qaU2T&HeBqkln%2mb>dx$LpX^!IN8EbKF-a15i!d zn;S$50bs;q^LW$29*9SDe!DHbunK0{31Zs$7ou8=4!<~c9 zPVxFLz2B6NaNu|{&Ad_APHnR#9&3Uln@h#j^r>2Ob~+Bf^(gwX>G8JJ#5?D;6m1NH{X$8B7t1WH+`kA=_YN9fslgIN$F{zR&qRujl#i`N!+?8uR&l?z!*l zzOL)OuJ?7{6Kt7As$42ekNQFwj401DfUPxq&`hy?V7x9L<;K<0fYpox9p%*ghH%ov_yDv$r9qTxm;>FiX(nx78iu-8;c~owd%@Eu}llGeb$~0dE)Amuy41 z?0XIizOi=4yweW}niH@8|3WwHAw;vPKO zlo)zMy!IhIX(i9a(=}MEGFx}X+>)aNX=#Zblpg3b#C?&CR?B^wIL3`Azfv6(GyiHP z(qgPE!8W+luU9755I=a)w?Y+??J~*}9cOw@K~>+qXhmvRyS1-MGrwH(nT2a_@sJP1 z&m}rI!KE;!#C})7EnVP8O;(ZRElAw(Wk1^}=H^qimrA18ewaI2t8+i7w&`uM@R%Zv zQb!9hdJueb=&m=Ml}}slOW$HD%mF;*Z&KwChPWYR-?7piWmnDgWLT?KMbQgJuj4~E zl@QQ9*oh-2Y5M+4xlXi%E3e>msVE1DVWlCO5>k}dW`HzN_T7VAQT~n2sKsI7m+k^O z&f!m0(Cd>HFj?~iOJ+Up;W!Fg1qSYBLCPdOnm=U`mBkc0d;hQAvCU6Z*7^~IXa65~ zwguZlpLag}CkmL~hTuo-W21RroBv?}7a#xhpK&NoqOUz}lI@KYEL|Y_bx)p8>4anB z((*+UI6&aH60KUm0jj`hkpKPj<#?F+a{jj>kC1YdlJ+L79M!INX?57Tx1{qMHYl=d zH*o3pq7C;M97gmEgK7ojtzmU<%sUPUx5v$`i^tKB6b`MLrr5flp0}u9Bc?Z#-KF$tu3~SE&r)ePi z{u_eaYHcL;&ze~HZ%RE$t^>tMU+ycy`apaGywXS}V!pKIJ<6%hl1dYSZF$e=aiKYA5fA4w3j0)8B zve1fDwR51Su9spDW;$^79q3vSO5T~1KgTyy^(d2!&c!~2w#uoas>UqShuRoE3jErs zvDfF${_F$JC;I~#IZU2yeE1)9b@`Poh*LHcDiJr|e4{d32ctObbVSFe6|!=oT$)r!Na z^qB?6RuB3fxKbrE91rvMXghh(a;EF5YESurovSK-BbiHpvqk-hR1!N#KTV;3 z{{Zmj=H^{vTu5+{bv3HL*s51$Wmi8l{P2U*_ka8r@AcJ#nTIsL4Qy7a47yc!zl<#m zIQSb_vDdG^+KDcw$w_O-n(>0K+vK@E#jDYz5!nmxaduPf$=%a#lx)z(Od0{ zVR(zg!8`l4Rq48b4MX+mjI6AZ*v^|pz3MbQ9@a-zN7^vuk;-AzwYi3s{DOi{oS;Jb z0>t9*QB}Gqr(b##P;{^8kE+HWb@2Q7X?Wu)BT#PZtMfO460=Ehz_GDY>zWrqJsJ>v zA%TQJfs4%j3DeG#_r4_FS)J*Wi&P1ab|35_^%ujY^Z)s@{jn2N1O|Cw zbddSyKxQT%7tt|3AKm!ch#^<+wNd=5(uC8Z3F@Bx*_zLSpsvLb=2L&YOOkRL9h6i^xf=k^wTlSH}*jjOy@KKelm3(&8IpSF8aKx{TJpLz#AV}{* zllRX74VN(xe0Z(r!ABqb7)^nnDlMna7*#_){8tF4`m4-W$q7|`%WMET+9 zpKUXmqK|rzalIW`Cv?K#g^2VCAtT?_e!|%GMYHW!;<4zo!y>rvA{`GaueLY*`9ppR z$Xeo1vnz z*MGj$()4I{maea|O=z~`9Re~S=9t*>qHYxoTqT(*Jp5+UF=`+ZdjGKJ!CsKMKY?}z zd4K+#Or8@D1vNXDV?C4Zr2GhgD~qXq&foa6;>j^38MlG86c?+tm+p3N4kYI9>Fxkn zzt?HJfd^e`CKc}d_p#&x#k2C)8mU1I3+$EGM|j?pkP(mHY10|)oGZ2_K)>%C>TXqF zcViIJVCe$>U^2dl2d3RxUC`FbpgnLZc$q(gT5_fS+7QmNdC&NEHX@go{7|-Z;$F)z zH$6Xt3UKo4O6+izTkEuj;A`@FF=_r=(BXFoF#$p`fz?Vm=y`9^%&_Zpv5f+e%1mcT zeTDW+7gyjy4zt{Vq=jG)!RxK~>34oT$N0D~8hW;XpPDZt?D^El-Ge1fl zk{ly>K%m;T%H)22y07Q-co;6;NRW_;{C&%2i=#%P`PFcpbN5*{plrZ0p3>>M#HO-y z8+prQ@DxjU0e#zcb?mVN!w*c3@cqg0iC0tVpdXN0f@XVw?#DX-CblG*W9mmimg_~c zz1Q*aR)7Kax&H{M()U;(#2GkY3k@7syd|guX}oy<90(Z@`1MdnxIF^VbtNw=`@|pg zknYj!cxfh+Tb0f)9ol!S&ai}W-V(Q|=>iOInhML!obw(y8hQyW@lZ93N#r@h>lUHG zqdx`nZ$TG~1muYUR9nV<(9JUJPfBwyii?Xs0nR^G zWkQ+J>%*h*!qJGX0jCI z0TsT=GkWlP6I76!`w@^-3!|lvVx@Hxt=6Iv5*uJu79MFpFBkZ|XgM@o4g3Dfw&L~e z10{~UDX6R?1VUaR#j_TJboy1Yc(2lD#y_z^*ex7!cTjf76YADoryI#FzeM>xaIqtHVSXv3?2UNxuz5;#wZ5D!ex0(5Ln+_u z+#;0h4TVtsV`!W0?nS29H_#e2#qIg@ky}u_WROB{rJQoPBg$?EM=pc*bP#FZm{WF# zfywQ4p2Ra(`UH@U4z#&{6^#4aZBU*uLLC zNrbp}wcSI}XTU~G!qs`iy(|lg>$spBo8hcE*QmKMg;!B{tkPNQux#RnM~IJvX7Cjk zcr|%roqvnK6RCsBdn2Okv7n*y8$5 zznqE{KK-1#5BI52*@?!dvUc_ZOQCD$2WBQr-EsBRhW;HHA@2pnf9%Ib*grODG0AGi7$ z)c9D(2omy?%|QfLN8NY&7u}QT8*)l1vf|}d6*EH##2sfiLGW`9YSCrBw0BrQT~1rF zmLw}sT(U@?y^e=2c5tfCt*=LXtul9$(Z8?2pvqJolC+hw?N^qP7BosXg|_aedoh-g zpRrdR&A(mAJflYo9GuG4xC=#oscK^?9&k9i^nOe zCD8d(RswSUF-+^*z(W2pF<5dg)zx%B=*7!_mOv>Q|F&9w&%*_po9_Ok$bSW#;EZWu zV8xPa{I!rrRikQqSi-(GYz9~;fgLoq(@*n`Qi?o6(LRCEl|G$Udv-%U@kbecdt*SO zJRa~ER9Rf%7akJYUV7!LSdzHY$crEp=*o_xLQZJJ;>VI?P5)`ZqGGdNx6!#YwPehO)F4Z=@XN^vj!e$i8L58z;!@w z3#ZG!-|zve)(0Y19@j3CTNgeE+amCNO%~ut`_M_zlD>u`6vAaR)RH@$3nb%eN!*=4 zM)HT=OV+Ph6)ggMc}r-~6;RL2osZ%iH2o=5{uvktf6|0Tp)<%V$t@bf30S-rRqxN! zcE>}_Ng$pAh>D8L#Z=ttqZ?Oty(5XW5B7pC(QaTT$AqEzi?S8HiZn@f)NMKQb4FSJ z^fRyezxVS=P)AzzdsrP!D1}O8gxww*9(bn~jE7sCpflph$UnhmSmvjadIz^dO`(xQ z4WB|1?3wI*SM}+_MFU_vtr`vOmxJdOeRUC2nrSu+dMVycU1)-2eK+inpmZp1y=7_u zq7?MJbgc{H5C^-&kC@;>kWMgK<=bxPonk8# zhHr@{51xQ94rVbfxORgIuHbo~%z#;Yk@lWVrNjG!Aq!=xzHB?~Rwq14e&m=rE zwdGk_v30_^a>Yx5LhpF~2^kRY{_Apl;0eh2>*y9@xV9-M!Ja$ar?`-5GTKksKn!H^ z_EU{+=T^!noYc2L_ZSwWi499xX$xI=>$0>(uHbw<8~O>X>(zh_FOg{PYaY_~yXKCq zam|r}Xlf)XZ|zufr(bhcgkfD8Qyh6(9eZ*lsXsJZ#UV9=1F+sk;&Eai1=@ILCt5E0 zM($;sof-w$ne?-AV_$%+%Y&;r*e#$rx|4+GNk5(k&tvAT#)k_~O$q{`FBZ$2?v>d$ zcV1f`oev30J&R|tK1b=gT!xt4csbNc``MXvn z>!BJRb;R`aOJ8^}u5q)u^Jf(2tO5fjWV)NDrnyuv{#FCK?dPOMU4g%$ zQ*UzOIt4obE}pGJHt5QH*6SBH)IW1?oiw0ZFRnGAPJ7JbYe@^9JzN8m3o$OS!mmX@`-)WJ z>FJ~Fp9Rox_znRbSZNJd$~m1!!a)$8xEqRlTUg&NxIL}Y7LJpr^?EF* zko{h_SSwC^QwrkFJi+@x{piHLGy;)3VQi=mG8wgyE9rXXmKq$9?ZXZ_Z*u4KPuFaJ zW%fcbT;?q3cYG)AOBq_?T6bUIx7kX1L4rG`ptB@IfI(gy?>ddVJw1#^JwrN!*1k(-d5}sa`uMGGVY`v8my~T!t)=k1Cc}lrCici{%e~7^zrCaFVIts) zi^sPqBLX8_5n3Yzx%F~~h#0Zk@>8ioh`u|4%`m+d0n<`=j#j*GvKGtITt)>gwoy4! zwwq!wX}yk{=HFupic}}Ldc2t@8;z&7U@+8jiGfPp$%pyRkEyPZ=9I-amN0F%(3?JA zG66xnYeC4N0H;GM6et@y*5BROBI3Eem_(xQe_gviR?u@j>(8(-KwxA+rPkwI**ww_Sb~xMDVh^C!9%d|;(qbj)K`!4<_QT5 zYk@CMgg-s%eKOa~H^yOJAUFWIowC+DND`NYbb=L1uOaTZM|`>|FQzv?+kC}!M;_Zl9(kXgyGBsO_9R+HBZmO>A3TF zVS!zbd7M&%3hZ)W6P({>Qwp-KOfOQDD{N^Vl<^b{zbRK+c&m*oH(W(xJW+#04@(i3 zU7KIu9hKkE?X4bas_ts4T`x|Nq(QJMz8_i=jq8IcPTlL>TaPn>ntfMSweTHjFxPvE z;v!bg1e+gkK~oC&x7NPJ{NVVxic>9I+#j>dSoGiHg187Xnpt8 zXh4=cEt-80Gpva3cLLuiI<)pulQ5w@xFpF@(6W=AZ<+v`1Bb=Qd|j|@=S}~1M%5|A z&E>Pkpxlm%`T@@TyFhZpK`_&mKe{6?L(VQ}D?;45Vbp?wG~>87W3u)_2-L%UJVySm z08UTtJ-EIg!x)*}*TD4JU{Sfx;+*L9fu&S4x-Q7Mz#*;0k*$o+Jiyq3JF2E<49a(Z3uG>1_(4$GXt0a9dtaPW= zX-8^zy1K>E_^nPM@__-_-c3f*j_Y-SxKGG7L3wvaahm{6zL~!Ah%K74VFB04+AtzV zsI%}>xbxQ^1mMs$fK&Pk!eqo52wf%GKx{mp6Y|-D4EMw=;^w#_+wK?F&f~ zRqtaNHbgyjMHfk%$n-0t=tX)#v%8FT?@q+tEX%jy_FV4uA#XNm#Z>R5`05v9gx#e^ zz-0wJPQB51xP5QxIDjwCPh@~;eNLYU%D0_+k+pVw0w9-hH3^Wy2zc|%HU-xr(5R56 zGzj;ou7axsPm#wbMj8M7j4pBp%1Om;3T2K( zgxdHS*FNR1Ujk-EuOHNJ!-cO{*Vuj=Iu>L{mkVQJv>9tg5dGU9=09Nz3b=0T&X`2@xdxy0}Amo zsJE#$m@|k1f-p92m>EaKqwG53nNJPiDx}>)tc20bCQ1|RAJG3cEzX?&A!zoMPi!?S z;}Y306Q+wz^^0cnJMc%=cTL*fta$s*kYm32pd1gJHK(QCxlqV#{J%)&trOTvx6h( zTTTkcI;kH{d@gJKy~XJSUDpo7TSO2(9EEh;U#`L$M8Es{X&5Z_RTpWt1(8LjpYnLK6qw7$Rn{q_;;1S2cMe zkXPbF(YC>p8hhgY(c_$HD0oIZB`?%va%q+qvM|%HZz4uX*KA#nf^iCfKGSM?x3^KL zYe~_^RFSGC_miBTp9k-_FFv>gcPl8&n15y9_qbm(Lv&KX7t;tfP5a)|KD%YwAQhqqwOIjhlnhiMcWnLU|FnCXBPo0xC&;dJHyb4Kd$#42X<91?dQd1BZ{Fkej|~Jo(60${57i zBKXJPw(kfuU%c2LZ5TnSZQ6O?Cj>IVUkzRAYh@6S*;$4GOUWhDxufz?V@S`Og)?6ubt(zTjm&m%wghsy>jF>S!eG@t&@j7vw8|?m+L~h3dJzn zuqAtvn6d)l3iYHMqPukpbIddh0TiA!xN$}&#kub!d!B)oW){7jpDqCd=klVuF%0J& zmDx=-`_i#6M`3}$u(aUi^*qqq&gT4w1X=|W9izN1>L)H-d-*SfM70D}UGAoYkDPEP zB3`2Pe8E|!5hQPp53nY*kYw@9h;<;Gja)QUa=XR{HioDa1Qwqa$Ix76SbS!zI#G4A z-)HZtsI0_1UT7{{N)mnlyZ$oW3-oFKZH)%fjd;gykd9m`t3qCaizk91qPx(v`a=l& z>{k>?I}TG}93Lk7MLCh&l0}kg+|fN1#W7ys z^Q7NzW|=4Y9+~_o#fb5|=cb}%Y>9Hsa`6apsK)%rho+pm<5lYfRxNzS9%;`6M3X-n zR*ZHu%Z+UfTBOZB`X+=TkuoTTXzlcXW=0OCg+>4&Djm_UR&nQ8BF|vSru|Rpb1yVU z7n_%;V9y#`i1Rq*153ee)Yry_#{(3&lRViTWC-O&Q-el zs8)5^Qt=Bo$wdM1w;T9o(wA|tmA;bkmt-H$+9GGgE&^xI8UfhturWChRy!J8w57Dy zF+D4{L(}5g5jNfr!IXUSEe4pPzZlkhz-sG2e3k-!8pR5*4LXsIMb-ejpqK4tB%*Np zV?EknjnM3G}K~4`>h;<}3+1WW8rKr5SV68~~Q6HR`gBr+-Vh$h?32Em?F=;`N^%KJdAF%LAp@vr)+s+Wc_1n@* zl!DYqIgyt(*8OmUY4+)rEvS@NFEu(Tt=(u6waCBXE_SVsBPfN0C6iq7sJGy$3k;Sd zg~bOIiqv_N=8hW1bP8mG5Nuj%X>pcAfR+&5K)GnwT=`jR{g(E!xpcE?#@Q;7YW10DTX1)w^ z-@F1Yc#3cue$)mt1esZ>Z5;MvOn}73+v8#T-Y1^85vH`)GqRg!&dlv8^4v&-oh8iH z2Q12io95+?aZXL|khAI>02Xq=Yjb$?$7n1ErqOsxCt-C@uB9Uig`>bw%w-O74{~`S zn(f;t?BxXMK`q|ODt$s37`(ikPaNFr4%W9{soNxBceT`ayf~ShQ%^ByV01Go+Q%uDt1f}?&Lj|q@9H6?gi9t;ZwbSv(I_E^)^+5wj*1Ui++90pJgk~ zu@R(X`Hg1C8v3-^46l{8$&+)S{}7CGWqtL@$WOj3$YZ_R#i*IaT=Kxb;)_Q0HVGa= zVJ4I?IV@~DJ#-mLjL($q+D8~iV@7t*rw*&Qh4N{~J;2lx64n1#9=iNy39!)@r}Nry zmkN_tW}7JG6^2S@=UAZhnVHQ#$|q+1o;IvyFLQEB+Pa@Ve|`W3!4{`%c5z6K(Aes~ ziB-t?gjPrD#sh_Pyz_QQ@2!>(Gh4XkM(WKBz)D5iZB2GoNGYxBjTFsm(;$g+*Fmof z{}?%(Y)(2+{F2V--t~K!Atk@D2f2h7k&_)ea8yZuwr(x29kbpklo{>v&h&|mFR$fj zgGZ_6&YE#wOWtWgq9R@H4BZtl*;#9M@BQOC^2uEA9bqw__L&`7qV4@DzQDPDN{}2x zgX+tI-Fg8Kjx=B^8x;eCAch<6O`o2_F3l!l(6hM`UnfSS-8O6}&>t>{ zuX9@F;B;Ydiba1_p6ii$c1u%+X`1a_92RwzD|w#zL{`xknf{1*iJSYTf4mLnX}`H9 z5E2U+!C5z|UJo@L145L0Z=X9rK#%RgYpKr=hKSHuWLtH49P%|A9J26#0!5FiCizb} zxWS{UGJUg@{P*7JMKkBL6!4;~ZO%Uuy_P*$b~AADT(4ZE-+!RT^f-jii&5sfB~049 zUx(7hV2xVQwV*ayzvqAXFebx&^RtEotJx-b?Pa&>$z;qU@)az=B|w5$)GFrLA(~?6 zqAIBJsmckv;MF+zqI{!Y92~;>AnET+$T8xS*uxECAfww1=G$gJ7&v$?c~LYNlZJSh zHjBA$-)2Y$p8Jww_d74Af{L|n(SK5+1r79(Jh_~%*qn?%|234oPnIRGJR#k)A3f4J zvaG%6rp<@8XoL1z74B`}+M`w0zE_l{U>cd?FuOfhr62|h58GaaHU3P-vkULzcoM_) z?pxmfaF2I_K7Mqou-5w@{$U>rm;*qAw_@=#BQAfv0aBb8y4v<)AhI7Q{paTARm$_} z|7Nk@lej-=z9?5fojm{_U1avkydr3W4_u(B(^^MWUDV6|C-nIi@YmjWENczgl+S*a89-eGm5&|d_}z+CQ4e0fy^%2F z;lph z-Io*(dX@twXn%NDY=EKeBnl=!K9H0udNK3jA9P(4&Ch~CeHi#WQt1wB48H(~OK-|# zttVeU2&{p64elkLLtr z?N34M!Ka+)FnU2Hz(<&9;sj?%yAAwRDyc{Jf}eyjSTx7Ofd;nC%WCt=*|+?bq7vG0 zai=@yPSYdvQd=d!oHAb(4a>d-thau40dO@0z!{D+Ew%?z)V~j!in?&-JrorLXFo)~ z4yylRRLl9G=#==a?{22{i?`n04(~{n@16dd<`&H(^HiyEQ^nh26G$7Mvp;r7qCf4! zU2Y19gRv-JrSEo41|~*}x*(h}|FOD|xP?w`*ZGV*_SYXDUl>nbOJ$22&Tr#qxW+T1 z9Uj}V)2*?J-0k*;<}h*`TB?PqP5}^_sK=+12Q!ArM$q?2zNlcN5)x_rgQHPE+ycb; zAU+5)y7@yJ+Fp%#1S&8uxutxx=Z_mq!E0T0jRGY(+too-hwf9qh9X6L0n$i4l+i`y z=POm;GN|9qej)73m@lZq7O5g6*jCMy={e41Ig#(FA2rbIVclPF*2Zox|F4Zy6-)w@-<8w9P-w+o^g`dGjwyF<1syRCl;KkM%$C&> z=N&mj|>p%*ecap#2r`QeoR1+umCjnXYv7)4G90f1hobJVEWku`}6k+_or=; zNW+J^!%sMnvj1G^hy#rteq41hasKaDsvm&a#WiZJ6>&%i4!sVLi9VnU3xh3xaSYz; z(TDCVx9YW?v{b8!Zz|AhEWk8~6-`E0w-Y1`BXVK4I z9j9`LGtZm%`$$_xc^D$$MabasYu)INk~bCVOKa#;EX1;k_ zvvd69z|5-Snmgk#T@{!UN=L4<=@K@ljw3&H(2k zx9)TB&pO18_vAveJ9nE!cVp=}s?HHSg)9BJU2jFvM$s-Fm!XA(X{_nBii}+buObZT(C6zufxW49q0$u zF_Gk`pZ#DYL!U*}R(p!M;5J<8(w{2wSepfWBa50}T>Rx0V2qoj%xGE4J3lnsMr+Ds zT>CyTs*!#-_`pkN)IZ{L?58RK-eM^QNY=n}N>b5*70@}$UXpA4q6Yx!|FoX&oNxx^ zcDar>Mh*a7y5n-2N8EpVu{yXKE_9R$ta|6KOlj5+7Y+pT@2iWzQ%%0TJD9qeRAvzn z)8Axem1{hOn-5dcTcb%b&|if0=IQU(S-c0%)h6uMNdQ*2;ddR#Qh%XotMn=MNs#8- z#KY28&LIeD`x;$;FzJ!uszK-VU0ht$j$Q=f#-4AP1go%sSHrL#Q%5BgDT7Ro=ZXq4 zsFIEuaH$4d`c)|booWV@v=n%UEF5@^M(^o?$Z5QERC4_z z{jWoznrQ+ki_u1yYCbr-IK@UTy zjximnGWe&@c!p)I-JM4~Ds&rZzCcZ_lWoO5H)jn)t2IKinQw4QluP1vyF+|i_m*d@ z(Emdu4(0(%(qRC8b5NmO8-dD=oO&aUe&Z%ng{!4hLg&{QLk4EXfx5|xea=#L#G!t0 zS!Q+gnTPdUW4U*G<1^sn&V97#$CW7`3_dsF|AB8rsnEVd3jg670VH!~Hy68Yy?Re` zYS{O7>y5>1a_jINMzT4~AB@wPN?EKjho{H`}tC*-8xUA zx9kS0N3?7bxBYTeV{7)h6D%{H8J5vD#+0YHQvHT>tjisen4dnSfpAhbma9d~4;0SS z0U5L)~PMti5z^ttOww(wL|J%za?NUt)Hvn}nf@ z)P{`o4%p>a5vS}-iB;PI0Uwe%<;wG#qiHj0Sy9T|_>HA|itgJ9n;Hp$B`FeyDtjKh zK%JicP4?BMzbGV^W-Qft6{Z7y-y0 zj8#7m>Erh&aiv_Min{e8{cH6fMb{DcjbMR@<+ex6qe)5K4Cn?E|bDO&0$9_cLAwBTJo0>2Zyvg(#}83%^-od(+={4G=F^=+}t(^4VjO7}ZK6#3!==eWv*X1m^R z^Q~Ltmn~z6X1&&{dn+j=YBY@O-S_fhCA|;WLF)^M=X{|p{j#%dsHw*8>vS(*y9NOZ zngS2GxMfzddbgbT7mJYp0SYl)F2VvanuZE@`~I|x%0(7lH&tkeuN}BP;{b#j94=%c zGo~b@J;sU$f#|s-@;J};kYjVqr<^i|KNu+7+xx=$Y2bLV);|mN>g4}pS4;Udq#?+c zh|rP9nKN$4tNxQ|vAs-Twt;S#=#;fo+-FF>0aUHNUdlXq7)iX}c@ zP@-nD+4+w4L_UKq%8Cbdy`zvvKB9a7kmX6xv9BrEE`9SiMTq`d?ns_aoqy`2LzDu_ z4nlrJM*mF`idvZ0$RZUhzqSfEU>Dw5Em$r~87fgQ^Aqk_Tr#e=qenGbyUN4s4dtrF zXOet&EafN$K?56Jr6bt+{=V@{g6J7Gk<=UZQw7=6g;7n#wgiPcWlHM{^N7_nwvi)y zb;Gfa9M&h3{CD>9Y7za_M|Lsx4s&f4X&4qXjWQ{0|;DCP!F)`1_Xm^eubtW&pDOZuU)pat+3IZQ~W6&qw9FOIN#di7uwL z{;i0y5UG^;5ZQWiNb5k6MJub~Z8_rkc`4Je%F}jx4vvn`b_QJ&(nB9qxmCPTd5t*= zUMET@))3#O?d?DmW-(Zl0L;B*cQQoV#l7|%>sU^Y-5H&}kT+n%ePIJq;-AUH z(eK=21#7%Tl8t9b4T7)#Pl4i-bT4#5b{%7?;NwwwV@I^}xj>Q2PL;6_QUL?(1HR<5 z=!R#olm-R-SWZ}|lFPR~V$q$2WR1K;5;fSKy;mb@wHu-*;5OcHQ3>Dz6;d;H{r4Pr zDerv{;ml6~@=@`x7^V3gN)6Q4Mv3!d-3$!Y?@dIfdpg+(yqg__d=((KCU@7!EZT6z zzzQy|QQNiVsl3kI8G)(6Kb9>JCEI+o>DIBtzK^T;ay{6R9Xe7s?q$2yMdB6Ar^@sE@+@()!E^{)9 z{^_Rd>^kpK(1dcJ-*BRFLOz5qe)j6EH?zso4d6XDvX`7;=$cwWBhHFEvdJ_fJ<#8u z0N3To)jX0JtGqQFfaz@eh((pZ5#K!{C87+%41y`r1$L6BB#`>lYia)D$M<&Ke}(@D zpXYoASD#wD*KO}0@BWo`@ui0&N+!*xoCVN85}swe5jX9ApT*CcZI9SSwMJn38LqO` z>gniHx5fs8%LI2MddeT6#M(CItEzyKjj>qn{4%2SvDCcl_MkNQIDFU1R^Q?!hCE#2 zzV(zV!ObxVLx6zGKg<+sZIpT%q&Z7;NAorSyypc4++Z{u!6lz;cJl4q$C&LcQE>)`DSW>j&JFE}Uue+2u6qO_N0 z_Bw)q8X?4nCVnVQ&gO1P&C{#;_QFbTztDY29%y{>gjuorenMQiFX1=ps4LZ9Y8e%x z+ceD%Dpc2rS5NjmV$=VOdiw3Y4w;J#;i zJuq|WD{E3FGoOkQds$w06H%169~3WtyXD?OQ4Z_I34yE0W$oruqMoLFesHuR7<#2s^Bw*@|Q!u;0`FhkmC>wv3mgQ$qHb)U>oYeuLO4omjFVCdiU_;)6| z4WiRy%kxh#y(cC|W-q9-Sva07F;SQ+R%;NeZC&DBz_hx2H>_~@d_{4y#&%bWkYmqy zUZKh^?rcmrU33Dk!YT9EaXl(q9Q zu;csOonIO^B;j)q(yeDZWGhS&+6k!!a)tA^^`G*Lc&tuuGYP^?3w)LuR^Oa3C)3Ke((?oz;Os*Mp`GsAa$ zD+}E#BHlcN=mm-Sl+2|kECO}yN^}ex+-nWZdV1{(Pni|B%5bk1Pt#}K_^LFvzr-Nk zB)me%FYyR`6Zv$acPoMT9Sdi+OV!~5MQ^vpmysfVG(QQZjDGDaInP1Qb)uLosJZtj zz~=JbY#xOnM&)=IX`m`u#*M12xx_brC|W^x^uI}MR+TcpT2X%E3jm#BHagefRh+Vu z{2w?E7Pi1u1`>IA1pr*DSiA}Gk(bT-?5K`P1}odmd)kwozv28Jum$=0WZTwbB!0VW zigy$O&K=L}i?yzBAG)Hr^PUj&wtW?LO|VKzfi(g)INw6U@neNf70dr zVRWVIR5W7_tc!Kb!%3wf7IW0}QWpl;m>ejsYX9m7!ST(ukh^PccDp3;TJF^JmTqKy z_zW9OzT^nhDPrdO<&@WmBKZ5-b2ss$(qpkM(p%xE!UBqhuyD6=hN!m>-#ld)qn)e2)EkZ4psbfxvktfVFZXTd$ z%Z#r;;<}9T&~b~c&kt2K>5HU*{36B`thspt6@V`AIwF_%eOouwqSa1YVwgsLEVWS2 zhI^vod#kj+KL^8w`f{DY$Z>6u``1_!d%=EE4ZtzWU~4MVhNCJUX@>q9rAa_+#-6>v z)uB2K4dhZ>w_Vv7u4d8eWVH|#*S(1MipOL9+Gn+cEd+!<>8_ba@;VChRp8*>Yz#L@ zD+`~U?4B{G4lhmN$Eu4S$42pG1MXx)+jn3gzjZmPa$-~J*rwNm^yEul{NCm`y2im4 zxNGSuGHN`YoQh95;P6bF4HHqmgwHO;InE4SbbOL!V@ircbP`>K&L()}8QXvyfehy` zsb`4xykO$q>^fc$rt&?@(^Y8sD#NfiJS7{vp6d!3zoP=vu>F*#MmV# zOghBB`?FZu-S2#CL(fc!QsA)j$rDEep;9XwkAwOXMBB50h09ew-D86ZgqFlti}HP?PXDG2bWC->z7p<-vj-x;L5hRlaQo{1)t!*# zDYo6EN}yPBa4+}|f&thEIXFh3E#4m}wZX0?<4@{exzO~!@MxIxB~px%t(}2HESBm_l}xVlK z^Fij0W`=$hIcl^3a>@{)1jy5hK`9-6xWtxYPcwY&uTO3)R8v?HpLP|uZ)x zw@kG4~6B*O!8MRd%pdc8o%d^@$u6az}ab@)2jG&JB{s`j)chOsdNBgva1y> zsR)R9w@sd%Y?=7u9#C#D!0H3Omkca;o>VJQo}aUCHLiZ7jp+=B9Tk{`F}YC5kL~pD zbSPEZkQ@4U()aGI0wEP2*)*|R=HNxtY+bF5A?WoD z{g}v~QRDqwLh)ZfUgRtg4!ul5X_w^9`#~uX^jarRu?3<3x&{ZFu)OR<9PWO+wAgfp zLB-T=@4a8`egWdpTpH{8%J_Yxd(!fHY(t~t+NT>9Ar`w>hqb|c_njtFK+XTd&W+s% zP_54uMQ#+p@h$^Wzr$%T>pR+Ff&Ck_(z?a;eFrH3dM~RDW++RoKr*+*HFc(kDuLXskffk`~$#P=o z8=Z5iT7`?AW>OoE2(l4&`eYtv{*rKwQpVLy^ol9Q& zzuGz1uq3lQfFoNpVo6h@wwj?ksc2=-l;Nx`N@kX(Vh%(}c*!VB8*|bzOF~WUHkys2 zm4%wO6qK@3RPx$@7uHI&6ors2YrJ8Dc@1?Am7`cbyZd3k>?5Dvhv$D@-uJv0&N=_z z|EHXV$jWnxf`AfZI7;zoa{O_Z&SG{f+Ly@MZ&vEzoyapSJ>0smky|htblc%fip2Vr zy61RSK`2^$g530)kh-mGQjD_^#MvB?v<7Bu1o(J^8VkZ$;wpE zFdXZs7Hz|LhToKG7LxuQU)wos_;Mq~I|YK;@#Y2&O87EU;n&T{?k0_=;5W4a6waUV z64Oo?%c(xpaju}8(_u(`qFV++v0{jje7_vwXpuoWQ(%haaOG^3qK z!a+91>v6@~BIJ|uXpL{tLSKC(-0m-FEzcFT_Lokz;Y-p3PogCUFr<*a-9wGHJlmI9 z${tbK!@qayzGU-9ANurHz6avSap%h7Qg_7%W%R`KP+M#`Yqkd(+LGFv^+^ORByB7< z7AbC7v;xeZSQVF-R#Zf45)`Nv0%z!_>ONAgUeU3<8WY?AIkzG3#b%3Of(z;U(ufK) zl^G1`tbK0Ae6K)+Y~cwg{Bah;UJ#m*&~`_%OJ}=I;&mJO6AV!ofhyf%r54W$=*@hc z&PlvZBpzTCCy)GA(F;ytP~t%(o=9%i5~rB2CT9I%Vi28! z*ff|yeNh<`NpGnx+K^niZ|#!d(XE3& zkjtZiq@hjR%SfJ^dE-GH*>08n9TN%fi_l_T$U%BcsDTP2R^IS+=k&Q3NL1{7>FdjU z|H0*6)FC-IoChF-c~3Ux?XUbFC%3L2sqbwNNsfBEQ3kQZ!+8#gd`nI1$CK-{pB)+l zgqEM89!X1~cjXe>JSJs>A>MOUmD$6v*S@^gb#Pb|s*wkHXVVb+XwNXfRgO9x1T%ScSiHymb{~$&ZQy$wI=`W-#zY9X8>=)(ZVoQ zryqC=M=>X>W2H@>m-?-YBQzeLsr7sCUupezehYIkKD5hnAG_FbG2w(=jnB0q-u7nz zX)HR!k((=VbaCl^E0oVcyCGT{c&{1d$(n(|IeL5d@})=a<0gsri|N4;etb=hR{=ZrNuu zF-j`RIn*xr-Z!#{*L@=yLxmutu%@%9E3bJ{A?f5HPs)WYRG{@s1|Syw-1C>!&)Lo!9!QU{_O}6A`%_7`}?xKfzRy)LEp@ z|I{@&jedXJnm;JzZ2mJ=#{a{fdgD)K3g7Z%?Q+v9AKD;*V&i%0h|qyHJxm#xPqTw;frrOo4}9 zBF|bspL$f3932@ZP*&1YX41KfgVQ3mHAsD}tWDK5s#Kkkzso;o7z zDmZ?4DF{7-(3j$1d;BlV9h#znf2|B{Kp>^KWLN*`qKgj<>2yo1nmWEk85)Z)l;U)E z_)mK*KM9S1<7+@@P9)-MDQ+PaRCTH>C(k&yG~F-oY~^0LXMcWUvQYD5y~~Vx`Vyx3hMUN{D-vQ-iPuDu;rTn;Q?p1r@uEn2;V)*PRy=2};s#mu~! zh*2k(5_ZRr&wUSK)BY9_*0+Hb_|QuIy2mlVk1w*077azEuF_Ub{mndy_-t%B40RYy z%W9B!p4^}renI!CN7w${Es?hBF7PZmvxoZf!ORA#x5(<{wO6o4d*8XO^Q0g3bS+)$ z-SqQrNGGVi?2m9PXRnuTeq=lM!wAQE1;>~P<#};v6OHf*WEjfY!W_P(2?9P2+g$8e IwqB8c0KAE3G5`Po literal 0 HcmV?d00001 diff --git a/ui/public/images/assets/demo.gif b/ui/public/images/assets/demo.gif new file mode 100644 index 0000000000000000000000000000000000000000..be870b7ae550292b96c5405aa56ceb6eaace9fd5 GIT binary patch literal 4137920 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%0A5=OQrnuC!{43NwnB1_~ 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]pUbeFewa1m#z1f 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 z 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

~GN{9WPOy6vNG3%$PA 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Г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

ql^l7ntF4i z1_^IxvtI4|vZ1gW;UY8DjIQj(6|MmbF8Qs(%+NJTag=es%3h;>vgSnLMTOjQ5GZNv zK-uv5!GKjL4api3(U;3}s?{7~uNYCmp%E?{7NGkfXPo;4SyKfM`FSRN7ayK9dpKwi zUymTsK|Uq6BEAsU0ogQFq2&PitH8FiNsjXlc;G7P@?q#xa1j-KpzC@;bkXYus0|-x zOM&k0JF4YbdL|J_>ok>4Iv?nAMfTO55EI|mL;YOAg{Q2eeia};5^vI>Pk+LSmz$-D zeCMfx1J7LINqITHO_>qG6O>iH8K4gt6}BqHW#75ENhSE9X@|pC1Lq>Ub6>rz1wbgZ z=RVEDupeYGMi=4o9?vrji-(L{QIlLN(X3_>HFx_wh`v?8?lAR2i`1~B2);@PFJp-%!o6;_>57fyhXRv5F0|Azu8#bjrO6D8? zLS!E(aGz{4qF>A5BUW+?zs{pkzs05^TRiWp{TyWmsaZTMGG7QKYcEIFioFz=#=?*}^GErHo zn@xtmSWg|7b-Rh~VT(gwd~la8gc6a%rbq)t6}phpysb%eVGeRuwyLMIK-V?5J_=t% z798Yz)eM|<*M)6NQUwMgI`ndmxNT4(^1p@sCPHk95T1R|bNYaO4se!KZ*D#wqvhjMY-9gA@ z7uyK!-l+mO>w4r#^JSS(d$TI~Y{cBYa(BaZhg&C_$fe6t;=g@re{?(Mo`qoEfg%b! zGURSq-vX<1jcbEMssjo6c4-;9Zc?S1o&sk~iA}r4o^Z}{SQr9Q=M1A-98}2|@>X+4 z&rL}b9iKu`+4e`&Lv^ce65b0HqOQ97wg;J z#-5ESeOhWxcy!b*SM(I;6xe)S$b0B)Q*@ILjv}rOx%=?yVDkvlpuEE<^XY|pl2_A9 z%*cU(9GMbqW*u_3NB~Oda5oje6D`wc8qe-%F%dVrj#u?}K(e0|#!kQrinRz+U{~p& z5SOQp6L92%PJZ6^C3Vsa1VOCLiYRAhx2kY1E9jM43%xlwsMl4&ffPsruV`hPi~G*t zoUDp>ELQcfcS{$BB^K!N-0D)*^C=+z-~JOfQ7*E&-}OdJs`2~ZMy<Tj z@zV_0p2}FIWyX=CUQL77s*6U0uKnQ&_sc45pWXSs2tGn7^^j=bJjy1TivM`jrauc) z)@WQ_QG4~BPXldSD@ZNR8l0$Sdn&q2qfxI}*HGb9I6tLM8V5gq7e?ZnwN4c71Szln z;6pqWP@Qa>(4a?d+*(b1#V{Rf-hpTaG!6|G1(EsA^SO58;BX-#itdP9Zkg^p>9kDA zxpI|L_$>dFFw4Pun^VcTzCm?9dzz6q%JDA$H09JmKl8JQKVkRD9r>biZSnFECRxya zsYrjo%9oz!c#N^{mxB$iYTfzr-lemn)yL(<4KqpYML5awW#Qd<{O)Is3qw_58Am-F z3vVp56Egnk5tQBLpJOII{MWvhUw>=!Qdciq?^zlqTIXI`MtFwU5lz3V_Z59Q0x zb1pPLFoPTd-OzbR8FSCQI7uV$`J4{5IEpQDswpkDEIrmTbZX?Fq#%o{yy}|a729c6 z?y4R93q~RvYKd`9kynTBs-{URr9$9BG#;7JhYhj@&=M)D#8&>KY_;C)h}xK@${&iF ze{s!uSN}P`!}`;EFe-n1V^;B9YwX8a6V7;96HDG*ug+8yq<%OI`E?{Skv|!wP3ciX z%kSALz$(j!urFdM33Z|9!5`MSl83p}*MavK*y7g8?+P!D1gf4|H1(%+vH7;EWV)-X zvHzpKvD;H2|2lrf=FoW%ybRPnuw@&CvJ`3j-_4G4MS(7Fr6Y1|zgVy>5x7>Uk<{cM z!7uk`C9(vEp0DeA*(~qoQuQa@($U zJ_43R!BYg1-AN(u(!*U+*N^_LpH+rpckSui$xA}_<#H#*<|R@~W2ulA63x72U=;FT!4X2=-_6BRESXOQd6MJG>|K7gvpB@NOB z;pchmhKb6H8Hd-A&m5kq;E+WZ{V0Y>{G2Jo^rdGen-7;yt-i!pBT>P`XC2146B+YU zDn>@cytXz4z~-q6e?W@Ee18v8>)o1zV%F~_D7jF1!iX}me&$4FvZl}%7H=l`X)Rf& zJI7hJn{}#~GpUO!qm8FRb|;G8HmHoIR(>VV|2224p*a3ncyyjJtoRCkK&fdKpYRw5 zCv~%|yfjB=4ZaOm%q3I40p;J>9!|K+oeKSQ;boi1s-a8vdtfAM|Lh z250QD)A&)|KZ7ZD46Kf6)_ z%8*wWGqoILR)1}MO=rY;1TNRs{dX=uQw9m57nZGmhM`bL^478nwbTS4-DN1NA=0{C zliGQ!jm)>pC@|{NLB1AZeivM6LlHMI9Oni1H%FsHBIaR1CQp*FS8|{e-C=L}YF4;+ ztfCJYmQAoCt_fV!+*$a?R#9bjkPt z?bJs%OdRLbIv(ZDgy|ODIvMrZzphPA>`~KKjk`cqTIY}2cC-$Nv1@c-BYb1_A>;6G z1v#pM$TCd0}5 zNhQZ%_?Q=kZkIfScqfsR`C_radsIi1ogW3W-4`5qJad2Md!(!$io?CGrbE~?q3t+~ zw6LzM@K!xqvKFBPqz|>EU!j`rxtV<+PmT3|GLE{F1;nP(Zd7TM!@OH&l^}A5_q+9N zV%BZi2ZNjgee1$Q(+Q4u+V5@))= zE&_62v{uM3(3Kz_R+Ws@sP!W6B{qs4%1YT%`%{T4?P$W`HJds6HvP#6|2E^Wmy()9D|AnIarO7&J7Y9^!Ny3C3&-nuX8&zzkvxzN z=R}lqgNH=phjXegke~LX6AE1;mgS80C2C~VmOuY(azwxKMkaip1~1*db3N<#A;$93 zo|@}nfB%%Zx60ItoYFtOJBQ$di;bbag=0L#`cYH=2kkBXx-E?+qS+QJLfb$y+gbDP zWm8kXK$enx^B%v;^kdG2^xq!@I|xTjwmxnTS%KHo79Q#1qC$giRtD)RcTpCxmANK7 z<=>*g2YlaV+ohPb_mP%R;Q9BR`>*)*fR3B21V=B3R=lhsbj>(0$pT19&wIbWZsZrQoQ11pGk>rzvLyk zkjMd1Nd_q*{nt0cgY?X}j4}KjvQv>*L>zeFgZ`sgXnBN2V&Q*HJ6Ei|z9x*%GU&s?vKNZ-B63DMNPQ`@T_vl60Z@9H;FZr~(%eYd?*G?ld4&sDV{CdV z6vudz)rQ2RT?)XrrB%Mt+xw^`kM!SZ4x)Y6qY1uxI`adZl8lp$8a|t~A2rA}pa|9H zu0ie(cwXv;2MDgb8u5s_qB*8VV~@iegQ;B)eX4ZRpxQ1_2diu7E9@XV0I|Vo^AdNA zlaWQBKyp%OYaIT_A0Zk^rrHPH(S;ox)%BTA7;%g^J^`Q>WUuCY{5|JTNH3f(k{BVS z5zzuvVqY2@^m@wiwEod+ z=jylZW}WuOf1!5X@ad-qfzdp(h5Jt?LGoy~-2m;haL{Bi36DFSlCDRVIv}ds?nGBr zqXC;Gz1kU<=O=gb!zykERH@K&;*V|TZ!;juYe&9If84*+U)cacyM}>%&6S&v?j_5+ z&Do;i)r!4S9&vK)S>{`$NIs$+DkGmW7(QtDdsN}=Y{NPDe)GGaswxe_$;UV4h!v*8 zhGw(Z>aKR=*=1AIW2t^EB&Ir@Mvvv^v#6OxfH)9u05tvk&D}BKB%4$`?QQNv()O4 zYosF|Ht|Mjd*eqj3pmZoFZ=iDIp+DUO*AqjW%T1t8q3?w51xuNc(XyI zPgDgZ)B-}v5{k4rNehwLzR)XGvp>_awT-Xc853kh#(}|;4bbD^4qB8YTh=C|CieWe zFpEg>A|=w*zjx;|Z8X>H+L>UjDQJr@Vn8QAdd5UCGipqxT|5c?%-h&XPmIPy@mfuY zvshm)s4%dwUe}^Uw4DwFD%s1?dh-e5%K!Yx{<8RSLZqMM(P<=_Kgj5S=NcO_E4TX1 z8TyKmz{6R1${67=i&nAHb_I@IRcndFBO}N!TN?WzC|)ZJEQ!V>jDxT0`MXl@P>TA3 zP0n5!zW+{Uf;lo)X%kC2yv6s!@l6r&hKD$7&F$}`D97*n+;JVb(7Omvq9ZTGA=chA zU{s;!Rl^`+3XsUCiXtc1&~X(elIpg2>+PE!;~+>TuF~Om!O=tV7Oa83C=^q#@{2Kte^7C^V z?!afyGl2d1kFVP8JE!l*QD~Q{I~NKpC|iXgCb*UJp!0g6kwW=^NfMi`!cI3;IIEQ= zk>B`u*TLCDfy7J2h)7!~H>I6HE_5KMI~gIHJ_;Srn{o_aa0=F48B$-LfjAQ|*UeOPbpP?XFkXq8SS~m3{{Q7<&Fii?s@<%_pCHJ! zGa4{*%ommFr%r3$CgbxJ1DJvIoC3oHZiAbqVy6E*y-nU0b)yXVs$!dn%VU#E zsB%8sdr=m+ZPn^)R)RdY#WUW&FQ=9`g7cir5OK!VgN|dbq^t9!Y9zYz_V2$-^gjx% zM!TK0+ehyOZ*KSGi9xWJEhV_%2pxxUe~3-%cH@oOSnG+!y|lfe2Ulc~4=!B@ch#&; zsJ>y7KnA!_%*%>*Z*_jNc)33W+)bjnl}A8nT=2Pi6a7pzk~jrU^J^TTgSBD}^0-jd z5n{X*Id%oI|5lsWSjzQMN*2<3a&gml-t<5vP>bVvO>;ArZg>*O;-jB(hfU%B6-1G~ z2|Xpu(|`iMKIIExv$uLnqn&jHemcx9qp9(LRhw8M;*}Z)VMx5y5!W?%R zszbM})95o7#I;fBXAxcF=#1K2#BOpfE{S|E`8q-?6#Dqf^`{{XIRo;8VsclR$-v6@ z0&dLHcw@|GP|ks`nj*I3F233ql9-2&yBAAqwIAc&Vj}-*l303W7MR4a&xAS7%#s># zJ1?*iD{%51n-s$*N+7w;xbmxTMsVAqlt%^0S52|T{tkfxtEyd%p5X_>7^^oTgrguS>7a1zlE~P4U}4f5@+)_`G3BN8{T^Lq5a) zhoSi(jgg!?`a83O^LecNj=;d3aNE;hNDpk&{JlJ-e0Kk7oub7TJ0pyXc(|n{IBKWB zc9DM2U^MyNo#aSutJ0YaLx$=}XP7r@1-|OU0W00c${7-FEy4f*Kp$s}tDYO8wxk*` z502g1{L!Cvg=v6X*p^Y;0@RN_e3<9C;D<;p((gr z1|p9J92CxG$t(~bfaAvJGvC+h>~$N80jiGaBW5sbC2-2SOOJjB*(NfpV)`k3TESU1 z<p$tuTDxVu2;h;s(sj_`pV?8Kz!-F=g;$G_X_PO`+%fYje zOG4W?%6?@yVo{U`LTvGz*Qqq9{aO<4W~>}mcOpWO_v^J_QL+55_gB2$mFX*nbQ}Z1 zX=Qa2V-XMf(;As8U;4~;K$PlNX@=p9oxyWjPr|ps&!%l21r4R}#|OfBVW2-5AlaAg zfMJf_U!Ng*Q(srJ9m9Kf#D+}s{6*xlAsmkhr@UB}GipMd- z^%nFprAT{TS1LO<;+Jtz+gXa3^Q_-7@?$et=3cLzUo|&#@bb>FX2(s~6Zt{eMj$?R zl$K!U1k$2W%SchTn4}D27SYgp?{zSvL59rwkma=4f(-Mi+)2mzD4GD`D=KXhaa-Hh zD32lgGz`MTt0^3F7r(6*AokEcCS0N!t@uh;vO2pAxGR@-&?H|CO*X-Zl0 zkbap=3qVuJF(`b>3Y;t=Iiu=HEeP|Ez8i&0ilHZ=XByuupELFc$B@%&h!hSp^z-XH z&|6biPIxj%1ClPGXlpjaG5ehLp@e~IWR8CvoGyN0jbk^ZwUg1;e}Aqy{}DB}t64?=&fNipXn@ zME}dQ{>9g&t(B`;^0K_wV|4*A#5`yual7^3MlC9Sj{KUblnC|f4WQou%~JqGs-eA6Y_FK?U#pG@T2tc0n8y9K?exVJthk5mE& z7pJctqX?+JQ$4)!hU`d@-CFJS%8!k4NL(kkWzk17Q1V>6KeGAUf&3^|27Mr=7zr7jj-#pz>p%v7 zhz|n;Oe)@sz6QwB$gJaZ8HF>U?DtF2C;e|l z88V&{xzpa-lCz_C!|mQ`^pn{@4aX6>0^|^zje1G({K2^}+CNltDg8=o&u{C1(YOHJ z6$?MRs|VhZ*Ll0Yd8r&Qc@<7b2I?sQwT-Vw#6lsny`nQr#{0KwTO<^!@<-2T6M%u* ziF@X@d7YnY*I($V;%ZHmAND3{b>Y$ zqO!apL#g{nmP(Kd8?e_0eM7L7{Eufpl@Uj}5#^BOA`@t0pR*`>K=Pc7BJZ^@9=G!* zY%rTTd`*rrwqf&w1m=a=yi_q;IEHoqZkWJ29tkCmgXk^6x!YD!7$z#em#rT{J!s&{ z$KK3s6C_^t3Z8m*%qE7CM3-fMpZ}te#PU+V^sGJ~q#f&_KzDM-Pz@gCr%H>v-fAH; zGF?87iY{p3aUNu)s!9gvxp(O8$Z8R1jUzEH^%HEV{xF9MnHSvbW}8`hrNg6}g16BP zM&}J!ImxIWtCEdK$awG2#9c2s_uhaYM7qO)*X_;?m z`x$c7o{3xc4@!UAwfZ=b9?tK3UvX-#|&=ePU4^{bPGR(cd zDHz(6C8>pvVhMgjDG&N)9ouL}qNBts27?@qFpe-#@m@jMI}&qYFv`1Qd z<$Lubgn^E-j+Zsi+fAB6N`trbvzqIxm#!W5sJhkVe$nj)ciL}S$WcLhQw}NnMT^~o z@RuC|NIR{n?G6$7YDt~JQ8|A0ko2O3(jQ70KcHXb_sUys+VQl0x+d31nVL!JMG2Sr z4+W_%M|3wIS3zacrsZweW`0zma4c_tI?~{N?GoCK>)4ZA?1}CI+-w9|3B2@~ ztjoSJ+p8P_nUMA{JQ1-e|Eo{v*R^k8NfTN~4VAji0!t{*6e{8QXpYGzG#zHcc1(ZO zs36OO;PYt@Q8%$;(1&Jy_hfx^QUx!E*93g-^V?RIVTwW+BoPg!-ZEK7^#fA&dFMu$ zXzcj@uUcI6^PS;U;%gF1vWr#uaIS$RC!LInsb~%&n*J`{+rTKnItWR+mO61s=(f13Vcj*wJo% zA9q3)uye?C>&rh1%Gm$9%v^{oCx)6(@2pIi()XX+Cv0n7rWJmK^IdZM1ioxe@y|iR z`dhQ&w1+6Dp6Ph6qTxjUblE+EwDP}GFAM`r5`j|7+>{Y zNFGZw*Q7!0k8jia+Q*seqofu=gc8x0pC$oR=^7%Qv)+M+n%*=}!o{OAe+|r$p)B-%;Puy-|5Mq(0{hOi#0T43`cDfPzOBSoZXwdNEoLBfs|_47ZTo-&2gj zpmDlm9JIB32~W()Otc?!$WO1oVE0I0zH}akAf(lXb3UjwSp`PAXfOfio7QKQGH#=} z;7nP)BezxFk_Q|zlJ8_#aHZ$8rC&#hEyZ#cBnXg{YZ*VNAcUMVx+AN+pB@KcLb zwhN91F0dP44H7!{PD$&Q(r#TChVT91#&VfumeQJ<*!-MQ30j8=$lfHJchVmY&1eM5 z1T)^jw5k$T@h(1_;!0-Y($?8-o&o~p%7qMVFfA<#S7XAG%}R&j7T1iaLoa)cZ()d( zT%|R`=faA=9?KZZrm)X|G||a<9tuv`GDS zJER}A6C4S+Fz}F6RJ4eAed!w&MuGyIpRd-{R26#1_CW`#Wk6mb{vkqQq|lO`&8L>w ze_I~BRhYD0)g72Xn`^qURzPyl@P%+6M2XuPF<$Hp0I219QkS&_RU}m9luJuDoqH|d z*tKnKhfBwn@;3xB{_6J0V#JFK@3#6!rU<%C5;CX^RiQJky?<*|s*>>G61y}j`7}4; zvHQaT;p>BvuSo!MEJ^%C282MrCwtZpA%ZA91J1Pa7#44P^T z)VxT#4&>ty)oUc6VpW~-zdtWl%MIWXj0J%K3dG~JZmv$Fh5?$4GD|_{7*F;2u#^@} zBR9&7`lAZoz62>|TeEIFSUD1Z-`Dvysj5ferlNfe!0vOOip;p6C0#}lPFXD{UO+!nC`Uw z0Enuuw$8Su;KsoZ5>@SkTLsOG6Qu96gN1I&)`tZD=y|bvRd=`TQtfDk_#qX?6IYmh zA~lsv&ckMdqG32etP-l2_>_Jnh($fq0d4ghyYeKe0aU(y{=biKXWth1sc{l$ACZJM zysx~9Gf*Gqg_QYU=gC*J%6YC6@Y^3l&A%!)Eh>;baX4~u1xSFh090>C&I$obG1O2UzFGhw8l83`@^)6C9OVx(23$(s&I2WNdXpUB0DG(A&}h0V)TwWaY#ITK`-xAB-thA z>&noh3UOBw-6H&aizIkLW8lViEb}@>&>A7wzI+h*?P zG1^F_@q z(tgNFduPID)Ev=fxpt(u+I;UQhaa?`MNXy^;Hzs+(PSV3R|nWPdpkO%{g@x&iKNAD ziPMWe!$b=%w;xqm$FD-WiMM}pDdtED41lzRyDsId(u|FrM|^C^=XJWb4rv5D6%AfL zf3&{=7S{sMiYdSR^V%sf#ihdAJ#m@PJ;GHA!ZFC?myF7;ijZ|=cy&pWZ1ww2~fz6G@qmkZFBh5GkY|#5V1eE#8Q-pEshDpxK%BA0d0+AyZE_l zq5FD6nTnAe^^Jn`y2H37SIXG3ei#NFBs2g4iCOv2Tf2M?$J8tqJ;@}tht6iS_m7HiH4j0K6n>VR}oj->JgM7eBeeQ*u02#q6V?{k{%Md9l5W;N~17NuaE z_f&!}>f4t*hW!Vo#gL87B%pb=YgzCf_`g)wRb6ilnJnr|_F`TKg>d|+7 z!ZF56v^%$+dAc?ox$(gorx(P|)aZFfcyc?&kLJ?Z0;_C8YTQObd-Zw%iek2wj%oC?6Vu?jJR9i z!T5S*0E4_VhP9}J9mdDa?t2xv^3@^;BTIj>hDVPUn#^b;y~_6ciYqN?*SS;le1e79 zdlW{Jk`IM%Bv~Tk#`bldD8T`YQ7rU;zOdUD!nTs*`a0Icp>tJhiG*A>bm%122`+tg znt?pYTUzLM$c3AEPaPF04YO&fHftdO)Gm2by1dNe_~i;V9)=N zSx`v@3e(tviHojcN*^qkfIsRKr!1{;oSov-@1riNvh`ZY*0z_2-mb!h+gd(8`L2Lj zINq}$MNzddX>j=56pf?QKPfYiQU*h?B2@A43p<9k7LTj~ioPa^_C#1A)b!^U z6+!?YSNw22QJLR*aKE$tqek5kC!^w+>(W>KW^RUXLXb8=X8(m@Ist2+gD^O{P$xTl zjg*?+0As)fRG;tvekib`IA8{6<=18`V{3nNX{wC@SqsuFD)Sqjbt_+4ISV;wpi_axo0&Ka2{nNtHYhgT6lHMu^6#62E>BN4mf~EH~3%jNT+r_-qpO6Scj!1n0Rh!<}05;9nOI+$5`Hd zI$M7?? z*W#{$4X`ehZkDEt*LZ%6uA=`g!9qD>-zR4D*F88Clf9{?Y`<9^z<{fIK7@4}PaEo? zlyy28kOvbU9F(6b^Q1)%%?u&dMHl)&O$cVUL)}x&q7}C&o$_M#2lDBS$imVWN~U0$ zP=E{$f83S)C72d?AXE{@L5VH;STN_7!u2@E#5@xMbE;U=5lCZoD_5ZB=tm=yr{aHF zQ>(wiFbqq{QI+~=BSQK~omFAIv6PjdeTI^J#~Uv?NL}H$-f$~}NI3?qmMJ)TJ3ys1 z#^!|9F6h(c(eSrH+7z{;7ca1cw^#v#ic4f0n;?@kym|a{l~9nxnwZFw7U8c0haL=c z=VcoH(|o%Vh5gw8TvF{)E!V`=izQ}9sB*3U)s1-eznBEr1N6QYhJS4j+eXOtji^wolN8zylW$6uX~;(gy4>(UM7U?CqoppD6J21w&XU2+AWl%i!k>oO}Nkv(``W=Lc>IU%o5 zW&M=#t>uKjCkCYnaWG}Q%S}tt4LT$|u}i@Dc86G33-fv#i!a;e5sKdB3I*^?Gxqfy zz5_E0y7GZ8v4RWA&XMd~5btC#jck3yDZqB=_O}@srQ&xslsYdZD2_6!Xy`izOH!kN zJE?moSGRB)Ui5|z@y1uaROiA;iA8})`bmb)*G_<@mM?OlZfBH{a>E(vyhIo!`QN=N ze3+~M1^_7J=&SMcFSa4IJW@$@!mcD6jk>>L!NLo`_Ec~b3oJlfc&hbREe~y=4*HO|J>hzyZDDT_s?KT@WY_Gb)2akS{4uWMePCsRJL%sn{f=c^S9D=%0i&Rf3Od9 z%m>CD899h6nNQcEpuR2yFa$F&^sX{f;#U=6$$aCgpu|EPB4;P&PWqkuW z%g_h4P}>;>AT9l&f*&xwwD!ZhdOf0Y`o1%59({Z~8g_-ZEDWMC0Q1XgxW1gEx;`+| zmFCIZOokrK0#YEv@Y4Qeaf!4mLbPJ=vTP#?(IujO$7z-}{O3-js)jZvVA10&BW zT0hW*F^txsOCuuHZTHyi+m~P>r%t|2@3Z!Uj^g-}q_oCfp;ZTHd=t!qE3}1wv>Y^j zzxf)!?yvk`Y$z`Ov+3Op-EHH1uYZ|CGQro|=t9RS!3%UWG^l~dQOnLczd%9ex$c(3 z_%@wvhqOh7&Xs#fOn^m@Az^WPk+TH$T{fyQw7+uqbnm>%m7xSBbXtYrabJItU1%uD z*hbL^PnqspwRYs~=m$?Eq~9&L^dzCXDolK8V{)B*5?7 zW!yI+icTfPXv!(YyqRfT`ZaIW^{pS32S6AaDZ2tuC#b+~5)`ppekVfN5c z9T496^( z;@eHZdxMrWav&R*w!0|A+!}&2<)CC##ST`7F!CWSBVW$O`)@TRRO0EHM*e$YZ$W{1 z{F+;RRZSo={i6(__mC(1e&PUi^Qkz&?;DsLh*=nt4tci#)KT((BSG>Pw|5GJxTK`b z1DD=f8kiq%1ggY7mDNV@1$@BQ1tG3UYbe|3GG3Nfb=6_zv6~Q#VE?=!C%-8gGRB`9OcEk9Aull6Jz_ zcI9dHr9ZnuX5H(Hyz_FFg=Y6R&#@~8BG*MED$Z3wf)B<+%1%0>gE2?4-ULb+nB~zo z4b9Q2p4eg8H@2@eW4G}u|6cvl6w+!2y`6^t77jA!0m&jy;|@{NXa9j+MA5oNE+TC^ z-viGjZT}iJ72+jOp+9Fk1oVHB^p_-+3t_pIVFHoaC_Y`3rs5eC&r57-VnVa>3>QR_ zZFdrQ#A|iIOMBr%H(O7^xptAV@2}U9W2ffQ$zxmHl9`5GF*qx&qfJ`()J(LUnHDe(t|(prZ)2|ljX@|rR-brk;$iamy0r@ zU{j%G)}HpQN6}^|#ro^(gy|Bgw00<@A(0;>ViuLGZ{PbkX~VAA@{iSss9x2E8~_u# zE`LkBMg}Bpxkof~S-bvTxlo4QT$wiaEQXq)UYvgu zW8Ly;80okd?j_g`xemtUo@5~)3qPx~QPymCh-BoqJr?<2&aYDoqObHf_JvI+L#{?M z*i=z_)+9z=i4afiAcHP$h<=;$7#8}g)ridEKlpzBX2@9ctG1DwV=13*Jr+)l>+88= zQ&!#n>)EX-c&DO<4Fymn*SXX2glL_dgGg;u`DS>;aBzNVALL6!M0Ty;xqqhL&ba@% z^mNz`bQxYa$(|Ut+1CB}>cqZVul$SoRlqS5(5=$BE<&)(DOyG zBAT1y=O$RD- zOi;`-V^-tkFlchV(&=0bOD)z`;S4+!nK|oWdB>2VXcmL%7pFauWuzOqIao$sqE`KJ z69nz_CbulSHO)JG=|o+y5ML?NcL}e42)IHDr;8jiTg>%xSGVmIqqR{0j8I8@);euL z+t!WS&Iv#JjrF7-_A-k)M8b}1UUMtTn`)RDBRjmh^89n_6)G73>LhtP$qi(ze%3Jo zpDB@HhqgBCesSUlv=*P!-KuTb;+*(fJoT2=Rtw<PoBh^GO|*#k~#*fP18SRcdI@UJCji zBTH&Z^mZ8zSR@=qg#PY}7AalYr}+Ltm;Ny74;LT!V!ZEpaFmN19BY!2L*(_d!OeyenCHa5yzT$5;f)}{~k)_u4oSXeZ zW_E}`qhPHoB%$^OKM}YFH)oi*D1)J6b)K$c9Re^Q>C0%P-OTJqjbYad%?U}0UHv%c z^v=RX{voXz*ategS~VbFtEM@K|r@ zo}&C137u@x)on($as#k#^ZcP3jODuhg{&T_OX7`N?n6+nGNXn;anX&l*Yf+)nc6FM zs#{-5M$=6yv()YAV%7lz7ClukW_*M5qY!aCOv~|it?zmt=I?fR4?2Rm;@j&`DWC*Fh{$<1=VZk4xzrpfXj zl-9|~{ipSMwVfE;ZQn^L%$?X(d|JoGy z@)VHxdESdk+%J*P(aX_nwoKYvJ7cFX+f%tzfvco-kRgPkVaDQ~J8T%lJ=;_> z#FlFCzYLMg1=!sH$lQ;Z!6@beM(WcTq4#_&PH`h=E) z8~OwV%IQ#*FgycTCfsWh-E17;s`UOMbQlXE<3Tvzp^`+a`K{(W>P5nzBN zJE8gNzA~0s;XR=J%DW88^RuTG+A?<#rlEG`GjIfLYNhLAK?dd|C zsD^b_(Lw3^Vn@!_X~Fc;w(veL7n8i(GzNy;Z~O#97$`OXx&H!DB-s%gg=uezNZRk= z!-nnAlOVKm>!|YOU&!z4k`sE`Gh~V9OoW)X*IcKb7?*DUF6_k}#b-cT@DJMuKR|mi zHT{;DWIY?z)x(naq&210+_%%u^m{D!ikr*`s47|BHZt^OSn8p14GCuE#kl(kF6}ll zWmk*rlXDCNL?QKuqZW+%ejXVFD5d;JjU98ZOBf^42E;MjnJ04SXe$i{&*^xTrc zPA)T;0~(eO-EoqQ8ix&SdTc?_e7f0Rju{rHe4))6d$lob(&5=Uj@sqdzf5&Bvd3{C zWz&EUoA6E!`lLH#eM5JQf_0Q?Y)!xPorxv1Ax0Y%E&xudY>D>O+vcL3P*gKb!+Je6 z{N#6$<5vk$oQwOL0Ji8r+o+bD>Z6T#^;`YrtOLrPac27tn;n`K5YsdGkr_ErSEJO# zyaTp+kc6)A6s<+8NrXbpU&LQqI(*LK($k>kre)@<@1n7GCPwRC(d8M0rDMgh5btI$ zV13eGUFAX*bOZ%N@F_GH@QZT=J}cINiuo_8sU7E*w~VXoaYpy)Uh!rFzH{h43!?WE zj(7~wJ$YE&sfOQfa&W%8Xyb1y7CoVR6II<#5*7@3uUyw!e+H{pZH|weG$@G#Q+X|g zMkK4JKOIUb=yPEre{hCBJw$p26sPqB_fr&zhZK@9wr?PUcl9xXby{f<+Oe?fTaBgn zzkAV%Q2YynCk(v9v3K05**GmOiSG+xU!`X_Kwd{TS}2L{AiYS~6Xnlzq`Yc|U0 z6#Pbt%-yXAB(bjj_n(|;v2MZY0ZGl=J6jakUmW-as}tsdd$VD9zf55)B>h}BvkIz~ z(0fv%y6DJDYaKsKZ3{yT8N-fe51PoT#lHcH+)h+6 zMgsLWd(D08eeWjPuIuj4(L6h-DDh6aM>ixmnaB?!ij|bNzHB{9^Z#pdS13VmNU5T) z%$&d}Sk@REf)J$jygXE@YY$60$Wo&{1bf(k;5Z}rQfQWdrj>z`RiE>evDGzqb$ENp zK|{oExN_N|m3O_pj}_+pUIXX3E&)&jPgZnwg;<1V`jaONR@ z_PQS3_y^rF2XxAzqxN~afT@v)c7-P+Ak-Eeu~ElvCX9@$l4hN$2!H-LDL2_n4x|bXCJy&B~>^VbNYwyW$wU5RngwIHZT3ti&?;_pr4uUnd_>N0Ej((#(F{Et}k~mx8iE4*3fH;-&F>4 zUDa&dc`C&GpQs5Q)`Fqmtl|jL4jfpGb8#?2tjom~HeHY&aQR!J-~&_(c0ci*FD2$f zLW)gEY?TatiNoUiu*4h>sqebMt<{D@1-JgoeB@>*w9t4Bi1_X1h02B5>cX|kVV^vt zvbI1Q5%-i!8%$_85szx5Va%E9QW*4u21sYNX}GW9A)D{9AF3Ry#c)MJ%2AxXs;Q3b zHFFt?QERVf0|TyTE{QMExPtHr$uMik6xee|oGJb&x!W1oa~<5r zH?Su*jf|bDGqRH^CJ#T>LYFl*?Yc?E$yplA@9~j{OZDa(q?oXcXv)T#r&JJ7lbrV^-V)`Zgm@&vU+^OrcTNsx4 zY}<_{) zHmz(IdfMJouxG0OZk$G~@2K5zXtaOSg%8x<6Y??>mEVe`l)Dhr8B9k%C8Q%gs$C=v zyeQM$csA~^tec=F3^?*Jv_nwSL^_IEQm)m^T4$1PL8bl1rzQADC~c5FFONt*;J=|p z>Mp%?18`;ajzr^?V^aG~YN735Joq~UvB9ok(?3nIR?SV&06~#gWLToV%x8+$9yh3hGbQ9k0i#Jvj?<`wvb*cCJ5#I&TVCX!(vy{hCm6BtI@xBn`bL+9%FRC14Dd z?ISx1Uq`RYZHqeZsQ$rQS2R8XI7;ZQ=W*d7Ztnf-j$c19u^0clu!}8m!t4G6( z*U7m%!-~hrl&G1t)Sq|!-VdW^9p8I_i1}+qf4AbwfoaXZMi`a<^h9OwMZ@Bp1^A9pE4ti)l1R-!OUg6d;Ck+lNG4n~%tl&57gG1{!T;&qg?2Ag?W~a?Ik(7r@Jr)n2|M=>0#pK6i`D_2cvYxAtQvT*m?c~74H5?PC1foC9iJ*kIw$v{qo-jtXs7_GuW+qizX@-9kZX| z$_p+zE0o4e2LySR9K@Q7_QElkg@=}c-~L``9-LEr%&PuiT%@yh&LB*1f5)@MXD+q7 zcXn3(zEcC4(SY~l-UzncBiF9{34y<^)6|4|d-i;+rh64`#fnu=`tlZ9G&1L-irPo&k?lEB((RG4v-99RO zT#L2JR9x6l1E=O^L0VApQ&(AtN!RmN&k|L1?Q7HYz!*?`x?q{I8B=ABD>cFu}w4Lv!Lxd*}hu;sY49uRp-GURy4cuhsUIDbUv`K(VvPK(3qh|ZUz^y^(wFXEqeI=G~M#0H#J z&@tXqo|B-kLYko>NLE_z&>fEq$%rIThcye&pCuygo82Hp&E{BwlWOl8bJD$6+CX{! zT%l`TD_bW&Mks0VZyT%hQ~XoOUg+QcZl#$${ws=j8G`iIX~bZDn=}f8z;(Poo2V~& zCu@?Yvf%i)cW^+Lu9=sE6?~r);BVNoiCRj9hBL`$3lF+)HKrUidbi9X$wtx*?xnO_ z9{@FNwZbmDkz>@q7-tGbJUi5&RWm<-q|a+Hp!WvQ5!XV^l_IscRP5rxm!!iik8L%L+6x_$gHNfuWe}EaEtO3Or4|;Eh%sn z$hbHkQ6(Z3HozFxmXI&9gs9bylAI$pdvoJ{E?&~BBg6OynFr1q^1h*%BFETv4{3t$oJuDRRGPPYkt}$ca7DXh`Iq&YT^q!I6MdnJ$*1 zK~GLrgFt9Vu(18`o{vwxTng0h7S%$EKFS)b$#01kQ5UJ7(Jkdx2}nloJT?8fQSAI% z&8+5+XQN3=87gcIE8NPnQSZ#UmwQa5c3i;0f~+>2xX9u&cy~yD(N(LE3i2JI@92W; z>ibq$W-=FvNH@vqj#LYYiuB5(D}C41Qujk#u-abL446nai2?*pZx0Bt4E3r`W3VoW zYVo9FwuB3$j3)pRFg2!*TqFHV2`E@zH@$)F+Ky_bNVjgvlf!B=Pn>yg8hGD(EEL7> zSUG7EWK`Q0FTqYfoPD(P%4d4I-u^uiLe@tI5k!J>w70|Y(C5^we|@zrS#Bavq{>&@ zLQhTQVt{R@H4Oe)No~w}X(aS^D4UIHVpi$quR+LC($r=6-xcQ%2$ijt?w(+$CvIV} zGWTc$Lo4bZ>E#E#F5L+uHNMP6tue;Wc`o@k96fJEp8)?R+`6sVr%6DAu!rNHj)0Y1 zWeN4l<70iN*(`2$;>;dEt4WN28zMyF!;nRUOo)d4cKhLRrO)ck-lTrwc6j#1wgWkm93dbGD8Ee2Rdi)DIZM;muADDjH<$sw zY3<`Ci?y+}PG3e9Y8@GQ(n@TQ-OXCD{PgGcuSrl-UvBm}Z)qvuS=#GUkQIdnqOe9G zol46*(4_vBJ;XF65MO6%)UOCp>95K?{bp9{Dk|k60`xHW2j2+S@n3J-{1mJfI!aNm z3a+!?t0re>N~d$KIyEusfo&Q633LvWbR+ZAxzB3uXjMhmJu>f&ry9hiXtxkvezDzX zMyS=VVIN^=u)@a>Tel3jGwx4Y(So2jZd(wG+gmGPf+Azws`R4|mCMl!B3`RM6EaRT zt%tq17(S#GUi8=O_vLvcPPf5JcEMmtChNq3KP=nN9<|N1{rW`>{SW+Ayc2>0Jb0vZo(_knpQ}gewKPQrv1Q3O0?t_wP**yRexcaNxUcFqcTD>a@zXgw* zrYpuOK9DZ(fgfCvGjrJkqSY0jW23bIz^d7thxS)LcQn?P7QO0&@$Hn5rOAaeskyax zl-aSU^G8MI#akAC3xZZVbBj@qL#*n000h7|?@>8&u!~Pqdaa?@(RMeZPqOq#$1nT) zDxtlqn7rzVw!mT)?!}DCc3lKRh08#N%bpKh%^mj!cPQa$?Yi6bEyR!T{5+YGgGg6e zCe<6vMqD~OCnbGRP`d>@htA)x3$CIYBFjwQ4++1gU)gVh4_Hd-6hP0hSnyoFU64Y_XkRs^}u`O-(Xt-iBH!e~=&8$0;;h;Y`@r=urZDKd- z9&4y$to^h-KO;*%!f%{rNw;LU>0U7zQnSNcRO87CjR5$7S+Y^=c|-4;Qj08y%ixac z3{xb8qMHF&D87V~`O_g;*IiI-$~#wFa}a&m4W`(!U{&&w8o852<;Ys(TLqp`l&Q*b zb7dI7AU>4A-VA{6P=BCzMheT|cCli@RwhXrlSNQC{6|oGJpE&A2CEUJQ>JOB(HFz5 za6{$UhW14ms`Y=es-k7SjVv+d)unKFxl^EfVwdl<$ei-KxpL=TnMu0xc8*RJ*YvVs z%;xYZM!s8jR~Q6gv}X&Bg`IM=x^FxXI%++l%yeO!D*YBT8XvY#&R*Gb;I-tK=-y$q zXUVPNa!FYlAAyIro={sdsPtQ?<=AT8g4|Co(|RdH`IaE zWX+AOIf=P=_Q?MjIt#ZZyD*F|*x1O8E@=TNM+%5AI)xD*Afj|giHHcOFuHMc3L~Ur zgh+{_Q%XcirCS^+BC?Nv;5p}d-gBPoyyx7{@1`{h)OJe{l)-Au?`cFY^~{JcF^0W< z#mjrgrt_cGfWP60G!xB(gdCZ)oTf#3&g6JBDbs)Pb<1$VVuB#;=f1T+(RXaL{LK@e zw+<`flh*I6R>Q9$2u9S5*>dO)GEMPjX&jAPKz4b3FkFI}^RI5e+{uuR>UAf~m|z*nZl8CIWcHyLU0<_g_x? znZ`kFU_incQs)jel`z(VL&to=mG6K`&!?i!Vl$^g-zJA6i8g+l7Cp3+6ezM$f5 z14N{rh^6_!voP_w6CaXvk<{(L`xlwvWCH@Scn@IDG`|F4 z^VRRkpPeb5p=U-t%4w=uVY=lZ3Uq_S_T?Mg!QZ~NQ&Zi?WO`l)f|inLVj6GdB=+8@ z5yL6+{e zLhU4q=U=jlrbfXiKJlACLjP7AXg9S;31q`Tt#S`Cu!bx5f)x!iRlRy^sZ{z*LcK^N zt}OTf0{0HQYM#1+{2DZ42M1!A5;*jWl){c+iqU||QZw4e3~>#EEYY+!lWbX& z4_;5AzCo57I7OIX0Z_;+$cYdwfUavykKcPCu4o|faF~PEbI|GJ+m93K?Afsj<>C?r z!Xlpbegv#Yq+osb_0NP*ME0GfZ}IsUS_!b$xaPykR+_!_;*gl=Z*z<8d~e>l-KMMO z{ET56R>+%t%iO+_<96A29EKiPoI5 zNxM@_gC5H_z5UoCk=LMSFmbC48WLs7ELFuP1DoCg> z2uTBLj<=2CkwF6_w`AibvU*=AvsY1?>lX3X$JT}lvRetVO%NySD-X|6jq-fWK4Ae}ELy4^nDT5ym7=hb2`oLc? zI@Hcld(X5Bm>Nym*-p&oVW8S*Lk2**CVXtKTwP{WHMb}yo4*qt} z>^a8gmex{Idi6SbQ)wp`7_%8XtLxw4p8iWrQjbHzvVn>n_%u|4Jx0}C$n_4n7Z=P9$w(9Lc zm3F?g%To}Utmd0h-sGD|uHy>zuB7_8zdx-GzhuQRZgaY@@4{=W1nHJ}tFxU7cW=1t zE|S1a655G7==!Xdk?O$~vQaO`%G17BXg4TN9Vm3;bx=SkIyT%z0-lZlMxR8l z4`G9R@&lF}FCLcS8guSFqPdTSyDfyhX;_(^Q|K3|2^@Zln~kCM#&Np=SfW0_v}sB4 z$zZswKX(>)RIK=4)*m)?B874HvkLstp64>{H!IY@(_6ne<*`E;;Q;Y zkhUsV_+VM?XpUo;ZkORZQ5! zGj4pseaTTIBh_o1-PafW)lQ4)5OymyGPDCWJD`L3y1CdZ-nYl+*wOpa23;cK_h_wj zGfZN_t&3?fIAF`?qyShxfBW#l7bEPxjn_U?zxCJU3Y6<Irakw2*`=-*Ux@6PRqZad=%dtLQ3u#pbL9I3ZeE@Hd#;GUPKRMK8#Yys|;)?C8i zmyVo26%f@!pY1PQMRdCl5_J~t=q(^_tDraS@BO((MQes*9F7fL0=tykjKB(2I1{x~ zu8AhsvytM~S$J$+aH>o~fd%om8_M1FY8LD@tWehibS0iCHZQzOPR@;glNGCbK%vZg z(yV%ye145qMUl{2S?n%#yMGBJN&2tmm#^N}j;IG5{kidruxoDD5|I@-f#tEb0hAW^ z1KZe6KB5yt50awhQ^6^kZ1+e7jpq}05;6C{5Hy6}YjZ3zTa}tKSVL;RU*J9?&%vCS z6&j+8qW{?xNooyEX-3hmBRZKxg)6?Ra5I+<-FBzX?yoNBQc&AZ`Z+QwU*sh$;=2SZ z#@7;4s!cl*t>Sf&x4NW$p{=C9mN9o*ZR*7RIBK{yadmMb^0A36t3WB)>2F+pz;)d) zLbkxIn}Kkl03iD;92<}?7ZyBYQHU)+`h~a zxh}sqY>}`#r-9-OUjzqa5!!{JGS6@R z=nN)<=)?n1%iU48=Fguoc7NnA=jh!MeJb{#oO`#XRSjf*XE<6lJfAwV!F8O+HHYFY zslcRyT7YXq9je`qZ%si1ktjsKf0tOtLy5~PQ5)vsb z^8cC@XS?1MwQtBRJ?oDU97}Bh0@G~z$xDgq>odgz#G__|yv&2`p*_FPLG3A$a|bw#qjrsA##}%8+)}1z3&7=ie$bHj%mh^G&%}r*zYMH(XMD z?!A%y&-IV9^JiyXWG{xJ8Jl8q2T@XJ!piY>5)jGIN3midIH#^23sCxFOb>q6JZ|c8 zA-#T+E?W(tOg2BH!QG}3O4ejg?EUTu4)mp;y#efuKZ6x->*2uy0W4Sy2Iol z7(-!9Lz@uw#s9yd^E7r}?wHEjm!DES%xrFO{O4`;P9&XO(5GAejVGMD*RazFaD^kk1r+>`h@j=fL^s71`a z;MW&|6x-gmdr=~_7LZMjA_)q*rq@Xy9zch03Jz8v3;x2vDrPqI){Lc{He&*@7WN`=z zfIzQ+hxB9x2li2dI#zew95K+^eDL35H}717>nCNyz@-`n4Fsh)+SI~0*2d>x*+%|B zl?fr_nNU@5qp#>2&5tDq2@8+%PK>R2(qU@NyxQxY+FmDU7A4n7vFVV=|1|^qT!tm%D<-oj#+f zv#Gy^I!gU}h-8vo>iFLNp~sd3I9I>Xlzv2U<4V3d{Ay-`JvFUFMQ8hU`j&42{2RTX zMRxkgOIaCgO$p53g!MVgY=I^+QRhl1*D0dQ&^j^1ViweuG1UX<4{!<~gd^N{KBvkJ zqWO}`iK=0mgfS5vMQ*{NcR1$MR;Q9^yH$f`Av2z~tPN-V`bgi7(q!^~G~1?R-Msra z>AoQ!C*i950-Os9Y(eQim8IQ?pj7CaIY_NlwEi1TJ%~W9h>-2&_H-xOltWyCAXd{} zbX9Dl_y@7nM*Ks%%~RnK-%7Rah;~;wsY^YOGPMUESaPVt8Nihy+P7mhAZ%ugGyhh2 zmt{&Ef|;7$Y>nP5R%CO()J;!JTeJS67yfmSfs`7=#seZQ z6&P~YbR5l6?%#o7Q}cXWPKlTvV-eDb@u|qi)_q&Yb!Ok1n^rcG7}NZ z(M8_j@~c$0$$U8J%Eyu|PZPg~Wj4VcJW)2u&SKY|)GHwE2sZvE< zP1aY3sTXM2kBU-Qm79v)8&Z5g3W4Ur;jUrVYvYcr$o$!lknC_n7Q%cji6M>zkOGSztpEdC}!>4thOQSt;XB`qWh(Onwi8(8L8=Ljfd-R z=@L}~H}>nma}53ztH|}_p!j}!{o4>w+S24`CSR@-v6=ZN@&SQ z;kxoJ56gl?9NM^caA7OjDKfDo73V8r2|Id>*~~P8I}EI5&1O7fP9>Dm3ge&;daU7A zh%k6M!#Bxdkz8fS-B-C&d82P@h!+;>jNK`qBvYPm32$NGFy6Zz*&34xpNn~?WP?F`I z{KOt9*Id?+WZ{)}Xm;yu-#F~dF1gqJ;j{F*&@GV%mq*KnwSM`wqx%-3{NrU3)m)4c z>8Nn&>w%-YoddpnF)ec=VvjmD14?=gpBO#!Epub;%M0XA(Hk~D{%hXa-6W=3{o`&y}Y?|Q$#vvb!x$muZEmJ7vxuhnjG*d^%< z%6AYG-P^E|OM<#=m^^ykKOy@yk(Kj5Eerri);2maOGd1DU?acQzI}r6%)5EV6#W|I z(vj>+@#y;NfzRAd@x|d08z#SKr^{f^>hFKpbNKD^h@IqM6YoauwJPP;Kc}p4MtyNZAxSSW5rQ5u;+#?)zRU-qP~5 z>|?(N7poIDY%g8u@X1^eQ7fVV5P}Kw%FKj^>Wac(*Zf*>H~i!Iv?buTs>Z9Gb`#(l zIr?Ps@#X2`-oHGLLgv3@bAGS+)x{DqQM7Y`hjVw5M)O%ht zcG6m)z4t?v^rNEX$N3F39UJJvYwRT5)^@LeN;Z@)1^^EOvWepO`I{WZ4u)K?t1IuN- z$jOc6+FJ%L+8<389{quE&hYV26%|bqd8vwc_Lt#?K%SXGj%hvt=_0PB!Y573d=FPR zr1*sq4m|or+`iqa7t2C9McR4fLMml@`z?r0MA$Zq`C-EP&674hH*kq-X8 zdL-8(KWdoYO*ae$f^x-xxH|Z$MLA>+AzWLL56_CU1s%%;9Yru)eTv1B3{X@Y70S1` zR&!NO)KNlI;HG;oDr!}!!SQz6>g{evstEwO;G}RiK z!AXr{O^x44U35)d+DSucO+(#D^E(p)h1IlP({gaqc3;!>b6G&A8d_2523_;B3ONZo=;R-c5F-GsK!(um9)ZY~j9c;p=P}BHdqgk$pwzpJn4rmwW?Q>D$P>aKqJ zn|`LQ}Ik@_}Z~FVXJ`dV_?!JC6Y%?ImH85v0u*fy2ax2>q=HQ@2R# ztw;yADEF->U$^L>t>`efn7FN&6t~!%t=J;BxXP`#2DkXOt@v)YgyF4(DYwM=t;7|# zq@As#L$}xGTdzTna8%nkhDXU9+sXWoQbe~?q#va!ZKtX~O4Hv?Gkuh9y`ApxDB}v6 z==&%$Xgf3PQC8e`R?4I7obBwQM>&<-ISr3;+qQGNALR{i=S@9&Gr#?2rNS%9Isb6G zwj9fr=2So>x)HO9=h!LacQ1-^;=hvW8*S%{x)=QNFS1mt*A-Ra9%vB72o}-p&jPLXwL-VfY%~>#)wNYt9q#ngt<`2=hyB&NJ4HwROGM6AZGtOlr?K=M)fx`PbEnDD z+Fg{>l~hhZq!kg7CGM56*J+J~B%r^XigxAeb|BMO z>S`>lm4rrcRn>3}XJj{G!o5C4UDz{hB&f!7f0sKAKugoLD=+$Ge-}a{h@8gKmSeY# zyuOs~d&hM%aS@$rSIbPjm?nsH$6o3|qF;sTOaet|-S&lW>Y^o}?(ZH>-Cb+@k%D`n z^l1RjlAReLAlMMl{t#W*@7OnAv+5s4g#zqc3V}!Mrp6o?X3_ALoo#vYFSVkos%A0d z%dcG-=w^R);Pb1mSH0Y@Q6{ZUVTESks;cJ@Lpoixp#R+LUTqrlU@372{*vqhNqAUy zU7LE$nz6m3&Z&`gHo3Hl+9XlNMF6$F4M%;no4eEh$b^HL8(&v881U?a&Toka@z!gB>{+MVg5)n@MregssnGZ?+Y`!$uc9pnakgPa-Mj?bCwaF7kR9GEZeUfC1`CO-!EGji&}rc1iQ=_TV)d*l*2`n~S`~7K&LsDEeNBb-Q@7l%vP@AM;!X_P6a^<@bidKEg>H zI^bN@-*Bg$ANi69Ljb?0sG}LxL@E4O1Vi`2j%@L}+`LbJ#XbG=?&-AtujA64@3l`O zgLV)&G0IJeJ39L+D#xl#?r<&uWV1#d?+tTJ`Z?n+s{5FwMEyHA_S;V5eFuq!Hb7nF zZjXL9%tz)1@wjH*U3Q>R>$jwx4Vt|L4Oc+Vd6OxmLh$P|q9grJ_g88@h@zr`J-S_X zg$890XO5Lrj>{ercTEZP{!PP|m3O~*)$~6Fj>tfI9v^=Z9~C+-@0F1EE#cHSpr{74 zo_e2-X=w}8S#o*+8Q<09?Ay@yL&Q&n3;~~Ap6SZ`kpCfp`S?t9tHwI|UD*6hT0=Qx z?^7n!smhMP&_lFjuB;*b2}Q@xyZ7Yl`TY%VOIkcXwILoh?r%TP(b;r)rak`{uCV`i z=Tv+=65zT!%;2-#(7bB>%j9=tS;}c6!}G&K|Gr2NgYh#UqhE7Sqmio{1C3fwx2((8 zU9FK~mQ-;h&fk~uq$Wt(?uWVmo$Q>Sak?;fe zd(qeZbg5qDmx4Wx5Yo}=pi^`Bd|J=pFDpP|9rU#`tkQlehU5bL5K(R!0l$HM3Nm zs8OHe7nFNtbF*izeujz@kG0%M%UvxZsED`1$fx;%rbL#K|^c4&eL#H;>l-EYJK%Z zMllGF7^Xd}zUpYabdl)$lg>`^4(t9WXc@rx8JhCA{(GJ7`*YX4e;mp0W*!1)s{xlm zzk90M9gKgEK28!WsrGHG?sW|Mdm1>W5!rB#X2R`<=X!ko(Hh!*cduO4Qa@g0nNh*VPDGFL3e(bpv)72GrH#^0 zSpy;YS-A}HQW0IH;<-xB@N7})3L_!22Y7qF*pld*_VR|(y`bdR@~Ux$TB6j+^ztzg zaoH&IGEv74q`L;!&Ahk}T6JP6gQ8>L3 zr@p=^zy7=t+4R|b;H^6A52Er?ccqW_ARmQZSc^PKhTR)*N`1*z$p2WFUr%epJXFrm zEYprYIG9FesEE2K`_PMU~Qhtyi-kX1EwpCFNcG% z>I&LtT-OAU)$6`kN_D#4bIk7m`=!aGtGEI!N&WOj_g-9#ge`+3|L^WNw^4`}emU&C zp;45&%b!aGPNv)ve=V4LYzn)saZUG_PwkrFuefnRg6L@CiZK|@qel!Qz!Z&W`iFxdH13KqQD9Y`)MBmqmwq5P|2?^D(0PK)EYZ&ZXS-^0zQB(ZPb zm%Rst4_|60w10z!_nd^h1i62i0YcJLsmpIv^z9 zMK4bhpk^UW56rTBj3U}2QqHF6wwT`}QfwQ*xfm%a?U4eScnFVuk5E$=z1LC?hz7@} zV#h{SV>?_8rBC4FG?k(dpn07W@sI3S3oNGmnBQ zMu(FF9MwB#yKc^5oiuez_!MUc-f!#BlogLqze7fbl6u_YDqxn_B?h|hUEy*BH61p5 zBHTBH=S(+->5mj>Mxq`^=3PZJ`1HZuS9xwvYm2&(7>Qjb;v$B;Tx)DMQ@TRa3=(34 zb3_V6JdvpkPGA*>m`{|_ywn)laeZ3g6l#a0fjyF?!@$V9(h;cN7tZHPn37h{k72~ZRs`ui<1ZOE7PliEa6}sRD zau66ARkC!Z$k`Rk7s+}%QH|LhBagyI-A@4NLmH;ii~QbRuys=&GMR>^^~zo)PJs&Zk}u4J3=QMNnY@rg3-t_=e|3 zA>EkpyQAi%H{Ll|aIE4gr0JEAB3%@n84>kKe$`B7_^<8=puLb93!(j%1aZsau- zwF%F1Wk0F;70It>YwACfb&|aXO5sQ{l(dT|UXl-;YZX-s_TUIelzrV>R1ORjXbI~uH}OOMMycd(*16HMsB_X>U$uyGte zBGt3sSC*5@vZQngIFQ9lEze|dHq`pYVMb`j@!@$4u2Y-~i~LQ_rPboXs4m2A+*uKb z(@c0-N8u_YQ8+SRsK0@rcM+GkRyFjqUx(s7{&jCkc>XI%0W?){=44_L?Lw5!^{jd- zFBeDsQbTKIr4T_uG4-^iy^@a(UqtNREV)$N7Q`_{uJODviuVyLbS|PzK%UiS zb@{%nqUOab?*AB(XKJ+K`|Lu&9@aoJ>JjCqBzVt3@BMa4s<~#;cmByJ$a%)*WI#K` zd=}Bcn>N*7XWia=Sg*_;dazTDR#%rxN1A?NgmU%GXN$fi(;l0@jsE;X0u)r_F}&%P zUf01l4A=6Z0XJoytXo^dsR`~=nF@oeGq>Ms~KxNSru?q(S42$4ZKr};zLxOnErYm4KMHddc<37XJvKw3#R+`&M&@|Rgo*w zzF>rhGUaWqgvotDVS*zgG)!1hdp`(5N8xr*8^vp+9#oXbv>~{xyaq1y0^Baa_(TFx zMNqm4Dg2&D6JKUlH~H!kR6W#hzD`$L@{Vt};H(2>Cw{eaUF4kKEdfAT5H>m;^Ru?+ zkfKN&tuH0!&w4_LqHM z61+8{ij;a^UodoEc;Z*M1*nw!`d)fL0yi}9J3W;hW3ag`o`lC=(fkJf#J$1pKi0jo z{$YZhzYcf6vknt{-orb(d=Im_KPe9|Pk|f_N!X1dNkGJlQk*VEDxsmfueMaFQas2% z@jppB3OC{}!3ckGTo3P99K|O}rAaB}8xk4azlP&@{U=NalCW3Bg31mOm5%5*gix>) zS~nENj1za9K$0V07LK}ypBDh&`$GX?y3(nNlK|HN5#UQu^nB;2 zZ$t0=pWnTMQ~aXJ%{e724?P>_cTV$5a{5R2Yva3D_D*Y?;xbBdL}lX~{M?QFes!i@ zPG)E2RPTJ>sT>~t-q5rXmZ0yRR^#@xKPX(?)XzJkX{{)ID!)3ewzsIL`b|~G-h4;V z@LFY9g1dJ_V)sn<&T+rH-(1i1Sm($_zTatQMDxaRk-AND(98A7$@QVK?+t0gwH^DN z)y>VlNmGjrZKIc0(-CEEgY47EJiFa_`Vk^cXjQ|^yTSAj(^1IN=HUmVo-1dMie!c;di>Vc)7E9x%cbxa&f9Q&3}0D zynpfZ@?w5$`f{xLa;mra^y2vVcq+q?!NulszWVa?$NbKp@B0^*E7f70qf=vxmzT#; z{x3H!FO;;MEE+o8o7XN^rY}j0ms9Ve@;heskHD@V9tekIZu&^0UT07rgJpVp=KGoFU z)^WbFc6z>eK2^S!(NWHkmNzB4(=d9xIQV@^c6ws)Vz{Nhp@ei8alCV!7qhiB8qwWE zoF(nWVM^K_2IO@|>|AV(4ilCxNHN{V$9se2@{5Gpa*R2zr*26#sn=$&X@||~a$=f6 zB#SgWT3)+nL&R2NN>+}~@<<6KxR%q2(b}Mny_mHN(%@JFiF>+fV{uevTDCOHC#Gfk zVyoqB)K4V^b-vOv98|p{KYJF0S5a9cHSD2vOQwg9hcP9j?-5)ngv;-1ms2I@V@>B% z)7@7qXR7*gW$kij^m3=S`zkXl9p_`y=VKk0D@_0Z07eFo;as`|d{;P(QPgITP}mcV z5YW%ltuN|}XOnSX9IP)Mc+IO3$E9dO%PJ>mR%tWT@b*f)=s29G*I4=~SK;Z7XKa~p z{u^{S)iwR5^2uV|RM7{+euDZYcZ>Dk=)bG{Qf1ZX{%!bO)z><^p15lU&DC>_Zl5b3 zj5OCQy!Tlheq->y_FH?v(ayJ#_jNx$V!_nhhAo8Uo)|{4hodd^s{=RzgM7o*hV_vw z8ISLyt&N-Gc#U}OyKPO|pUcgv9)4IfL=K?Qt11^{-dN?vkj$@XZsRH7I*kBjLyQ1&-D5QI1-IA zMDv+}r+r{R^%C52q)4gT=UB3jG#0HYmu^o>MgY21|GmmeNfJG^T8@Hzh+$@y_+uH4 zQxUqi6ejDfMWlEG1KR6zmhcM3>$}`wLQ48=Uq^PpyC@jJ2Wp|_9wM+X4s%>A(9p^K zo;BRhA&>(}*MjcNw>k9ZkCc*!fPezuMXnoMH$;VoJsDQy0OE`Stut*-NyPkMj^mZw zyGUY0A{jBn0F1X1s4RY(f$oD=t#7MC*)Pbcz^pjF*qs`ibgX7^i)2%OsB%A(xLwET zPEKtPhrt9V154FjqkcLv)PVTWsO(qIqfZK()kkB>zxR&D(UcO$ z6Pm1E$CJ9kHOEtiw|*Xf#^^|#Oq*MJoqVx!tvQ*odH(a{t6j9j>8w+x*Xf*FSpC%M?$E+%(6)|M@~rXxhm%(pf6G8pk57JQMdnxPcul25ysx_ z{r4A<^5zApm-X?*!Ju&6#o_3!Ul&IcIye6vPg_3zcQWf*_wRJ^`LBP!m!fZ8o~>m* zzWlRQR(E;6_x@LBN)h#RYZRTZH~H@?`{3uRh>!NPo98{YcJ9wnyq_KigE~gY6rjkz zQq@mkhP<|T1e`a0IrL(+fZ}J$d%Dh0KUNzn@Ifi`p{7rG&<)KV)bA+Bk{T`bAYvP% zz{+$1hOiR5RH^KHqTjfg$d(vzkR49wSz#pz8kobpBxPjB4JqGPj}=YP1FUcZE!HA} zYXz3oCmD1QZ%0$iHXx!6PI>Y;y9&%YKHCgj#Dkh01Qs>hV z%KFr|H1pXo>F$tf>2%!%L2*aDkM|_+hSM~yPXY$J+qVxhi8Q6CXuVMfj-#wW!&2$Y zp-~s5qwI-c3kcw``obgX+zehYL{ChqQmO`jZ8=4#uY)So-7#!C6JmI+6(uv z(6^0ckzTqSRo0Km{Qd;p*c!Cy{bYrNp?mL~>5SjE!eHk0BK7GKx-YSzQV>g}vVW8m zN&I8|hZ+3lO90BWbDNZl6425g#FX5$H;Txdx-P9InByFK?QTg`8opF(I{t^;3Tw8nF^ld|@GFVs-%G zuX)V%LN8f8lQ&kX?w+*`Mrf`62$601IK>Q$(sHQPIl3;q8jP z!_2j5S@2xwxYFxz2WtCOdgGIyncE$lbnfRI+}Kg=gOrV1?K;y`L%(j(cKxuuxuDbQ z60>)x_`u%wuf@be-PG$&8;>=_7AG$Unj5{mUlRnnZf?+4&E8=mkKO+;q`O`{(8y!hEZo(PS~x?(1{iAHxp+ z2K8*2iaD>5Cw>0lCZ9R>vxvH5WQ%OL_HtOKplarkeqxV7uAmYI15Nj#w5F#;~0Acn4 zOlBn*9Y8I7fG`AoU}^|^axkTo(t%}&TOqWcKsk9vKj}s@%^D`iNA&^~vSP)QOrp5& zmQ+2BnEo1^js>uzDD+}LbXWk`>a|9FJVPN=m;SB2D7Xbly7d~4gl}6#A6O>M#xO6Db>b)i(L#3M`)+Ye zU65aANOwdcI-ALT0t8&TGLsHwP}2ceL67l34g$pi0%AD@-dibxb{OVIlS2;*SVHZ0~|Xp%ARDlFKF2&TpXxJW<{BI`f4VD3q{ zJBb2#g-eTN0{KurM?)x4A&hD|HW;`y4tR}3c}%2#gr>K|gRk*s=yyRlFtAqMw1j(z zejge*Z$dT^T&|dF+5^#a(_tEd=wJYM$>_`r43x7wGZzO3VSp+XZ$!Hw*YMyF9|#Hu zv_yonA>k4TDisn%GH=ogI}p$gOi2LTB1O$8yxz}dUUH?O^vMXfqshf51U!wJ zT0bs_r=Ajy00=E5>E2769m=Tu6Z?EN6p$V92mv`pKrVbB`q)e!vr({I3{U`{EB~Yj)s;;%1!uv6tT9lHSR|_~g)RUrxCC@U0}Dv>X#{uy3C>Ie zO4vn9nm|SDAdUd=z@LQM`bZQLpi~i*uZ#G7285$vrMl6|1c>egXit}sF&23%I%&WT zeGAB#`&xl2#_)c{tN*!CQvS9 zRtV!1{U+XG2=IOs3{C(KY*j!f;Y+T?jFZ9F2CERE!Q1a0NY4R6yny&~`fwDCjD@7Q zgS5{PMH4ucCCaCjNWcpKQ3=o*TO#uWz>FyIBLT@vb#z@2*aS?(2XZ`7L`Ktt$OlE^ z?zm5Fx%$J1P1mTKytD=+(7Tq#f>{t82d4(qC$zx@h;{RnAznAi8C{x;vlC1zk!vEU zX4JgTjzLNu@8}94T(J(KS-CVv}{~$O3{KyBwjRz_@A&U@j z0X!((E-wpHvd{ne3tRQ8KXA;%+lOtGx;V)FEQIObf@?sm0x|J7JCf?Q(#qNU+X#RJ z5$uO3>9TH9Q-bjj!4Gy3?u0r4ADWB>Br`7}oLC_!#^~{f-fyX?l^tmt3(}kbTH+#( z?Ha+M>DPRKe1gq%QqVCmq$M8Mii0ZIL53?3hi)KhG^K$7r7{@>)Bg)Rib*UY6oR@6 z)$|BIDG8w42&X^q1+o#J2aua$0O1MHnHvq*H%q(~`DO{oilQK+04Qu$I-tIQn2F&9 zFtkuk8YJtHKt@C$7y+@mmp*EZ{P`T**G{3w21-8*zOpC>5r|vcZ)1jAlu`Jd`-qT* zq{9JZcp#8&31EQxK+l)D{sO@~3=~FmCQy|yaMl~$20gP1?q4c)nXCA<+r~_Y%ta)B z%VEqO24`a^vwZ*p#0KiFH>`M|A|_Azej^1R?JIYvZ zAQsuX;LADvBnlzaFxa~9NXp(BOUzF|yukptmw=BzU@|Fz^AG*2xnyM=fVGOjassSj z2Q)$fnDC_*c;H6c@SHfmgP_P)pRn-hz5(WSf z0OEWFHz&P_4}}IEtX9=SYm(dFg6~HF1eORLoYiHyS z{u1;d7Zc4$dt6%$!Ukb89em9$O9w`KeDw+*)Ir9*ckiHp8dTX3D4GIl3(Avr2a~j8 z8E~+tfvZ4E0Ie(uR7c37CQE^Q^>da+%2#&BOL7n|uy2?r01g~!{|Jd3OB6~(z=02a z@L3{NA=G;bdgm@eZwMF+sDlEToe8ZR_B0YCpcRJFY6(|r$YX;7_wP-!Ov0UT6x<|g zeF7APnuK={$t3vJeV{U7M4$`8G67~G(Xs$Q$^dY`9*BnY(c=EQ#Xtx_8Vo>*UBf|d z;d6zFbuWgJpC^`s>feTAa$fxNU4BVJrwDq^3jlqX84aYU<$y?GYDcb|q)=m*fe1@+ z1X`bxrlvd`(*|EE11-Q`pg=(nDBu3+W`6>LNC56*nd;&*N~k;eu!~e(kSQGSAE{^{ z7kcY9B9OdLIrwMtO1=lh;5_%@j|Kcw}WV4I-hv7(;C9F3Gn98q+E0xm;gDT z{HXN^B!C($AmPhvmQu1qoH3&zM0y!cdL1)pEO-S@pxc3%# zFu4k8L!@V)00$^mftH}R?Lg6nIanJYAHL$Pf`;`!dIM4sp{+Tg0;-I!RYui!)O_ww z%&B-Vbw@wcsspaV@q<>%Ucv4LBLF0Wg)kAI5bQUtfQlCg3LXqF#d|>h+n!?()&@nXpWG_lWzQ)d7o3Z)zI3>7>o@#AKz9G_$l%(u zYe;xl27C}e#Tw1juYBPEzD$DPeIPOI7ZKg}A?N1@x%<$gmseI(&*-gjPsVj!+k9CK z`2KPjWikh_GZ1AQfSiFaNI}KC;*s6z)$KZD&sQH(o>DP>2Zqnh;ToOP+J6sw9m)Xy zBE2LP-)imq@k0gENhG0yA^4FvO^zJpPCBRYH!?^DtPmmPSq~Ck9j_-<+U_~}Ujoe9c4Vp6M~YlTFlJNO9_T$4%5x#q1-y zs`up>;~FX5B{nl0ZUSH086(N>ctEtyQnVaPC7oocvIv*=T0YRhTV*wVjGc!H^Y$;5 zj-UV6^y&4&D;t=VU&-Z2Yn|}ZanHUsY0{Q%?QckB!7n(pP<1Vfiffh$b6EkK80(d- z8qt2E(z|sDeKf8hHY^T@Dm~;+s%9;QKiH^&S@kpN#Xbeq#E9u(^E~x{!HX|T>U!%Z z#5jBSxT8}3+UMcP46okt3;C0KU@(ot*=MW5jL8mZk;y~3{}bSn$LT1*3V;>6R}Vov z=et9v=ngzaa!(Mt>eU(65MAkcq+1p z#3fhL_t>=OiRi>4Yw)IoY%;?Bd+qGV<%}L|^DGTGW?;KK#HPK*Tyb4FRNB5;i%f;p zh@!d0?Xd>Jv5RB;i0+UK*|DY|Tc&c|T|Xw&9-=|v{cTEv?RdmW-ai|hzNu*7W|KkD zGR(+MOm*p=_!f%8kBPV!$7TZs%^gdH4(i#h(kWBBGGycckD}n7tsP}f3Q({38r?a{ z=Wo#(ORxwt2S=%1o>!R^{O4uSJH}*0 zfk`B13S*hRp%oxK?-0VYXKlRymp7_xfmgjwxZf2vIuznaZ`@zrt_rj7zPEKM^FtD` zW_A4z%dIO*Z8r7XTlG7=_wm>s!qFuOh&UJYx|E*x>*w}7eAGgGsp;CUqAK@Qnv14E zJ&l}?tt$Pg=A%(i{Ac|%5fy)Qp%0!%Ac0g9rlAGRq$raL#}d2i0&;P409cp zO^oyIA#Szmj>FnR4?MUyi`WHya*#F|BGu?!)c#ycTlw3?J8Wh=FGoZ{PP8E3+Ic{u zUbYgwfu98!E7363{j}Ef!MCQnIgj=TBOyW-fJD({%ksWc-}u0cX>i2@ot_IPoq+9vLv%vD zK4QwLCh79vmJJGU?AJfH^3r+in0K2a8V}evXQ{dG7vI#moPktN0Wd1iWbHLAcO(QT5p_)XWgv*AAU@G;&6`mNEW&@ui5xw?>$u&P^Yuef>4i`+ zFWldR*{@Y~hsnwwMP529K>W2aKSYGV*TWupTj-)TElU|moD#Y%CYeP8AFa#np}UJ*+^9pKIpY2gCilt%xI2}wBnw~^(})KE2)s|-K~ zr3la@FmLt1CCl@z1nsCyp&3D`XUky`0ER$MA%HX7lKNvSfW>&b4ca9*qRAX_xikc& zTfoDEiaVC}Ff;*+#5q?8Ev^DrSIj}nsky~2DI2elrLf)KXSV17N;Tr_?TmPS?!X)u zHZ6x6Fa0V{Wcb}sDYtsh2RD%vK|nJE*ZyT)Yh9;AXw57)bIc)Kr{Sn)xvT^Fiulx| z|4_jxij2T9?&$$MzBd~l;#>**kPJ|4e`9oqZr=E_B!HVxc`lR+WGV;Dsd^jK^#ei` z%&6)-ki|lQrQxP$tEQ1TsJD-1W|Lf*&w71%aCO6;t%j>DV6RV|E4!tIBrhsW-7``f zPAVXS%;@2Fo8gGw-^3itj{DWTuT5rO;mG+)Q}B({f7U7l+q_>d9?0Ml)dzf7kJ)*f~qQ*Vy zErmv5lzCl4XLkUMuvh!KhWk@v*X56mN8U7)s#`l=ojkW`=C2!v+R%eTC4nq{ZV}wlF;9;%DPMX7|)6_;cBw*sj{e`c($oVH(2i@1!`(u;Y4({?K2?I+8`~d5f zeim5_{(3K%HBQ-SII4t+GwUpQ6edB$niT|hq8#Wk>Au%-TOLhnmL!-5>yFR2a$#1; zFA%;OlDIEYwNUys!`9B;OmAL>Z!Eh2 zsp~Lr>wXc5_gkfn5O&RLe> zVx_Z0J(Y@mPcz`e6x>wi=Ns}}6%E^e!8Y0mq*x<#Fs=>OXB@~n&w!al!!5bUhGz{1 zVxAKPnG$JJ^ZLbxO1D)%EJvU9W)JUO{)me#w$dhsyxBL zX6zBw26SGO@dy=p;kJM`4P0+qO7BAk|2zDzOr^0qn6Igc!^G)ymCcNyk8 zVpdyMnTQqTN%9=95KP81kwD?k8*2Hyp_tP5smBj==w&PO-#t<{GIDCZ3|C|Ho~!UZ zsi-N7F`b3r8>iKx&KHkS%naf*4f?utN%_?A<{Z$okK0@$E&m5Q)CNCO7SlN#M0l)p zqEJt*BAhCvl66RY43(#9F)PBSU|}L*lN9$4->IS%0h94FItraZ7+QM#N;F?js%P%p z(LF14AM&XYBP#EAIY+~ng%v$6UPm1&9SaeLB0bWl*!>Vq{-c%}_wwqE>Z+=5~FU1_@4F;K5`Fk7$8wozO}I zWc|cs1tL5R2jPGJRu^byhDHZi)OLim%pcb z##y{4zj(3qyoNq~QnbLmqNpRK^h#aPDybft)Mir1*At_h zlI}%VdAq|rD^;6s)eVGnj+Q1bOE@}5VNfonne5w-J17q-O!%UuI~VB7F~4f-X) zh+U_z&+~g%Opx$n#|v^&yz`AnI&|n~KMZ;nb|J3X{-@BUSm+7S^}gZGyWz4GfU}F) zA^n!t0^u?FnbmcHGI7`?8Q+-;5fl(%VT3?+(#R!}Wi{wcPCkr6)$Fup{TIM|4sy*6 za;sVQLSm%;dN~s|%zKASe>Uh^VK^lV<~}-PCpS(U=MM_{`!pe}w~s+8U`Byk1H4Z+ z5y#nZN=m`yl+vh5z30<|*FRvc&K+~UQ=cQ#f2t3mGx#f{{M8cl?$mrz6v0f{54EnN-?sVgNS0| z>@+WUaS;kXn?!y_EOVxDblccG&NsBqCC9A5$G<|y;Pjp^}V(v zDk|_@1(|8TJM!Uf2ZKZ@3}2ClG9#x=H-xA01I1OUti7cjRPTGdF4IZ4rDRp}-8?$Z zqD4zhj-}tVFJcu?*Byinb{sEpy?tFmlP+A>ox859YxFYTl8_`5y2=nF zX1|bXywtA2?cmiunBJEF;+K^?_;UdY zfLf1OrY*uW>FX&R9+Qj>yxR1vLOtR;Q1?=^Tf8bIrGOrQ2>yi`W?#HMOSv>ITpTZ7 zSDE*$pww;-y5Tf||D@BnkdNS8r=jie*wxnEh^=P6U%lHFy}8Mg*^RPmeAyz@IRxLfs9E13Bv5&_fpNip6G@%9dQ2wv6JEC!L!wmI8obG4 z^6`x!2!vm{W-f7#hZsVhI}5X>13FxxK?-0SZRL>y_fVJ|Y|*1!-+ZuA?*-?a6Trh~ z1vuZ;*Wunb5Z{1H;jecV-qUUzI$Z4gQ>crDA`y1KW!{M3hg+|;s2VCiwpZlPArooi zn4~nj!{!j~<`uJl!eWZ@7wjg^S3UnJtz(QBy?gzY3e(T*h-^C9GcG=pS}0*R>76V= zD53QY|ETZ`*V^%_xx_+ddr3f9tZARE-ix@zI4bhQMF$0s&47+lwq0M2c7C;)2!PXk0&&@SYpF>KA73V!qZ}{FHW}L1&CF`B7vr z1)r6BS*-ROZ+AYZZpuaE^AUeq0edEi0PpCRaYAk&AdHlM+O0kDyw=1sq@N>h#XdF6 z^VpGP*J3LsVjJN7UWj$7U8W5__2P!pURV{y5ofUjy^dzo%5`NGpI^gV4rm)|K@S&B zmdp?fs0ghrKt}|HT~8WWb#8nCVa(*S%j&~so9qDIwP&srNqwFadZ1H$?}KSFVzcTl zU_$IboHKwdC~!Xo#x)%>thl!_zErQNq=V9(Sr3G@o-O6To_Zd`VPVuwX}&f_L%qp{ zQ@jbyi;E;1A9&ht?Aa*`6M5G8FO#Mw8GIA)c>NB+7^HXnDnCvN*AgXNyv#!}(0LT< zj=X9_Li^@t(yg8pcNyOid;RIp{I8a>|GiaHx&cx5jHQn^-5WPp(}}3@432jRrbZML zV6U&6sSF4xF8@^fZRUDMWuaaMU_95hJ^SYNSb?jNpuVAK`>pY@h%}8q@;qkMhx(W5 zCwsnTe7U<&%&A5jEAv}L%M)?uEhhAP;d-9r*(bG&RFS|*EVPrs>;eK+Mghbh5oaG6 z^1@RMHY5}X1K=?gXy^EJ_?OMe8r1|QymBR4BAJ1!geVCQh*r!%g@}N`Fghn?DDS}O zd=TVirJUH>=$d$EIG4j|ODGPSgEUi5J4dX1-!BZYJDzpY)%p}MXlUpEc+<{IK1RyZ z7Eh0Ke?NK;ZR7vdGNAv_uA(5i)=>~1ywk=~*^mQ@wG++`J2cw0=*V7He(dUs3*}i< zbmxe9ri@dl0N&O*-V=I}X>#GXxHK>Tm^=yLl@`a%Nqt>BoKgtG@(f8Sc6w^lI*rc{ zmTY?B{h*ge#on0V)>2|V&u1tgXWr!(KOCt%vuM4nsbE!BRq024xRy-KXXCaVg*)`` z&|4M=_ut{Be&)Ldz;7Hi)^S8Mis!nO1{Gt^LpimEKi8h;)c#Sb6%JOu+EEl%fYHu? zzJB~(rbUrtY0glt`RJhV6uzT^x7z4U!d5|etN<1*SXpmWo1%9x(CWsPk9TDjI`KdX z7g{bP#XXH1*wLC3R+4jCd{Y8%yVP(XKG4J!-|RQ6D@8oIUtn!A7%Cwif3U!#Z(RXen+om4-jv(V;yQ<(-Io z@e_G|ZeJ0)Dqw-YWvyh_!@`t80|jO@@Y7bHz-m7?u4S3I^vd3-W!0vK$|Gw8EE{@x zZSbZNTXa-iMj~*^km`{_--jPRBWPIKsWq2M(l#kaE?zmN9MB+p%u*MR<0h$1n74{& z^@YQtoHYmQuwOq9+%P5Lp=gk|RXb&4t0EZjajW4`ZG2LIcRYz`V!G6E@bc?ssHT3E z{+bMGLD`n7e(N*f)SR z5onY!>1pA)*7a*IPW1i@t$Wy`Dm-!0|0YVCemCQ-%kR}MJVhwNuV?F0W;73J*uHf_ z=2e+8+Emn#h1*zPN{2Zqc-HwomUfC|M;wUGgD z7*R!CKbqs!ajTYI`GGTOax$}(O$?51LB`v(%^?6A_u>}6;k5Uh z^SMK9uH%^L?x$?L&9V}!_S{lSo%bfzM~wa}_u2-=^Q#A=NCSJZg?oVp6@l+-X)p_T zSFHU-vA~waMQpH)6UQSAR2(*LhfT~6i(rn$oKNGwcgApfsTB-4$wmzFQfrIcZ9Cde zWZQOjbY^cczE}BW z&+wA}^|JZ^-S*NYaBi;nMNkYN<>rrGvN8MDJF+86D*5D40)vHWazc|8O34Xzx8~yq~j2fb<=W2)PLAqr5XCZX)aA} zDmoN*A~-65P23PyxxvcH9;)@c1=!DPSKT?MKQ`WS=2E+2j>jHkI_x;elgx(=@rbEDBHBh~y^ zPeW2q7r=?yWOf{Ro7^k>xMzN-VWZJrl-jIF3jr8BU%Q8ol$^2F8HW+e;JKDbBHbhJ z$Q5ate1j6+Ul)4(5TEASLc#bL&v^B?OC;gNlgk=7$isjlczA?pXNEwRB%lwNX*sRm zRsi?X*qdt^7hhOR^R_Jbmy&376LXZqis_aW<{)<*^~x|(cP0^4#siiqzi+E+h+Nm7 z97J*^S!$n@dHP8J&|4?{ZyI32!mMS#m8p ziJ#N;h1#zVdqVeiYO2V~yU|y3EOG#WEBsiWad2cQvh`|{INtMZ{KM=~Zs}lVo|-XJ z0e9ty*X`k{O=$y>Rx@`6qwz2t^QBIzmk?`NubLF#Q zQTYGtWpM5{;*MTn#EPFtBAnB5D(ct)oJdB<%jo7WV_++?P}WTq8&dJ2$g~AetqII` zAD|(Oqx&^-=2;XM7*2b+Y$YvT++dL#VHZ;_h>(tL=NnZ8LY%n171b7yQJ=!Ybs3!# z*E{$&2P9^PWq824(D+}O)^4u9g$nZlN;7v?xa}7rI?l{60)%}#}FU2G)wKhrgTk`g@bAa%7!)^_w+YEHC4=bfKCa@(Gq1BAhnJRyX+XR_x&~4Gd|_otU0Yjk1{aZGrd5aFUQb9*%RZW6+l5gITo2WqUWwu zqI9KY6%QJG>8BgT^(kcMNUM(Na zp&*8L@*Fn2JB3&rFNlg$z~L^kmv*88Omb;-BzvyiB$|4Z>N#WpYE*@QWHIC$1*(&f z|IXdqU{v99_iCBfHW zWr7r#OLgG7KXR1BlpRHlaCq1jSL1#VL`MTw>VOHMu%e6gD^5-sS3OON3yD+=JRS$4 zq6IfU!gSsAs}=$;>-aU(SzCW%3EDD}SjIeA*$UnXn)b={kZUbpp1A#_XqncLu$T2G z&`?poZ=us%NTXN|5AZXNf@7QjU3b6zOm@jOMAw`^O9>0@QXGpbJ>e15|v_?F%p9W6!ze@fCpX$wcNShxdcAfn$U+76N(|pq@-7@fh|x z?48-ga#$45T)s+0*Ad67*7U>4gq85Z+9OY zR2bcQ>@lP@v3cFKvPXfcC)jb|$$hdS39ArCZS0T@u9q1g@BCUAn!4J*WuolUvW5a# zRWp2#Zi8IfYSTv>C2bq<1Qu5aT)T)29p7{@AQU%faj^v8G61$4a{ZD3a~0HAARAgj zt(bup($dj6nNAxIP2e4~QIDVZsrc~H(Xv{ngcms*vLyAX3BYL=7$sKY^{VA+ltSoF z0Dc0*R#e(((0QkWjF~X(!o&)*@eR~Rt514+%=vlmhu@Ar4#Oto@j%q#klr8A0eNrr zW($2+yro1W2~~FZu(4pSVe}KPOt~fi<;I-K`GeP*0M$9NItKD?W3cw9j8JikdKjs6 zV!L;$5}&sux?Xzu&Vl7o!rS9Q*8eCo203~55AAWN$_IJMwxNMIg%=0PkCqcN3@%21 z?d`?l&GJ|6venv6>9L8M&;i2^3PbapYjAm51Ek z;UP>3o3DWI8T}T~NE5nxB?(PX<~5;0|HkG1SzabrhgpFrw{%pM{f^{QIkP>%?EQu0-lC%XOJ90A>QR{L`;a+TyvW>h~HItj%D18u@r;z^_s! zQ4HnI^XS7U+1j^>+v>oI^TPHxlh}|~#Ch-@g{w~CPG97mWe!BU$fxgvf~0a62N}68 zROOK#1b)z!$~CIk~LirS_5=)=6Tav8}3(v5>@EEq%* z*v8d7;xP2e7n6UKp1tQekij<_10>8RT^G-?8ILpv>@uXGpQ_(bqi+Hp4?26!_R$Bd zuF1b{Zc21%-ZQRe%zXfB3-d}xA&MB75mnGmtQCfiI)1-ut%i(31@O@<3=zbNw5zB> z-Rn?e@_QiW)f0R0rx9|%M5f^ZVgkI&(7(s>N3ix+ed<;4J8WU_ZM(94HK zxC)|;wMz?U-Jj+X~#5&ydd z+Qxy7qjDPZ)|NV%aW9}4zm zxRv+Xd_OHB(9iC86FJY2yyOg zZF@Iwz4OqE007aDYG!|A;w$(BZ(T;F@A8%*nx|t_xRNGZ`Jc{ygv?{zU-+xC-T8Zx zwmhz}FAw<(9aRAa8za4@`wyOh5kmS&7A%Jn7|MgMu)m)y=EZV(_9Pa%q60sFU^{ak zzRl#s(H4_Th)sVk1baPDzMEjBmV*xx#W0k@9G?lGTp$_+S3w#2qUg!bVUprof#`lf zmhZKK7vuzC9eG86hiPE9{^JtJl3CEW$k!cOCrWozS3Nbc%^sxDYhC4>l?zhV&yR z#Hi+zsEvuP!z zCL!#V%qswqnde53Y|dHPpQrh_$?Bm7R_3R=>38qg%_A;sd%#%gXdL%VE9F<|S-?0D zfm$qaS|D0|;GNXBo};9E|#t3GN%BOH|@8+%I7QB`L+3(w0x7n zNx$;!LEU+tdZU&xhk=u{lZEC4QJ`h!_>R>-cO28y!&>9CRkG4e{~?UzXQK=px58|_ zWa`r=@w0GXUb-o-0MFq2Xa2PHHcng#1=Sopar%y-$Nq3ozQY1)A^-9-l`Rj%It%-@ zuKgP2BR7~m6*4>+xngVBtWG`6r~O)*$55$L9hl0MBRxZXgLBOl3k|ztOb#22xgh~fz*ShCyjFw$M{@Nto_=j50;o9kmZ&WwNFkU);8B?UaZW~- z@}7zV)IuIq{go#n>Y`FuW+Ko4=sR=u*KUFI%~sV?hgc3A22sN2JRP4Sl2)lRZ zMSU9hRR`+=kuJT-49Lf5gO(+{2$noNUC!}hzUXbK(FR5 z^5WS_HGM5qGBzsGIk(6}@6%*K{_CIL34CW2v%z`G)>Yb|z#$dfR2(n?b?Iv)a1rtyA)fp#2s6{X37B-EIOL;|jGb zXWu!W{Fe6;q}q1kWhO)WGahz`}J-9Ps5-QlJj~^`pL+e7kUW_)wG1 zTt}Ib8!)+Iq3kgh(5c1}@^ZF_l+YTN&O0Y^G&0g?Ko}DkE#dDi%5NAA8Mb=Euva$6x^%*XFr#{iPwy6O|DPQl zkZY=Z)^p@|J)bV&nOu>m?eqzHMpiu2AI^fTyu8QavoCUMAdag z5=}fDj#JxX=+HLXh8yxufuiIFXGCs^q zdlDNM%_d*(Ag^8q$VNyg5QooOEMj}jYW8iN4X5R|ZbTU{AlM?+*@@F{kTA!$+T#OM zi-v$piql~kErf02fzAUGz`a*yqozpcT8aRy0j1t|(b&s@p~B<{&|u?`-~UAWTDQ8y z$EZtTw1MdIl$X9GC;OfNpsWej7LV0mxalb(^-@dT1OIzgpy~@zz)iGwZob39=UrC7 z#{clQcyTx26+>qdI=JvpW`&sA0isdTlm=|!wv}$JPg^>hW<@%&11%R=P`NlSl9tJs z2!^a?C1@|_F)X#*Z5&5$@D9f7+A69Lq0_i$@iG2akFRV)z9FlP^PLVb;0@#LOlBYm zx9=e(j>1tRK zuJvxccyp-CN&T`~X03>KsCw8nR1=tDE_X1BMa#aMjSkc^a1w!s{G!BLJaZ|qwh#gS zjLOX;bHtPk*bdq_EZ@HES`M@wHrE5HD^xCp9(*AAPjEFpZIVZbizjG{OYgaVMV?s( z6s`94e|i3`X|+uX)>HAX#wXGPYf(^*HD@|^Rc3VqcM@j)ra{um9dT8*q?tF69@=KW zUGIx9CRyc<-&3hqu)-r$MkLnau`X=`e|y8Yk5%k4^syf2$JjT+lV+e^P5?;EgO;_d zL+r=q^SvGGKc;?xgoi(#4Hv+*U49Mzo#U3k0)|=~8#qAnOJ#m_S{JGcB$ka@6zc4- zLX|%*be`~5)04e9wl=;*ZRhS{H4%Wz6Y)3QDWmic#cNd7DN1||1C}@R#^7V&n-e=)5z*aiAjjpRCBu%V1`7!m zlt|I8g=!)YP=OS$-DcJ`inR|<=BMsTILfF@ zHRHUR9U$D>fvfHZX90mEZrSC+%`_?uzOY9x(E)bm#hz&@lH^gE;)L2f2YkqIbs%;I zP`$FtwW7Kr|9P$&Tsu>w(BM}tG|du zt(3OiovF@R>)zPlve-gf z-LnoM(@gJ4HpYsX@BvfkKWcbl_i3Y!_`J4@DIfbna zFVYk@wUL@>sv*z|eY+xuVV|Hw0``6UI^Z1Ff&hGFo>9NrkmDlMtC*8n$s(fD$gpET z3bZDkD%PsUqD@0srpD*}-JPc#kBP>5mz;Qbf-X9cq=}V?H22E!A78u})@Z#m@_IIj ztXd<18IO4&JjZrK6tx3hi#|LYGeNZ0AQ~O=lW1r^O_PL3{038yP&evjIlFp$vzpNhUox>IyM=5U&O>b^Q=E^DAcd;dt zvzDAr%H^}iF=caaTmoP3|FEUW@ueW#=Va>JR@V!D<~0A|Wy)dYv7?+H?V3k3VD}Gb z`l3qu5r);ipXdnQQ!9nW#~=8J6*Att$)sk@iEvs2zDP$#M14pZSOjy6k1FL+;DK(9 z*jdZ*1oa4jdZg(cK3!m4&87_}V3-iXN~xAC`{#Pz_-Na$C=qa(+OMkwRMiibon7oL zzUSy>nawSLB1< zhKnuueD~kgY`ch6tvR3wXC*HgKNY!(?z{ywxGPK2#@ejqX#}B_aRN147;G?vXhQ9t znlzzJ386w>o0GKf8V9LgK@c>)tkOgjC7?7^T;pSu|Odh#6?BU>$>j$w3pj>HB zOq&C-Bj;G8w@ByyxckFr80R!Z)U09 z`+6>6w;vQg;KE)%Fy$yxVl2WBTD-pH0l6+;0A~Ev@&6*{Z(4Exre$%HO&&RpDgmz5 z=BiWP;xj>f{y_3Ww4ahZ`o@uT`M<*etp0l9e{0nl>u5u zTYZ8wHfV9NTOee}yL|iIp?B|dR6Z5RWMzo`G=f3$5QbG=x{1{2AtXb{6Wr*97S<&$ zTo~+JOT|qU!rRz|VF2I)A>oNH@A~{)nBzQus7VNF$+X7Vi?m1qK*{94(tk~Dfz9SA zp}io&9>SlXj*%=Fzr^Zq+E!L5)sG%yT6SwBivkqI;UPD}e_82uQ*YY-eQOo`A)0$5 z-TyC_R@Qu)P2%n~!naIY76PzGnY_Vdix3$~ou(`gBXwv(j;RAdJUlY)$LOsf7_ucE>-bSB652W=w@DZJg$m0U|H_I3e zT)Pd}CDNFl+p0?As!!cqdF>5K+A@4iMWT_w)mz)bK79P~B}Al^Arjc`xQoc-x^HN;lJOH( zwWj3(u}n7T)?)H&Ha~z5JK5>d+DXgmKh#Aaw#x8Z{?()6g|@pxNoR4|-*U7&P=kfV zs)RTA2abS3rtX}8wF1xEYM`xT)0z2^BmNi-y1j@?z%QFn0HP7=AMAVoPv&c@AskCk zpndmzH+RVvy-ji!H4yJz(gL4=0^RscWzdSNw)DFptwrnN^=ji?`S7D6Kq6v^#79}CAAB%wt%!I1U#f}+pw^kC7J?&)S%TmuiR#jJj87h>T0Qny zDt5|u?aoSCUG9x{PxU$UA9yk9)3rEauN1KQcBO;c;{QB#!@rE6t zm-$SMW3RYp&@TOg@zWPrgO(!;Zo9ghX=%xdTIU<2Q;ZfNja#TCqNa-RRT+Hm1AuqcTfJ-$ zvUtsb=+G9fd;bc$dpwa1*xLM^bKvaGZ}|_bXq)2hJ7)gt-tHS~)gl^PMgNsV%T$`O zKRQPK9UT7gOe7VT>9W#xxd3Q@^ly0ZV~e022x2zXHr1JK4{ci`M{-ft8eB6Er*)oO zU@+h+vRX6Ex_|F`&X`CIAas)QFmI95=erT(6K&-?+$+xi;1efOpH)k}P$ zXJYza?Gd`vu^#dSztzlyec@74-Ii7Y*4%08oHJL}u{`>)ML|-`hAE% z1V#DpsCpx6?-bwBU7t6g4}wJG`x(0mR6MfnfY#|Td^bQ;_{=tawAt$?-<2Sr6;;Oh zH7jtnrxhdf?e2Jy#;X6!2GoCi8gsSfAw9q?&+P+hl^wQL*wat! z2iR^I=H#l5WM|eP6U|dgEbSivP31myBArIgk_}P#cx$!VWFNCt_&xbECnRabBnw2E z*}dh*m<-ObRp0g1^a-G5Kz+%#z&-jZoTR0@1eVQ3(sX^sTR&~=pN#?4aNN@xtb8}O zPk0Z&gj_>|?_HzpL2b#MjiMqOA+dyvUZoDyAR14MtlMp#IJ?mxy(r7gT=9<0mcoPW zV`!Pk!z~fCfqUYTPb<@0|C*v9NYv79kp>;8J`WbZm7@x{Pdr}h>|WjUWT4F|egpOA znSw7do?p(yLUEEbARE#=oVz6rWW7SXfo4(eN|Pm}&k%^XF51ax+0c*n{cP^wEkY;# zxS#2t9p?k2%g(5{}`TaCT z%(0iCx`Ql6;e$%}dLSqPE)>B);GBL%b9NFgK!&G7>$}oxR!y4u*{|vR;3t4%w#eYC zW-b2bef%j9+;hghi*W3BDb0EEga|wo^`o%jxOpSoenWUwoBkJz_a}DTq=Zn_Wd8E{ zk!>mo9cvIpTkHsRl15^a)Z47k)JED5C2kk!uP@q6w8(YiA$EyapQ4bN+e5rwqdx@1 zW*DB$JT6)h9cO+%&nzK!-IESEuPlzK&XwLS9_PS z^;a@2%6q>J;5`fGG}j=97XAb= zR`MOZYmZu(MJHRTYiAuQdgQ2N&e{SJ3v1+2vwp5L^1xNreK$n+mm^todegHj?4jMC z=OrY|ce|ip;4bw9CeFui7m8CX-Ciph=>2H!kO?3s1d@qf!n&;U<;KrmTA)>KE$HbC z+S>fjRt*vOg`YuF+N)`?U~r{VwD0=_HQz%-?&+sr%=JcaN6JKzc9eHK0Pdcz0;Dg6s~*x)&{HLq`xnRb)b+E+Ov z)orRIw^>?+XNsbrJ~Qv!lV>bOw(r~pvi%6PS$Q^}fEzEnQ#thqpIu<@QgH z?&(p3YT(!-3Faa(L!oUOH@G%$$#h27#(9#wDxPG*K5+ps&atN7f6DH~Zg9U3*Le3h zF}fvLDYrH2eFD`Vx`X(d|74SAe*X-bDJp2*-kj;*Xm(fgwC~kVmR72GC_%;hyu^E5 zggU(GwPW4EzsX$2vA4(>1ds+4KYWp|QS_JdFS8c?ryhLFD}WhvsvNa#-QC-*Xl3S! zp1c;4hHQ)%0&ieBQv)IX>QX%PHQ+ojKA+F~^?dn>ZD7^_8^%#J`uBI`ly0s(;I%JD)O4uq_6Y^Amx3Sfq>4_6 z?=siNq({)+vqvt2Yg|eaDuAbno`?c8`LGxokw2-Z&C&sP9Xv257hK}u%Pekxs z%7MnobM6h?+fk(Binj`u-!*xPQM3w4jzh#&|CH`=Pl}w)JHgFagSIO#K}6 zQELMRaPWEY;261R-jasaAq7ZT{cMtRC5j{Ic&kv;DLG{^CpnO8u>NiqlgJvKB8bBp z_*;Ev0GK>nCXMAjI-V-OqjF*qK!!e@o^x0Ieq|1*zzx8U8E=4wfgNGg!H{y(6}+K> z6$8{Vnhbt~S1NLKQg(uLTxyY~D}~ublbq2Lxl5Gij=njI4`p^xX+n47?_*W|tJdbA zR`#BuDK#$vLnYIa;*=xn5Vm-qjFX900g6sGFN8$x%XEfYPYkuv=Y>4U)UwT~ZPJFl zcQxF%3z|nllwzqa3^dCn;-t8Bba&@~toQz|5v{`g-FM?cq%!Zsr7Jp!8B}l=`k1&s z)R|;T3BXxfF|%1z>Y~4W9r652Q0faFW5iC}2>=@)-FM9v{X;8ff&;Knzl2 z+1r7z<8d^c-BCIh6e{REfF?n0U$)7(5vLpvwv&eB)|=$!3-;?X1qwphGL5^^!Bj{~ zDSkpu?+_a*rPmrS?V;CMG{YVM#%=%Gmb?g z&{?cPu!u8(j2aI>tDj%OJE(ns$K0^-Ci^kT@C;}9i9eASg(KXj0(#bO=XUL9#wh-Z z-0^PPI04@(h*9%b%@}s{wvtmXzpNMqV2p#qE4HdWagN8Olk(nNIyc?~Gi;ot3V=K5 zd(72znn~gYh2u_)Z0x!&+GSeP#$4fkzy01vNp8CG~Q&LwO zGm(NyWJ}9*B6>RY8C?a^C7qUK*cpHlKCKdZkjl!Q|01<>=Gm#>Xcke1hS@j?D9Ho8 zlAx7*9qqYCaaXpcXe6;~bu`QAl;y}eV7^%b+g)PzTsAjj-d#}J{)v`Pr?>-B9)NPk zBKvrlNTRoGH$&wvT(v*AA5{pCw0ZKG2Ml7$V{BOT8LP1boZSh@yy@O@^ul!jyA^aSHI$`9u z>aJ!(L>`X^T*Q+v-IY4k-=6&LFaLhX@#Zi;^X9?RHr4k;G>grbm&-TUONLEyL>!$3 zzX%Pk(@Lz6YbY4Dz8LN;KyAx*&O5JVwU3gp^3HO^ij4Soc$jRHq~CcpWZ%HrbikoS zJby=0m}tRC%2dF?w5`^|?>(_A|ZK9n`%t zT(Z3JBI(!+O*>*>CPRfMW>GqpjO|)O{QC$cGGTMrEw={Lc+AUSgA`sN9a5waT2*5O zu;MU+3ny6~rNe@6ZgtMHZ-S8p-5Do5l|oAr_j6YxJ+QyaREDbEq&D)QS7%7juHysMoftO8C*jmnS`1nlGQciT zA5d9C#-CNvz{D~LZ%>D2@8=^U- zhdDna{l?3QiLDq+^5hql6hr$x6!rdlnhp{2xAoJ98nvID#6_1opFtEL+&gJ?1{S3? z_eIFi*1`S7&JX*FmNACaTC!IBrzTAeMk6N&v}5PdTCHOvQQLv`IMNQ21$cQh$Xa(P zSD8BS$qznOR`)!X>IXpzi8fu4!F(-(Pb17Yx&|%U=b*ERfSqo4=t7 z|6UyTFaBO}HV0N{t2@{EXnz`XBQ9Odc!w-p?;=q?ObLTfJ%_lFw>z45Pyb z`-IzX=5=?<{ZIDbiqu8|w61R|#3l+_YA*gRp~q_^G1{G#?E&Dzy_$f6T2E}s6#O_R z-bFk%pot)@!G89VN=*tH4tT31Dt}@e|NB^tya~$eJ@$N!i9jwqry4F%#e6Y{rXDBh zU=^b`FKtT_^Ob?g1E@i{&}l?09VAApht63y2Okxa7&eP^Yr@vE24=kq$fZTRDh~#W zwgCP^v0#ABlJuix{EVAN-VmusltxU^!_sL{fzmQ8so7cODSgYC*~1By%#`GvzEr@P z408kEj zOVPcR7U63_csv!p-g(qUxK%@lv8HWxoioER=x=zazkqo|GTf@w)NNVLI$F`)b;pZz zcsb2FfkF7>RJk?J#%HC`F(<5ottX+RR~{#gqIcJe^kD8320zmJ1_TXv~#Lat_-7++SMJ zUkM;8K(-9ZCB?*EP69xFfTjr8zz01g1Ro8wWU|7hU`HlZrKrFp6QQ~Yx1{N@OOF}$ zR2WafjaTudBnhl1_1!H~;=ifbmmO=hWhW_S7>3gjw*BAg^pskq@Ygp=|Gz&}VMD)U7L>|2T5k7@ur@DC1 zV!=(W38d;O*E95A^v1)PDh=p@ULtXG^nux4$>sXx0L*u>CQPRQfJ)GfC#|U%G9M&T zPknmprYBVRG$6MCuUiwu13Wsy-5YVe;-6c+<0JwH*%t9i&SDC}Sl-%6Mx%X|SxfNr zNyHIDT=j6=?HXXH9LQQkiJ50klW;%wxLXox-E@c*RYlK!a+VX${?3s96yt6wAMOpn z|Fs>ZQpNDc$tuVK+x!@h*U7;e+@1@IL<0_D8m9 zLvpuO;H&Trq(#6R0#JN#vT9p$!^sK+EV3K*k4ut&XExsJ!k9OEZISH}uI~}von#P!}t9 z59AgRbV!1eo=n8w>bjw9Yj`JM&nu2npppUHv;fc*0114!Ed)=W-hZSv?yW#U;@DO$ z;vE_NL%h!SR@&a{b(OO96-R?~bMWY@+=_=>X9N+fl@{~#7>lU;YVzK2o4$_B-d@G~i7wn-X(R=l zv_yw#(Dkqb=-yyHvH)OQLzL;_0TBb^a#rfOu?UB9l3X3rB$&$IY4d5i~AP=yQmeB=~ zkxbc*qOi8Z7dB>-EMd7TZCAZNbAp=WUh9&TlaZm5s05lA)S<$?_&uvaIhj&@%=CdB zF^VVXr6raKTLx-3r06+{r0^xff;%N-WPqT4PrS?0*uf}1ND%>=1j`shRYV{mvb2xs0! z?u(YuTLig5Qfn26tI{XJlj2_(k;)ljIvmEl(|x*NIqggqd|#cSglE>K@3R-Q`YKQi z8B&6b3n!*XMT?ex01~0P$!C{^R1e&c347;hr4b^nKqAkb+pGQ^|2YFWMQ?W86ZS`1 zUPo!yc5|R2O^>WpU_{Nao+PPSvheb0R?~_DxKEZl44-#FTdPTE63dN~rVyqX@dEz*Y4HtE>Dt*%XB^xq@O?q zjR3Kkb`r8A7U{r59&2nhE>kkolXfH=a9z50%17}@GeOLo`t@*D zNOQ#XX$_sTwktX7KctJP2M+G?JoSYHqBame6uDUs*{Hb21U$O4csYuS;N@7`(PM_n z6SpS7$1GWqa`)4m&W6RP7`(S9F?~1FRL$e8YUol&4l|}7S!)MKBT|BK)Pu+oF}hqT zYRgq!y(C>IR5jt+?q5FOG*hSlFwz~eb<6WAkAd4KAvcN;s4|%RU^CiENtIEq#s?+b zI+=>lVk#7sZ?hQ0y!~L~MSy>z8{6^_ktB)S4TC|*nNkN5`O%1c>?^lN8uYzl0zBRZ zJZ+*Ja#C@L35oeAJ%UfWffs0ra8FWPhR!Q60856mM#BWHUk6iQ_boh zJ|L23nSk)8VW5RUtI>f-P)WuDb6_}~`Ze;Of~#c&yx@kS#FjN;_6<)N>sTTvz~0J= zoX<;7+!0}lRNqae%D5?D9%)d-)>{1qhckc;le@!2j|;J4^|!{rKHjJnO|Aab;RdtM zR}Pvs@=^BCK{a9R&~k#9fGqKGJgxgBFXq7eV~UtvT8=!hPb3%Gg-m54Yzg>v0)~OM5JrV1C7LFAK!|h_ z-;aurV5bTqgg*0u9{>}#PmO*(@uDm+bzXc(lLtnM_A-FYDFLXx(z^dsGnHxRk&Pi> z0*QsmhQ<8%VRFc6NO8IwQ9UTvIJq}Pjlz=m2n+nSOfyJez%9=r=^_-JB%2w>#Ax zIgfYzp`GgR40cJ^9Fn4FBIgQE6*AtegNlJE%jYflaER-YQ$R#|~|NsaP0C zNnC`{=XIudkH^Shf$9ziG82%r9pwrUNXP&Bjk zvEc^6WpPf_+NS@BnlhDq&$!gc>w6Z6kNftUli+ZyqQB`%zNJtWObMPI|9SZV3lUWu zwsK^5nOCzo&?bbHf|DngXPT-;zr|1~6`KyU#}71eHI=01Rt(Ktv5K1wh7b@l&;!^J z#|VQg4?R_~PhVMgmtxk)lJj2r4wh(HW&W&+?F96x68O{t_clh?V;H7}9U6LXRn12GGu~;hji#lY^ z@tF5#GlQ9pkV#-~g2QRqbauXNXXh8~J4_k~8PxW&*(1{sY75$=LGYT6lIyrxzc@d@ zRyD8@0rcAHM0wkQa;tvrKz>kMjb_BY;XIXoom}nX5S{xbIvqk_KZzQ;o0A|JyDeXj zVgbcZH&}c^eMJaK<DkHu~;3W?wIe`1oa=HO=;eYn>-Z0SWSo z!32d`eToe~7o)JK1F2e(z}b(%b$;PL9=baoTYjv3o?VPcni|4V_~JZyCxPB{&%tZx zk0(YhsUNf%u9vQ0*^Bu(uD$#mttMk^K@6`EONCzX-yb=|9V;lVG?9%v7%qpYx}-I5 zg-bR$rkmi2`w3FdO9mYzAHYhKAKvXDa4@A3w8x9V@SRus1ZS2sYpz&adTH#TJW zPX|h@aWS>G0g3jDCHm_CUWXRo;sv{v2gUo`|1srQyP~Y-RRmL_(beMkdI?kt_Gk1< zWh?~829Fq4Ca^FOP_7P-hI9l#MSERpJ|Py^?ggf8vpfyXd*zzmtrVyPz-N2hNF7Tj z30Ov+?KVjSwATqWv+I18ez)%0=li$pT}<+Fq7xs;)gJCvx(3eVBvCwHdIC9SKlv@X z&MWR_`vlznzu76Coous*ISRS|ksTg(ln|;Ig@w=FR~Uk~&@dUtMgme4GflqTHnw<>@K|xx%l~FK&Ix)<;QpK;PX^JI%eI`qt`E?D zIgLks31MN5HYi!Pjs-1~+b%pNA?R1fo^R3tI?*&hk@_%mXquYH6QR9Vq>RJ1s2AKE zsV2W%WY2IM+2&g}dU%LL!% z5cZX_Rjua8xBj{-#8Kgf6Vdxm1c9+CjU`{-al%M>`)Wzg^ya*@EOoRR$SiOk`{ zDa(uUorJ+#!A}RId)c5pp)86Jq#b%gc<5gP;(u0`6WxAH+ipK-QGwf#?SE!`vP zMRLh%u_C-1kp?^IQ6dABhxBINWel`cv4S6W6=?3gyo2Q6OMxmFL8Kv@>r;E-HN56GgKf&nqBn1g+ zB_i|{z^Y~gW-z&H#OzD%V7%~3{jaY(^?$1C9yFTrq{X(VlNw&$bnqk^ zH|7>_#7?+fMunc$Tc8ao0@$5i5Z~h98SlD5|BAPNZjuGUfx$$P_-9>39w5Q~*#`EX zQGyjzacch-eT#g&+rA)p){|-@S^C)iYfWW!7g>IatXRSEB*i^}CJv9=(Ml@OkSsNp ztZOu;7QJ|*;k*m!wmGianBDaCr75y#QugyUOTue($-#ST!;)_!fHR%L+Jv!D*d?~} zNX-*cf$tgXI~fn^{&!Go*Zb6*ZSgJX@pes9YxpXO&B`p>fey}={Wk~pZANwc8;hOrWZmQ`aJwGT8G2ZshmcI5IVW4o^>>kt0O`)*FbG9O6V!#{QbzSB(BX6QGu)iUKm82p)X!n+2Ld7jg) z8o&Jl6LBMN1xZt9A(_%&+F4SI47FpZgN@Sv%>m4w%onC7HluJzt5R}7tl8Bv6J3l zUzanyi@mXQ*iHxcN(X1B%1wQnGuvud2vw+#tL+HK-pQ%bds>VXz;&UbA?KE9xs$>m zSR`$v0FtDyLA+k`dZ0^tOw-G!xXZ#DwhQhj(HOtgTxlbKUROi*09Q|>=` zZ@**9IDy{u%NApRVI6OCsi2S}p2P!@51`UF9_|2S z-xJAwwKkuduTLr}@?B^9)XK-~8sE#Bu~_G2`b?ssf_a2BrLddUuhdD9UtxW|Dd@Wj zC>vXVbN(zgm?UqXe-({dmHM>8((3HflP>G6=2*N}7&hMe`n{p@_;_uE^l0r#2@~qq z%ev{p4Hf^9GD(LP+FVXN5`6A0o_E2#PWPYI<){hZnpv`+zjs~-v>wmnKDv~u(FS%; zY4+D@DW+o_HPJ5CR`%5=m5#YRz500jW20v@IdV*UMYUW%cC^*D!eFJ9crM?IgpY>Q zfcT+jXwX;%TnhlnHns2CZulB0NorIaJSnWD*{_k#)pr3bO+`3<&SJph-eRE=1yTl} z*_$+l2E#9q4X2I>tXx4&qWv;>eX4#GQb|vY`cE_)ps_oPHrb6|Q0;|rN*g-SFmTZk zL51{~y%7ND!v`#uF7BT$mf^511Z+M5xXr55jhyZSFtIf`N1xA>?uW@e+vX=Goz9~e z5vnJKc6aR~V!2()rXDa8Z+S?@8oEC00LMqwO}2!tL|8NeG8_cjS7V?>5Pa z(0~uyEts=8-P#;hV_(g93L95uCYIbfY?@6XdAMz3;jQF^sB>-*COsK^jQom zK5x0%4J2lU{UANu^^Rp|1KIjYsLKAOghCWG;Rwx?$kfr-FUU@EQIm%n$`0+1I)woFGF%a23`n0DN#BiINZ zr?xBVx3o@c@|Za*)rBBQa+mt15DYtc!Ptw1ou^1@k@SSmV6;&&=qbCx`D1&vnUA&w z>6g8@`+@C+t=Ry_t1U;iua9*Et6Wa8PSs#*dSZ0p52I?k*`1fvfs$dJatt%)h9|pi z!sLRvlD)O_*NO7!$ufOw%rzTgO#=EvTX?T{0sKPg&Okg=;BA)io7y2DnQr4G61Q}X0I!_3ckm!hj%`@3G1Udrlj(Yj#rrsu!y-xTs% zHxpNRMssDadv!mFR6Bxu+&egWr0nx;Iks_-wF+b!V>(M`V+plyNH^c;a17#KWdmHN z{_n8ah{KIy*f9XVV&8gt zQy%~0tGW%g7r?2u$C0IIIg6cZVt${_7yvW^sU4z&1J6K}mW4K{hJo;=`?Wd$>EM6h z&Iz-FZ_~ck0zQ+);)wbYk(5Ortf^H3O+qA+#nnIh#8%|4vXOM#^Xxv@i_>?n&w*oY z$SE~Q9nIQFyg-4#4fe!nSpD6F&{s*E>sQfB2RMC03i}?N-iDZIqp_idQ@3|w{Z_ym zl*B^dp;Elmy(#eMfq?$Nm&7@M&Tp5f(xdejD$K&3T~pzwwQ2(~-l|%~&v8qQ7K-se zk6A2-i58+>rnCcORR=}%Z~;)dOZcBi>6@MB%@hlcbH8%Ht8v02$1UR7d9AX{F`RuX zQaYWLC180)L(1Y_vS|a~MYmc^msj}m^2YY;b$001P24C(gX{nelCFBL6{o(yKk&Ew zS+o*i0yxfys#nn~tiE_p#3D>)6E6N&As`wh6ua~rDs))3{zWQI0-CMhX0#0DX|{pW zM+M4eB150N-*ImF%qG4@rL#ugf!~=IiP`VN z)wtH=Hu1B^HaBXqizGYn<#Y^3seQ44lD(D9N=TJc4BLNdQgOZwe3Z^Sbh2r@h*FSN zh5{NYI&XXO;@bBW!B_KN!!GRaL=yS=QIy7vqhbEZY$JuFav!--Pgo|+7Hipg$DCm9 zQuX~6|Kns(FRsnE?Hg&uPdEC8iA|QRkMt` z0C(hanR`w#l`?R#ug}OoPWShzIX^e@YrmmZA-{ZdjTf>fLQ>Ct>glVBc#D^Uvnpsq zlXxDlIC(OokWqI=wepg7CooXhnsv(fjPVisADv(CzCJ-!{E|?^_7{``9drQHQRzDF zh%S^SFwzyV{ zOhAx%=DJF9DL&>X4-Iuf;e$>nrmW;^SxLhYuF=P@aVc``7dHDm-fhQKe@|{5ZMZVm zXOq9#02YG&b&1olU^BaZuEBFW`*`tnT7*h`w*I`-@v+?F^Z7mbEPats2jH;}shX<+ zs85h8lz#AX$=X<*^-B5nirA^u@p>0$-`&8tREF9K}(iwPM+7JwQ?T5Rv&Gyt>){8@zokF znC~2YF1w|sxW@@L%VjzC9~pI=PnI%%(GsPP`>R@gCTJj3dFk{1(SNVSj~#k-&^lDk zo5TRWHo!Ik=0`Ja55lwu>4YZd%%n@0dr0!OygN}wpq3pvJBvTM>bRDF0b>nS|2#wz zvQcYl(fdVyL6z}w2#Dz% zDN=Qy$*BDN-xNtfdkbm{HG3?()&w*CQv@5nqwmExpPzgu%~7+Ch+vMl4&+M1 zM1F0l5G#{=RZ51S`&s!MA;Ykh&(9IoVnUZb{Rt(#yQ8ftNhrK}Pm#d$I!-R~u#SQg zU$?Vq$$jH5qs~5bo}hOB0(4%bU>YafuV410%Cd*#5Z+-h#2 zOP~+Nje6j0{?g(VbXq$XXhlEcO-sIQF(1N%PXSky-}^luI_=^@8C~XDE?;m8>sRvR zy#)N%z;eX}23k_336n{1Ny@z=p8oMr#gqJ2s7MtFojnXqp<#Jc%J03mP;&Rsh6P%5 zkgnT9oy1i9vqWI}eY;JykX8*#ijQN;2J%Tf9!kq>0&+5|UtG{*#TZz9ySkZLH{b}m z6)F_1Q4PQKDv22oDl}T0q~nb@OPV&+LnqkMe5ys@|JYxYDt5sAw5(~daLg?? zGwM*wwVwL+lL#&2n~97I;`hT>-kgAI^@?Q8zsLIIXEOwy%kgS@{ZdnZ{L{|Fv^jx( zVL-NgqUb-)*4Xm^B$+Iw^*Xs;g zM1vO-+TF#tgveVcs%cs-ZfE*<#1gTr{e%?=q>iB+Vb^jfi*Pk6Ux4tU!4gdAc}9F^ zNo|+8l#H3%eP<5bTdC3ZYTNEW1#Vpg&%v;jlz8t`u@8lrtC|xH-mA@NvFOaPULVAW z83VBV|N7Y}|I>~{2+6h9CIG_-;V_-=Mc#Yy3wp@Fa%jj=34o(w1#sAsXz1>&jp4Xg zs$(>_Ar5wjVCI1gDShcUwu!eAGHk9NiRn7dQV4G-CS_emM@kyg>^8kH89 z+XNs<8;+W=f;)6|-?NH24_`$C*`~vnJ&PH7@?Y}u)!v`ClomKAxlm0Wx3$2E?8MvV z0x<@h3a)?oiVt3e-$gz{0?Xf?_DhdGS%}~$lLIgY>S>n64Q~WArJIRXvomKQ`JQ z?!W<#xR`avFC1$sDy7|a(eif?q}fvhj{D3BDhDp|a542Y2H-%1htaW#hFB-^kHTqz z2#A=O-)6+D?jONzd`iC zAGEaCKY-Xq7oksC$JD_+V6c?{GQPDm(oQ`x4Qt11e|a&)Cs46Q{K@iuoh$>n*E2iH zh}WXiN;}}>!=e4u87Dx~r%&yF(w&#V!*i?j%LUxC-=;6Ec#c~(hXf!OsT4(-f&*od zqTLr4Uc7<*rqo)9tdu|6sQhL7BVy5A8Vwy7(<^p?wShjt4E&zC_rL>leyV(*$C17X zbLZ1ls@ZkAqdz@K$GS8yl2PK%=3jiZGhpH`F5vYrQeQcL%;270`6N20rRp9a+p)xj zFJ38;7P))82$}}9T*HBxxHQPhlwd_dDAJrlVuLHKfAS7zcz!F=Vj>Wxb1+YiZe-TB zx$TdmJ+@j$zuT6gD4zm#Z&YzwoZ*a0C3}(1Wtqvi9TCIbClqx4r!G+Ijvqcj)5AWa z=?1Ga%?Z4X+qD)2KW+oimiD0R`t!}o%=ZvF=*UFM46e234HzYYsiI50h!1Rf0zOZU$_R0BTOv<2-%s913PJrl+NzUW ztFy;z_Eu~U>a3RP7`i4c#19v7Z%y9+VJ+k`Hz8T_lpq2(LfEQNo9(Y}>{qJN0B%br zWb^~b-WnV!Ml`6g=i}sfgVU8;v7v50FAQ`}fc#^x46EUO(9il=N3Bes$U(HnomO`v zyoH`FaTi8u40zn4X{Y`0WNq0EMRX^muTM$#-0y`p+0`^9?Nm5q{~csRSe%s=bhVfc5f;jMP$p$0Irf}M=TvUFYQMU ztLgy%+CMs^-BR~7kHCqe+S|E1q-6m49RU0# zP5c+`7oV>%LA%iFpZu3?LW6(@G^i%DPjEW#@1RmT$}gM*%HETir(;rB1PDsif93>L zVqGdbsz*9j0?Q}r#B?jEAGiFGGDBCV@LEIAJrN@FG{;P*Dno^#xG73|O-Hn-^F-h= z%kVoQ6877mcm%CDE4tr09k77@|a9;i_7eSL1)rC7Cx_O!iTkIbrThr(oL5U&Z?8^oPqa*w(D@8kIr=``De z5=NIeg9I>cPOHDd?TU;GT2g^=;El-^HL03YGzlgO7x{;b>{>ZlAq!| z<9(CHbFL$MUZ~p|rX^!dF+zBCR1zfjXHf#MI-%wP0Ns*mp0h#81-n1gdEMy|874=i zwf&^MyQp7H^xrnEpqK7e)taBM5bSw=Y5*tgE`hd!pL$uG8fMBpAbF>w&PbWZ_p9*E z=?Eo;X7FPJG#5$`-23p7mTQH`-;jfihor;k#4nS;A|$v!d7hZNYc@MkXO!jx82j8q zs40W9Vax#O$lxZfjQ^g%1ueJ7;vO%b=D9-NYg&hpFQWgMff#;T;_89$T+$xrs=ZlP zu^%h)GVdG;`R;Q!YZA6b(iNHj$6i2g zy@nBZuU4tDU8TwRA6tRvzgq9f$2|b{e0tqrP#2pQJK^g$CEbsZJX<21)n1TNbu;8` z>fh>zHi%mtK_D8Y^t8suVyK4ST;+oKpt&npKc0&{rG}#^Q`o|l$7(C`4wG=WM7^Ea zK^OTPJxam~qjJ6cK$P;1z?g$?r>N#lT;4O+UEu2uVSm$wvVeP=t_8*EIQi!#wp4sn ztB=jt^2Qkg>U!Irpj#T$z6j5tt6VFyOUHf9hT0R(iFd4G24?5K&W$Pmgk)!6j*;Mp za3G+UZJVT*?>D`mp=4`7dE(*1FyYuJZjIbzZT#M2{7TXXn8wA*+kP)!qU9&&>GkAF7sP>P5C1Mw15;&K z_u$(Nh+C1ao~bV!tLg{np?0orCPo|f{&%LNm4;)s6OOVJ>}eP@E0lD%_u_jZZs?7f za?4`y#)6JZCuwMbzM3547L_6MjnmuD5x<)_d-0n_e)r7F8Qva=o1kGT=O0(j;M=3GG-1oyrm(;D&(dBYhys_V3*XZbEO0S7IUSEkqtQ#D~+pUFcfoyO9AAo2dPiY$)nM5bRM?-~BXFfiu*54EWAaTy2kLPQ8CzX$4wash)Lths6;5-jeD8obu~$x^K*%%rs*1xiL$f zs#^{|KPdk@Vz*NfZ7_KHbA%*CPp#jxEo*Yh_rI6yyL`W;V6%Tq-*oSG`vlsdW>?II z(${^Dl4)7DcWlj55;;P_tX zpU+iWPhu!o7jgz1O;ZC6m(|yG0_~QAgHl7bFNf?--Iln#?Qm*n)^cceYFOcNSb1uA z-E#Q3)QAhq5m!>T-&o#0xa>G1+&rARRAiB|s= zqnEbpN@{t|)m?VKb~~rVdjE4b4yWzS`nA_EhhiiAIKI=cXjA;T zw0#$@?XqJ9{EkVyoVNeoul+Bqkyfcco^28z`26eOkF-O7ejS1zVM_mIDjZ2t|DB|F zB-!M5vgMJ(cE1lhA4&24of32;b^Gts-AB?Af2SQjawO~bk?bSsg}>9wk7U&S&Nz1@ z^TO}UD@U?!{LUIYa`fKsqmPapd-40&yCcUx|33cX$caC{Pr%b5=@m#Jou$6Q(o1KX ztgtP6j|4#~*6BIkHX(;$!8d;8>`u=sOu>-oF^Mbr*((-stAzY?ZfK7dt(WVbZrCBr z9qcNII%p^Z1P`WXU#4S-E(f3348Q(Wyx9iXL67^>Tkq79kKe+!qtmok%Pcd>?N-a3 zGb+4SD}pjAx35<2&ZtUUtvZ}>Dr@yrc1CsKs<^s5qo!`P=3GYYh1J?C8Fe>S>jpFG z@2%E9$~gUE_4K=pGoM$_{Kz=_XZ0*RlP|r-SIBHoUu)3IY&2PGw9Gtbw|34sv&nm{ zDJb*&_Obdi~ zeS6lm#6Q;#XI;g#rb8;bmCRWzHQt7eh(@OF7tS@SHVqF>Al~(Q1wpLwa5C zkfkcRA)2=v*m=~DYCrUGS$)zbUHT@m_2|~tqxZapqb>5v6?FYbx_*Ul=tS!9k82O| zJ+&r;8olcp0{e&P^-1;pgQw`KyrTu8n;~a!9{y-Yr>xVeSwtV<=5%oOL^hh4O}{$7)V+7FJ!CHSQMYXvD;Sys@K(-LSq1?EaRfe+)Coy{wRjO zaPAp@!9olHn>z+nu5Lwg02vPSIP0&v%CdOj$}u{v=f(APmC2O_!<>K@N6pd!9GN8> zNnf~e>+8pV+n%sqoI0LC1B1(1j!#({OgPEq#E&=2KTffnV{eb+hm3n~8Uk#?6}b8k zgdKu5jsw!`aAlu6D|SQQPaXf@eqtRClcqq9DO+jle}21>gduU;Fu|>EG0ypT9k&W} zsSeTl5VcAeuy9u%(6SHnJbb>)wdl}>h!hXzE+Oj>ka1Uv^WU99v!o9b+$526k+T1% ziHvfQIySYpm)KvC5Y*z|;8xjR;XQFQaMuHK%lyw8{t%Ef_l$S1UgdUghT4^?uPSn` z%^se{JZa}V`Nz!gVY5p_%hc^3%*5p*gO#D$p071q)A#KjEvX5N4^uGo+7LTN32V@Q z6Uxdka#nJvy&8N@EHJ?R4L3bC+j^gAvdi7;_LhsW0w6a-Mpf0sr|;Sgt3x|yD~&6s za-v-wX@?8?KkRZVsMwN1KXq`eJi5lqrX#&mK)S1lS0hw_GAjn5cXqM7f_H>86a5co zSj@h*XiHpfmtI>i7^Sk`YHu*^Gd%PD!}EtiDV-uhqu*Hcx&JeC?vG6Ge;nU^v&}X$ z469v`yXKO+%>7zsu1T6(A(cxPmDuJsmlz>bnrm)pNk!MWk5ovd%eOO^q|51`5+(ck z3qC)*KkwV~{dzqf*Hb;4Yx8JHO|{mh%N!l7sWW{D%@|!hsBel>)5s>(OEmf$TvLn9 zamkL0h%~)Z4R}=C+EqolT!Uq*Z{{(i2jp!q5UAg|sPCcC+_v1T@Ug;IM`pUL0L&)M zbPVx~#k~b+hI=v4*{~t$T8VD~L(0>6y{?VbZREG^*4O$RCBme2)uPXhaJUdysABP5 zZ0sc`ka94;_*t%Xak~RT-(|O-QTC{2q|neS!l-jRtU;$mv=?pwDf; z_w7o+yS$$3c6)1$U;z)<#X%iiB5|I{%qgvEGixSxvLW!y3R$y;t>D%&BAVfA5W09- zdhKO_p?c5m>$zNxDy6|pLpz;7DBHEGCF!H>T=Jn^<-!6g3hhAq!L>4~ba06t<;(!#r?2CC%NMABv zwEN}2%MQqqAX|byURFN?&VK(Cg2I_>RQ7?4=PWNo*(`e)x7FrXgM&;zPvy99U8^N% zo33G2W~=|sG25uwN@7kg{==Tbxz5cieWGDKr+g*Cm_tEYyU;(#f5&AS2Nkx;An zC%G;Pwy#J2hp_F5k1^3xRl^aT0hvcPMg)UkVv;d0+tgYB7ZiUK^^u4XTkj~^KewDw zBF*Hw^_+>h-VZ)~>3B>TF>lck;+kkuhg6-{nL+#Yo;}fz2wc$rIiCGP(H6VY@jCj^ zjc4y`Y$d7hMonE9Rj(#K0-S{(^IR1K#=A2eMq-bb)beH5>R$%LW?9Q6{xq?6uPf;Y z4}xjYDJP-hMNbjECW+@9kV#Wz%r45gbYczpucf29gp;o~=XIu0cdE-%0cb4ZX7pU_ zU9Z;2L3r0BI@>57SlE$`C}rxvSp2?vD#~G;1HI6d2`a#q@BYw(5>kAh8@VY-5|MhB zoBE;c9Prep0B}hM3pS~jGZ9f3d#y|oa~~SJulI2Ta<}h z=pVX*#Qi+syABY3ozKHO^%Y|MT1xkMZxn zMw%=6HCtP-jC0r{J`(XuKr<^m-VFY1kb71ZL5m%fLRAUVT+756g-$Vt5jsN-X>wM; zySg{spU#-UJG4d!v>sO0AU<5oK<=UBw2Bu49W3>EiUw+S4iGj%yhDtC?J2zg1X<3}*SKF;5; zad8JD%_4SN?v8R^8|!tsmH^!qHLLeuMnNA%8EPV>$or;b!P|mIFZ;6Po0_=vp)!I> zJ1?e*+NYN%&YpX%TTpFqB`*+|SNkC{jfx*|_E@nI5q2SOcLi>=%wDKDE+nV(X@JOI zMLJETtk#`ckPV92pAWvKbQCX{m3Z}>#^lD=(71eW%Gq0+^iGD^@Hm?saVuJzm;j8; z4AeGwlS7IU!}sQMsM;Xnq92xh2zozU=;N-q$tnaG=^t33}6O)ep4kCvVh#9fi?18t3%|C;(SsC@Oqn>(H@fX4q z1|V3fn2FK34}CAI_$-8Pq4a&(277OEe}=pfpLoi_km~#tg)`Q1=^7Pb-&*&lKC4s< z8og13jg8)Q#-vYcd`QVOH8SncNDjImm*Gn8L4@cm+M^3HbUIamOPz9WF_}V#vsGps zx*G&>a8TxAHyOSujZ_*P4!BD9L+a&i3cnT1H$Jcl3W%$rGOj~lKfvX@(FC9KvGjo> z{qWnp{)Sm4IX`ZXfv%Uhw>;vj=@P@q!isauC-6R%D$tOqBXIf6ZF_xp@QJ0>K77pL zw{9B1_cA?$)Zau)1^hO?%pX!%0G6P?MBO7gy>$9U#X&w?2XF&F5hdiONJCL%j z2*dZJxbD5VRX?gieU*}uz7W1;+ip7_v`pnb9IJYHyd=3h-Lv)gyaq^FJW$yt$k6JS zaqj+U>Lo`6$(=4x;l(cgGpgYXQ|3GDIz?1{_`Uz|6CePfOuu?m-RWDq=y};Kz#V1B z_u1wJjxe<44QPMN(MHa)ZgV+j%j#1?%WS_*^kKyDuegjZ_h8y7z=5b(X_3@wF;FIj zp*D)KR%R;#IpgP-fPdB>0zwJ>b0BaVWgz#j14>4@5&G!$_uALgu;?Xchpg*Vz3AIX zm>=<<81IlLtPMX-p#kMWPL~6QIcs$(7Zl&KJ>^jEved;kGHi3%Gx%5+zgx}oX=YRw znn+Rc<|-LEz+9maew^W>CoF_F;I0NBTmdLIL>Le(& z^VH*xdf$S<=%UivP<2M$yf zY{UrBCyQW2zyO=`3`EyX+Y9-?!u;#5X~lsNX_?t3Ac!Q4IM9l+=bAZK*K9)!p@C%pB)(gsc3Z_?}sQX*;7hhI0YZi&vs3 zJ5b-oK>=I(dXXCHBMjyG@HJ9|M;mABD2(PE>j2X^$(Qg)Wgi*LOy_kPa(#>(B~~28 zc5sdoF4Fd{Yv0oAxgH5Ju%|?T)7_i z=U=!3%f^=3v6Jq)q6PW~wIePX-9;rgGC;WbAT|J&#(+j}3fr8#XbFL1L#duUT-eE+v>E&EYp2qnrtA2pDrYkBf(FAIA=;_XFs};0T~3SgcR?x z4(FV%$FG0DT&7{20c&wC=QE?C3R}y9$BKuU3bm&tcx_QE*V06eplcT=gL&^gXtozPr z=Ig)a?=q*uy26znPM@rYcOkSo({+Zr^*4Q0*v^+u?4>?k4F1=AwMkFafFCfa%~ z%EEAseg!@CzSi4toPn62Cex-Ltat6?Jp^DJkQ#ZZ{m9jx89?0SLL`2Myg~&L=6CB2Rn{}Gf2rh^8bnxQqb6pD&cO%oS;7<7gqaQaHZkA?LG8QA z`>;;(yIoR(P770{1i9s=XH zjS`4MS*1V8Y$18xU)uYKAFzLijfNMp<-UG` zop1QJaY3nF)=`Wz3nTX>CW*H`h1wJ`UDDz(#G}x!VlwT;4GD!FQ!&C{z~aCUqrRu2dR++Wfzc;}7p9*lR?% zF9OdU$wNEF!2IO5-kD)Op_03Lrnl}O<{Qi$kYGJB)4F(8_WdU|xa4iH=V=Qr$G5A% zWINv5ItvLivI%py+e!!7;!<0wQfjA`0b7pkMNOc8Tp2F4J^uM?zhae~{gP?i3GENd zVuSh`?}cb3+LzyLn{QJ3S8RU*x4T@-6OF1 zP7o99mWE5CdSXA6-QN}tDA1(3Pn$;1oqTtwBy;e^GVbk04>?av{t7P68wADOj#|Ok zXMC%S(Ix9mV{w4ZIpXqG7;$nt?id3&CvHJpvl|+rxy3oKe!2R1y?L4&zA0osio|{1 z;Y>KL&z!R!`8S&=#_eF)ox!+&7a}PQgq3W?d-aDd~meKCF8)Mfexg~t1 zU@>73kYd*ZavUp&(KxH-Zcg6eq?WH<+AaxglB#LGV7hB5)C%i`1X|;)G6Zf!1@)G? zXoyKBF9{`+fdJ}6V{bSzqrP^*4F}b~(MV5vaeN00s5_rCWSrh{OW(N@@qiNR>g>&e zB$3=^XbhvYQoVpzdDi9Kf_laHg_jhMtw7G+D=TDg^w+c&gO{a8lAV>0F&N)t|1~#3 zZrf(X%0K`3WGI6T2+Q;Itod=mqLQ~`YhAX!EmvLbH7R7dcsYEVqs%hXQ0zo08|vMJ z6EHxK+u-^7?T?w%Hx&6d6c|B>^q!8tv|Vp)sQOzCb6b~j`5ZEjm0<%7AZe@Yi9Zzo zIF8=Q&%C zgFGzz37@lIjOnYbMy(C=+dFO}==B889@Oc?ZT+VlT&74FzL`pj`{K-ImT2o@9cl=W-n0h5OzH8Mr&8i0omVN| zDM`{*#x(XA1HMv%|5fSo-4*2x99Z{oUpZGXdo*HXKIP)QBS(h;6Oo-7QoN&OW|Z@P zwewc>l;CB6hmrTc2{U&M9js}6w0J~^rUI;AaEt5N z@s3qcV5+;~P>trM5JxpePgQP_w#fEXm%4-!@fq;TP)#iUT#eNY9^T9jc zT5ogoc&>)SM>NkEXWI0~VdqL@;Suc}*H^O6?k?=Bp2Zrle-UPoZBjgbyH}_)J+bq- z{}&~qC?2nNU#(XDaZBLkKYohl5L4noxD8~F!Ux`3NBG9!$!_O4ts1RjFLzz28+=5g@%Jd0#SxV{u562H=9cW{?gM?85EH_DJn@0uyW1?={GVu z6ZEZi+hn9|yRhC9`BF}&vvy!KuK|2a{O{-|c_9ZSxP|ENKm=2}+IE?{#Gxm7!uY-& z1umc8JdM5SAb0vmm@P0|BD9ELKV>@2XhjgmTi(3xJt^w_=dCBWoI@73%124x577rs ze~OSXAjqewj*$l0#W?@0q}q|x&kU0z4b~CTcMr}Q{s=)Lm9#ydY^B{g`PnKfu{S$# z>)Q{Epla9aLI84^BYj-=QxHjf_4=)ht98ElOFoT(h?p^CZ2e7Dsi&&AQoDBi?;jM1 z=mPK>;(!LFZ%S^QJbiH2Uzx)jrQWv>a^+(pX8z%+4+=2}(S}`{P^<9MMLw83zMR8RqS0M<7Dlu5BuGvDNu8k!^Ie#_mD5RzLMYPFQln(vKj2N>-V5c2)2Esne? z%>CNZUI*^1`XdW9(3@^5+`u1J%K_aNFWpRgQ%9jrA)2ykx83ct-Rx6o-|T&9SN5!B zyca8nc9>m#V6kkc{8osF@P(1&DUyO*lSUywHF-lfwa&H1Dd#99sW@BeS*7E<5#FBy zg`u-lu8#@=Kke`1WBpn}hdUY`U!X^hT=KZt_~c^shDg&1mW}XXMI@--j4QipTwS=; zuxo^jvTD2k6?{u}oxg<%9S)tg_&jn&$2gcXa;H;D)J=-2xarrwAFfuHbgTGYRCFOp zVbqu{9+*?t37xzzXIp{7VAtDq!cqsE?t4ZbX|#f>qF-h_ z%f~e+chTe^Mg`^7?%ciXV_X=&wVe%9a(K?z9%bNP3|i3olu9e8HJ2QIdeJ>pZ{O-2 z&FY!Zz^V1eKLvkE?4xQWQ$e=T{n>HztIMooFDw*XM%|6iZ@*IdNAg`sEAE7M!5TKv zxPmQnHVaOus_@IS61wAn79bexx%!$jCCC2H9DTPV>O&7M98xtY7q9G6oULRD=zZej zY*;qwZ~!4|V(zbeu#}w>+Rq%oZwVEP$@Dp?$0=lUxT^xKqUWDtgReJ>RG6p3m2^OV zjk9;}KgDu}m~Lo*EZqLi)@nxq9Gy=1RNf3Gm=TM;Fo)NlKeIQZQ`{g5oI9nCVN>BS zPxAnl2YfP1&Yp5>HK~?_8>v9rE?ReOu_4;d|)>GhJxna#+$MISPbbwZKhI1C80U$$ICwccPfgVd(-w?{;|Hyj3BU<{CE0&5v!}oQM_YNp2_bWcLyZ%nYOch|@>v7;m(v3`Lv_Ia- za7iyJ->z}dCJOyW7(GaWoxB#6dMaTWXjk-L8Hu^B6Xl{7I_C>fk6K1ahzjoO&z=}m zQggJAcV>%J!{*Kp{qM4WQa?VD{oBnx;d0xU&S60NL)Wen&j+On~(0YtGDY|j%6CaZnW8=Xc-uax`;kT(a;$e6I$ zck$N*LoF)fsq;op!X0*^7Z$N#*}6g>Y0~~E+~SmQcNIjKU>f*4sbWO&(fYcKHbd*_=)tao6zr8rPsd7;BMrU#KE;i8Yt80T<$eM-hF&7yo^ zixp0r2^EL5TmyY5eWGPC>jZ{447z z%UHju|1woKvjPA|7qZcZ+;)tw8Y2QJyGwU++!69I!t-9C`7qd*(2pk&w<{OE(k(5e zuX*KzDjibas}SNM!h<#b|CgiH)Xw~gsPS=WI1`yn2&DLJ}d`;KlLI(KHi&y=+Kb*ufMI?>X;2Nht$;;jUr_!b&feqh#UlZ!%p%~ z1TGtuH5eLkXy+8ASFh8xU{7hIYWxjsWrYLkbw!4iruf_~XEFI#hKF{xMPa%>A;LxA zXtmd*U4OL=y(Ab=0xVsbaW>0W)SvYltNrxJGCvtB&8?c>}2%=1S zTH(&6ZVzp1-)szVmtsK7z`hs(m{~e@z(luCp`G(2{axP;y;E&FuuYi}vAyIl!>0BA zs4EDGG|yojUOdvjU4!>um|`;&LpoEbH91N>wdf2vGEK2!7w?{`Rjy0$qKi(?sLzGQ zdPT`y7p{%oSPK5vQ}h~0`Vn1ts-XSYRj++z3EY<2e~u3bSXd*_7aA3$p|$6dmh zpdc+LIC1g0zMuwqg71K-=U{a$^DQP!b97}bY>1p-xwIwoofGW>3fkX{cHgWp{gynky~Ak%NIHuEo~ zZyiz9m$P}0RWf^;5fhh_x;netyzPdk^>~ZPMViwI#n{c;*mTPMT6~}sZn5qx!TI<- zK@Mkx-x79I1ekz$Z@+wBiN8}gyyCc)5Zt#u-S;o+mL}#VfHLx{K4;|%S>%3IIc5yD zD~3ntrVs5rGHk~xUna-%0qVg$vXPx9cOyMOW=maS?4JHvY=gi_A&;F;i$U0ga*lW% zam7Mb+s$WmypE#Y51RkeB+v4mznB&C7ih3SlS(oxxuj_{aB^kuuC*eqe*OHg&i z8QTRXSkF+Yo zN6ORqD*C|WN_d{Vv$(pY;Jlr|v0U?Gj3dy;HuEQ;OD10};B7iQvkfN1s=f^`x2^in zV^PS5j;LEDN>Fb}_Vyi(d7a$Auj04D*E z^HN)bVwXJ)aWY&iavF&h~Z{+OSISUkU4_h2NqB|XO*zE9itq@}YcAJickOFS3-ED@qdj~-D z^BB4?&hU=Fkc1=|)5^IMNvHc887KGx0;^WF4kTdnkDox8Ar&YpO{G4`Ght0 zXFzf0On~kZV3`9!vzEU?S;5@+`4V{iZu>72C$<0TyE?EMPUJ#kazt1ulw~ z!kkLqowqM!L|Bzi9^dBxwcnV*!6H%d%mdZ8i_(&{e}BOJ?9qGbXej%Ovzm<14qBoc zC%_I;2y0_>&3XWJF0QekvGYQO0SEAxeoVIqi&%uuC#YDl;ca6lcRS&$Mc6)W*@GG2 z7<*R_LGM8cJc~>s>`2b6g4E2v2|TVdzbvSZ9yQ%EATbR zdSMNRU6{yZrgoXsv1lA?AT-IV^x5vWje~=J*9zri;IIR3hXH^<0T?_f*u-pw`dfWJ zpH@%36c@PDn2BTwDQjc9+gKKTuki;GDv`dN3#YE=8!HTbH$B-1Mbb&N8qhUi>7HYn zA3oqujbZH@pmpNnw>8ity5$+I82f4aJyG$H?~5OduG> zyWC7LoKBze_d@2Ji+wWdn~n6*QQzAx$fO9Q}% z666gei+MF*jE)_5pQk{T{5&y}z-1A3gI>{=gu_N68wCNbVrEi8QX-ArbWDIE)cD4E zw?`v4l%PLfoz*ReF&g9|gn;lheCZ5PcJ|jAO+4y*Q)bF1*zdm{`aIOo^II}2}|~j-sEr| zzw3_ivoVva6~Eq2!_jY=UV=gq@>VtbChh!YDFK2D4fO_zAFwE0@Nr)zm-{1R_drpmOTi{lCfA;;F_yR!en@Ww(z^od&&rIBK<$H8=^EQw zQuG*+yOa+AjE<87j-E&{x~U;3I5kU4KskMoS@WM1(nWBr_y2VmV| znEyhY?PL5Jq2}A|1EQLGdDA-K*ja=>ERYQ`zyS|3q0C=+>zfd70=6fU;WDdlMFFv` zVXr6L9|~(D=PVSope_v4*P4%zT!yl;o`Eo0t13}o7WoAptx>T%@PmErEv)lkde>yx z;iNjZ+YFg*9W*36aki6HjdTC=-Cp^WT)M#IHXT`&Aa`MLmx~a*o@qQ)g{u~5>R!B3 z14nwCWV!2t&CO1;SWvC9ozx#kjF)&rKj8sl&~PIA^{Tvs|J8=*Nn}CfHMxr z`UJT%;~vOF(~CE=rcNJHcD(O_*bfcHKZ>Q(0N`XP*2(g$QKd^21*|<=udC5lH|C){ z0gIIe<|Nr9-4Yy(NZHcIyF2e?QwOxB`;_U{U3CqV3FI%Ag8%o0*Qwy$_7>m0NA7Eb z+~-cGi!I9U7F&c;=&X8b4nJ01JLK0Fxq?>wlb4Q%Gr>RaA^QgP=INMBRd5m;(v2vS z?QX{X=m%IfIK9`ohcTwTC=NB{iaVsCmj&=I#gA|Oc~W342Ng%uH{1(S;@2GPG4Q3% zC)}v{f+Kfg(~j2#0D=NIExDcU&7ZN23?UbAh^7hO+J>(eRBUerl+i7j?0u2Xu}LEA z>sK(rYJ7kQRQesx&uQ!%0Jwg?R>s`3PzA0}xLh)nb%HVgXgh2@FLzsb(~vMyn;MQ} zL$K~Q*72$w02p(NRe$hk&A~&LHQ@SmWSJ=8rKZ0Omy#!ERi6N5kBT`=Xlu$9Z~#4u z1F%|zPhC}X5y*S|#@36GYNudx@qQP}3iQKai&K%!Aq;;7AQ7VYDaA@wb+LsqIdFO6 zq(fQV4*26$X_Yn8q90iQuT8Tn=f0QFQ_0VMGFdZpn=4D8qR(aFH7t_RbT|CL4CgJ% zxIULLm#GW&TlrysDJmnNpb)^gK|%g^&0~?cM&q8Kc^gQ}ht`0x(kVCO>jdc3LG1YQ zut6fUrxte(syG65sbU`uo2PvGUMi%>M^3v(-^JJOh@3wExWvi_`78bw1J4ntY)~pn zV07ngeQjf<><4xsj7&%db)pSt7`+QdQk=>JE8`NgA))WlPuDMJ)Kbj%3}zVC7~Xh{ zCza3@m`Z5Vt1@xt@+LNQ+hzO{*!kI!>fip6H#T=Pem4#!qei9MUBFwYnbvBBW<>() z#;~}*-Kdi5OJjZ^(lDAR??fv8TvA={z)~_hnp35W)39z__xFpt zWC`YHsLj*2_gT^XKa|2bVl}s#$iF2nD{Kf(Ieh8|94X$C`n#tQ;)4E4|B-!4WZ>Ey z3p6D4HLety#K%RyIJ;GVD%DOHM((W=R(W($GS}!^SKqtMKsTh&^Cz_R8V+f3%^$76 zxndBKjcpLSW53&AO{VCz2ddeS&-*-+BL*K?Fk3lh_s4}-tTE`6^B;V-ZS=&v<@*6b zwGBg6uITpxF6#u~P`MG1*$Z#z@yJA0N_0n`?3dg=U4K|vt!)&e62&`PFadLsvSk$b z3I%-MEcDNvp1O5>W_D4UKJ<0#65yFINbmipsz zwx)G|%Ike4{&`baBnRN}?QG00+dZeQ!`eBLPK0(C)jW=Z@PahEFb|^pWwB8 z?)pnJAA}I_Uvfa)_2@(Gc*!?8-!XgHhj4mFc_nlmewYFFlEy`U!})~40$pG!v`a2K zB4B-6jKAWZx8v^p?A{%Fy=>Ov>SKKU$)qSIr|}hC$V5`37F?~)*BpY={^0$7nWhV1 zf5f2D-8=H?*WL*^ZtC@h;Yj2dkSl~XWPWiK#ip99C36lFNwL+xak!gBvD;T!Oz7K~ zo-N5ow=m=C#r1{ha+mu-{G{*Xr=aG2P|S%o?>(qa^Zz*U6^4VQau!B^_+Pf7qvbO_omI#>%3pr_57Q3^bK!_KU}$xN4!y}wfE)MJ)N;H+N0kF{)>mn z1FhB(3;UfalSWg1e^mN>iG<%e4-OFdPv;;z3*w%4oqLduxAr%V&J#VoB%=}KCbu&w za`?IRWZT=ukz->MO+MH6I(I6-TT=`rHLhdHA-2c(Z=70gZZu6$y!%gDHatg03Q#hKQ676h-;VC!JABdhi55XioE+7pqfoSQYx#8rs!A)i!M6YMM=xi|C0W=V2Jxac~ zk_#Z{&J&$7|4LX4t`;bK6G+uel=nDiVG z_kguVn4iXI@y%vBtooX3d9<`;PlKi|QAF-if_t+;X+qaQg6gtE97eZ%&Q+LRv7@PU zv&A?UEqlnQYQ-}m00T7NWukUxVLa(EPiw~GTb{;$-MwI=q#Hwpv^hq$5;F-DO;**U zAG|HUXYCvie`EH)ou>CIz`ZxRf_Xv7FYbP#-!?J1i_=YMxgpcfS0-C$qJlQ%j+ofgVg30hgtkN%TLu_QHc5lRQp1XylKKfi(=CCyPwNE+frQ4BUaTi(9AAl z-@=N(?(H@^KgMXmnIN-Qp{4vd4IajIPf1_9Ry5F`?9HpuGdyI@+7NNFyWP#)Fqza|1sLOp zxf`8}0lR>d`?E&SaxGKELx`_Jl3p?2?-o*-kYWZ>+c0i;o%8uzkF&nE!3K3lfh&Wn zpSSZ#vY}b=^-;Z|zwn{-)vr`0kF{&VIP1o{Va(}~-RXjooKWdE>}?6KChmKVP{i1} zA_1xw;DF|HQnD4Fq0}-)-Id}7Gl_B1n%YEWm4^FA3&XWMwdDPMwX_;_9F>B5kX}y) zm9eUMR$ps4W~*A^fs7y##JRj@8gS%WrPIz*XB?&WI-!wB8_Q#Ezc&Vz(J?8d2~Ps9b?#IDOhjAuwPvJ7-1^FjA%`(lrxY)7F(B+vn{0S`pq`i&F)FTV-ouq& znHo|;Ms=!`w9A_z`tiqv;6Z1xQPd}ARojHD7UD;}XiAW}CgqO3J1|Tra)PuRyTaGZ zg3$<E4qqkI}{g z#?Gg3{JmnAqxh+zYNHbd|Estj;QVgPJ{Zs(eL|!i&{RoUNt9!WXdR6lE*yV+n{U}P zIa(C1l`zHCCk|_lO{g%@0YVxfhU~)Rn3eL&OoF*vds!ZR49B-;oagXn0BCw%0!+C- zmLjA`?N@v&XayNmrotXH^NpeDT&;W*?tsi~bV3fh2;PK@mx;m>i`-4vB*$BQ&*n?9 z<~m|TZFj%Y1|7L8v_^glPJ{B*#A^FoU9sL5fZgW>(cFDU5aNiln8Z{=-}LT>DNofQTm1ILv^5fULFB29Hm&0!z)FR@1O0~q z!$w=a!=ZG5W)Ug8dV_fp6VS8ueXOoL=9t`OQ|}OGOw)NYoCF-ZQ~37pPKaboVtx8q!=i`H-=%juB^;yhzfQsW)QlrBj0}5&7(YXhGeNwLEpGedEF*%Qd)qGB zNA(SXS{g54{AYdVcsla8wx1l4ohY`gkf1T+T*UEKnI9(_a;ieg-$5Ym; zzeAeh_Wc~mA+5ijfB{3miB3v{B1=*e(H5{!09!z1(xuK$Arxj%Y)t$zF z-Ey}g&GG=jjLo1V-MI2N+DLb*=e*U|41az_ZS?3__k9<5jmcmTVC@fBm(BTcF`oU*PZ!yD^FvdV+aWg&eJ_f0t0rTmx^PqtW~k~YNVud5u-5cem% zev-CwdoEllsSB#^=$5ThZ-R_kbW~{Um-lz94_TwT$&pP8=N$V{jfg9DzsHM0HFwhU zK=b1*q9Vnue!f=1)F+OsO{6hPTI{Yb_}@1&(=|6ydW-pLo9eQ3QB6)g`+-?OIhd7l zKJ{hcZ47^Rr0-viv^dIjj12S-g2LpPvDF?COt_7C%H z3ipw~6&5pJEaY1)XtMxWKlDL0COLP%gOXF=@{jHK{44y0Oxl==f)4L2 z2k!^4)D6~dqT6AELBDFbTBitVOh6sAeTy!yv6k+WU{JK!lam5|UdL4$ z#AJp5w09BYLj?a(2m452s)CGiZpqmcen{>GKL4f;Lu@my@<-9iGBhiIo`u;YPjzL| zINaxJIap1op=BG6Rf-$oM(1%p$AXb}8Mt*Yz5{?-^hp9ax-x{(@LfWEtO!OcOwlDeRnfvJ)^K=mQ2LGB1V-)#WPe>-2lo`+JFtAkrnd zgfBOZ^X|omXV|7>Sg|vbKXdHIG)$#EuO{q_C}6BGXWA>{3gbZ#n{zY7(531;Y1EMN5?g&0Cc;Bm%0dv zU+#JDWK*>6E=bMT!3V1H0baVGLj{QKqPvxn>JQ-D{F!VbpPMM8Y!`X#CAJwBb3i|A zagp~)mSDa0g1S!}=NjAkC@>6hjwFD&fOE~=Thnpe&@mZLgZ{`^yiG^SBU!f61lcr6 zwp|;KC=!QGf}VNlE4b^x79m>i2Cm)bX$13hI9$uTdtF{lm@yFfZV81TpWFnJKYt$b z^`_rqW}3U5o!+2M%;;SMSGw+%bV-&z!=v8PP zVhwmCK}HDSp{}=A)8hTwJv@7cM=KbLYU0hE0{cjSy2K1dYz9dv%pun1@FnIFnBAyY z6kirb6FyV3gIE+TQa|T zX7nWz8omPRR1Ic-fKq$pYC@pR#Z^nGGJXJ~s5X~Nl4Qk8vX;j|Y64ItVb|B$Y~w|q zb!mBs?$e$YCSPPNe-3mPzl+)RsCyWy90$$7ai>SQX+nyIWn2Ay=aKvZ^r$!<*Aprf zj(72lQ?iUjUV{b?2Mt_%Y{xGVyl#X(o*_Lo)CD)IqGq~4ME0mzf_Eo{teMNHT+P1w zc=)8|?liLz&f5RbsaFTk_BS8FY)V+MoG<%2yKwYIGrDS5-Wt$V>$jUnZ; zN%#SQ@o9<08UDaHI|H)--$l&wLUh%wsn?xC@LQ_|JS5&mpk>KROL%Ox4+NEDRgY(e zY?k#l@p9@MW7q}eT|7v1Prz{r(6nTh!#9!Ur0bA#*wF34R}w|JVlfd1F6}`0jBTxN0@q7OCX1KI7b#Z3Qz<%Nk7k~)<*TBE zSDVbsp9xz^^Kdqn#5xgyMbWJIz3tfH?OqNK2qsYdu}n_^&Swg{!Pin17vAeasdW}r ziLgP>WvJjfEBX_L{Rq705--ua;^gm)RsYc+wdnIsS9%hRbSrjz`hM(}x0C43hh|0aw7FCIHf*+~-0HbHI~ zkrSEeNdbHuszZYfc}X;p05yp6vxW`DH5Z9e6S*mKBZguFV3!pIJ-g1g4fX7a0rOic zhp$yS-{KiMG#+L(rXwJa4-u@o-ZlDO4qX61E1m{ic2xUQQ>iBil;m72Gk-D8RW}+s zFbh(XDt*%{N=eBo?E-C)2Ki8os_y41dEAa$L&TvVR_v^A9mr(k!nhWmK2ryAC}f@m z4798O{3h3L?gAl5#toOjWsKy7){F6%Iqn_V9CijDsO;=uB}JhmWEMRuca~AnTP6Pq zhs^~VCSW}tlXROvTbRJO*f+c>`d&Q$XLw|jyT*PI_=w1izbMmtKt%b9fvAM-T5{)t zIRzL*=J(+~zUU&6%``Z5#TEcjfBz^(Pv7r+ZX{I!uL?h`nxC1Hb2kAY(dA`v_A!LF z$zN~^N!DJ*{}8L+4WH#-C7}62`I;^W6}+$TyHcwtZ~QDVewip4}Cr$cuAkA!|nP;N_zsq^KeS;onwh85J3SGMBOI;%m3(V4JKl9i26sGUfWxOV_pb46Xq~I6enb!?PtWB z8-VRi!C$4+jBGyZY5l2ld|US&vmMcVN%e(Il~)(fHzit*Y33A>LC`R|Qmn{iYw1o! zzplZWi0tlcAhKF=rCr;1${pU`7bl?0D9;XDk_b3!Wqjv;wQiRh`5It@)tAXXIZEdc zgpN`t?u}OWuY7m8^GzVDvCCI8^2oFCS`fK{DWzjew{CGPe_O>dQ&gJ(StLncUyPNm zwC%W>>K_n9_ciu^90Nc0Kb`{cfs9z~NPs?FDZGI1%iu_e@MID6ZyUWhIblGGn(E_p1;wU7({4aUPF{&*qkFg#6_Ng zxd8ugbk?@;B}PPjv|9S4LTWyvU^jPq_kUPL^4WhA2 z^7RU(y2dBUe@zfa-1Cgxks(JjPG}G^Qf&R$01ebU-}0P=VqHZY_pFE?2u33f3_6AK zX{T(L#;<00wQH-;w+U_u=kc{p#b&7{CI0=KYV)`c z5mp+RHNUuY)bh>DaE34EXp@+1>2W(^o5G6%g$`Vda6R>IutG3Upy zTs=By3+kh*c@`(&K7=$9z_geL*Uuv8v4kshh2jl3gc7b&4rFvVh2EY1^!4J# zRE};_Naxa{Y%kN&ia$cHjN%^CAvhvE%D7J~Q z;O9>2+!HH*EWz?Z^CItTZ;rY127pA)Xw)XZ8hdqmvtI?3OaFQSe4Vc7!(`qML?vi{ zbTtN5<;pmf2B796O+Nt$pN+hA{fhs{O{}%NenE+we^2^l+Q#Eq-Jz~@mqBi=#1UkX zN0S-)vOFHUomok`)l$pIZ%*Cvz4FV{4L-y)EU#i%!^FpaOMt6#l+fkz!}PUzLX@?e zXCV!q%esU_;3|2o5@~6#f0U7am640)YSGt6z6zS6nz(2D;^GVQ8*eZ4O+HHT|30YU zH}KN_{+VYR8b5Q+fjSg(lh#gtKw?NiFhA(%O!ULEer~*uXrtOKaxB*ft((ZPM~DNX z!!f3>2y7G4nQzfz;b>H`Wzsb^^<#2zp+d6m>cQ<}iRNZB=P?>Qai=b;%kXfOPhn+G zsLXckUeflk`)B@1BoyTItt+wq%=xc<&q-yg1&o+A#>;kYtpEP?{lEWY=?=Ja(qprE${!7Xywv+m5*wCyf4mj#W396T9lmAS_(&;H?NB`=Qr< zW4X%}DWMp=J7p;B@WRxfP9Ui)Up4gD#ITZyzr)L@AcZh5zL|E{W5EnAW48nZisuZV~#TZX2DF-hNRm!4V z{FSVJX!4G6Mvb#oyBgB(d;U+^OVh{rV@=jOR1P$w!NOz-`6qhoS+u%Q<^puc=5 zW%aNi*ZPssh`-%QZ;?mURnF`0rrfLy-H3=i>=aY!fV2!p+@7!o;{G4N-+LCy-0ZC1 zBW0;Scaj$pQ1o4>j({}bIa3)Zk{OWOErwYywU;<24-_-nu5f_$a%+ATDrE(a6cC)& zqtM+igRucVYc)ysGE~zO#-=sedXeRay9)Xc%X^NIn)(q`aAQ)C6Dejc2^=lT|Hmr= zk+XXO8e2Z^f;cHD0rhY?jg?2sOnJv6;tE2h6tPqKn(+ctdIPl`8~`^Ha>>>lrf%Q& zerrw)^0D@$eJDmkibwO~x)xDidULEbk{B^*uj`xW%FFL{1GTR>CFDby990}`=nbPTJ8H*EOX$gW<{C|E*3I5aPqV5Fl649+6N)RV>z11Z z1I-RRayz5%ll>u;3;jE%?XwvvJ^y=1U9d_)168Q1Ni16dey?+X*M1xvgLyi|h*BZ(Ouu3M84&aozmgX7~bmoPdZW4_#wiEXgHFH=R z?#Y_psk<~AX(Wk>ez|YiALMxkVpxOLsT{NZ`Z6^Kn6O34L>LL31uWXzVn!nwQR=Z> z99YkwxcM*-J<9S3lJ$Am`3H0K6H1SV-aZjtn!OE*Go4pDuuSwGr=FV zEQE|vE9={N(}m*IJDfx9KCpP)Tc{?-33r*6BTf6ExVd>?Zlf0@&Y73v<_q?$F{+=X z*Lz6Z^9(d>D+rT$zmI1q#B~vnn)i!hVib0g!O$N( zgk>sgr92M0mhO&2ugP7Yj%fXbKlrrg_vBgY7Fn$_qJ4(5)h!|T`svxRibZ(;ay|tT zn*WK4RBD(%gF7&rPq)5fU7iV#*1s)Z$qG&)>^0-bmgz|AO=9<@_SFYyo1U-0m;3jJ zH7M@Vei;6=b&nKh(oXL0n47P4xo1+n*J@Hs+FE0^x{w5e^2S_$4MI)o0`+DkA2<&E zD($Yx&9hkHDS5U?4e79@4@%a0tqDBhnI^Fa(u&sEcjvQy`|6AoT;6kk6k^pjjFZ8S zgk)kEdTApHUwpM4Z3J=v>Y`=~|HHA_;};1djanuM;5*UqW}c)Y+TdXdzV4mjhg{EW zneM4rPee*CD6N*nkcgz8D_WEt9>p7rmSc7_>E6f%k{rw?mnrF z3I$VsNQQo8{|diVDAA*)&3+UYBlj-+>f-<|$zI3}0Hn={YhDALx>alU{oDu5Xg|J| zyP#OT|8iL7cAomz=SXul2w08I*%j(r_-mmBy~QQ9p34RF?Sk`|X0it(;TG!ku`uKw zKM9n}S+9;~F1wkZez03#wCGJ)WZ%wKNo#0Tsjk6aZF?N76Cl2Ks>51G`xQ<`M;szs zj6GxV+-SdUUum2`Op`N$={}`2_(S%L~DSQUP z;!pA~PRv9X6&nrP`(_%AK)s`;o<-`VQfflx(YFxmqvpR)k^PHMe&qjv;yGq!MjL%Z4$e%^jz%7&)Dbh}MY z@7*Xf$X0An`kuL%>@b=YNGz6#C|9}Yd6su=nROfaCkPnc-=I9gl#M^_;_jLZQJGts zGEI7(t-@V30PmGR@SNYr6koI_@FlCM>Az;bOT?%BJ{Ww_e1&DQxy1oepeU%q?mTDi zkLzdt&M2c(e2VTqz(7QjQYnpi^-+;H=*UmBW=zK!Y*oIwaxd+k&_*_*Figoc0JY_J z=1hUaC0x5~qoNuYam*_Rx+|-Vg2W4C_|-6H+M4 zEbBP3B1!-q<||SJyE$~2y3aw5f2j3z`~Ylgs?eSrL2@5fL<5w%#f$POMqftAQ*@vbb~MQ?Z^vHQB4jEounWZXK;-P1*uDjo_suq09*=OJtcWY0 zc`?|}7&pj}kT?w2tCAV3(Rf)*spjMXQ@EK6gOC&SFC*Z9N~W^T@bz_HyXcV~u%CH2 zL=CObJXsV!Gsvi6hz}Ey!+f!DhG7^1S-+H%#Ya;7MuaZz| zUC7gp&V9nOq47>Kzr_e;B#C^In*ZS6b+^~W6uTsr+XC6+hx7iWC*P>^Pj@n{w-NF` z1Jp!y0u`#jju1?tdaZA=iiJZZ%Lm%{XaCI6d)O+D8P4e(XTZP+j?UDu@S(Wp#8s*t z2GErjMse`{b|o*y;z*tKY!IK3agH26qa-Ia5vrbjheEm%Px8I)sy#C0Z0oGJKIFBn zQ<(<+H{*Oe5-1xcf$w*sWRz2*9T2vY(khQHg1ris9=Or!|1B667CRK|i+Yg>##10L zK0hFGJPDze#2&hvJ-yG7sETk@&3Ei>qda|2){=Mhr3GEmP;vw_Taehwg;lwkrkN4EW9IFi7<)ec3dpmw(Vx2=*Q>N)L2Z z^9LG4H>>Brcr&vI5PxqsNkj0E`j`ds8+?h4e;_=cHeBlWGQYNxX)UuK(IqXm#8*k> zYr)y^i%PC4*~$wJ#{G36bIz%O7p&h<6=!j2j*5D1Z?xDu$3a3mqkK-#yz2=R_LWLm zncL~sj4eOD!1V&1b>7${p@RXf=g6BVH;QB)SY1A_KX;V!$X>)28*p^Op^%a%UGHZM zBbUVvr+N}z`^a#_?x2-ag6Te0gYhoq2kDXBhUdUwGK~51mJc3%L_ClJkd9!=^kaRH zK>0UK2numefU08QJxK9rQ{k{jONkzJ$u5j+pGs8frQ20OkIvR|c#+}D6y$}o*7$qo zEf>UFvORrj&ov``WI8)t1-sonD4hFeODL^d4i#x_DqM*2%pn9)-XMG7!HQllvT^S? z3AQ{N;7<(;IE3GLP7QW@=0hpC{~vCKY)~_(H9TZsu{@SfxxjfXxiysL1Xd2Fr3kp5Fz0Qz7Y=+u!m>L&IDE&h!E1>2e? z*PjmPS*pRX+FuVBV7NfN5+RKu^&Xb#M+7UxB1$s6N%X>VUs&OpQWs+Hm+HlVIV19H zR%t{8DOv-~Mpm+2@4f&UP;!AKgRAKikZG87+^BZ$lha2yd2bMbQ$&e6>fxt50yDon z{DOMYORrBf|9BFvijy$`q$)Px6sy=%@Dpu{m|g zG7unR*bd*26P>qV_cmhXBXYeC#31L55EXUa1|gSLbKgd|w?13d)r)3*NNJo;lhON` zXaB?ncT2@F>|J%n_?e`dVLTl%siC7JP}2lI^v-Lqp^*S&1ixf%I%n6;CVZIqZXuBi z@-;sJ;LQ_}8L!g~OI%Zl`*(gEnwQBFBZdK+$VzvCF=5Ii_1s(M8!=v+of)m zo4FU4S^MDG7n{1#a(!e`X4x)4=qDsY8O?gIS#-cv;8Qn=TLPd)bE18R|HFpNUDUY- zkXy+ZKHK;4ZQn;cw*W*yg=c@dftwBGk?i{~#fCquh}I?LxRA1sk~n#KC_x?P-Sa5} zNA}wk5VNjsr1g#v((v{u3i$PPGG$Wtj|1V~XTn3Xs}W;^(ERvzKphfg+8vn3w|x~nJ;dlO`J#l!4xt>8ADPWQjI z-(>vbySyiw#cg}^WTVFS!`K^{{&+KDRKBcFf28nw|LNu~nqu~s?pMKv=s=nC8ZrlQ zB?2qUWuv`@u$f(bx3f(O-+BG=^xA*kQR_cmM=IF+g>>L>1kq`u#6c%rhKBB#lbn*j zrQo)93$A#LabPFu)?Pl`Xbc)N4Q+87^Nw3L#q#hHdv}=&-G@k#qhg zedo%=#daWYe7xa$^8h@B?-VeE6DGA-W$LI7?sB+~zvT??IFd*4?>Z|&_j?9(za;w}VT-nH5DXZ??G5kl7dkWOm6OeDpAgp1gtR(fdvbO!P-S=PSFx zb6g>Bl9!qZ>BP&kiw9P6=ck!R=?B1J) z*&lYH{l-VP2$JJ@IpYs0HNk$-{iNK4s^dJ`shHCw9mmu#@qK81yon`Bb8&1*TQ)7? ze!rtz^e>)}62A^r{qs%!o|M^o_HmxE#a3ujWNr=Z z?S5n%&j52}zb!czxJjyR9n*w6T#P8owv>|x6uueim zN+6a}ZDs034uUs%>g)UIx_-rXYhmjD`F>8h;J^QjJN{_yLrc5e|Gbr$n{nt~@jT9T zw@Y*i$@g#;o*jX?hTM@OL4MI}oJc#8QPx=H=lziRUlf5*y;soM`dKiv1U2T(4b z5uKIUtp0q!UNuUG>P@3S)T{#%-=hP{ka|{zGpW&!UAcUg#9!0=m2a*Scu{afWPw@C zguMj{e7KRw*k^5eYuUi;vhZG3%32wwacAbUNW~?9^jmlFewZdMt-sZg(W0KxQfaoK zJApF0S~x5e7+soyg_`;5RDC((r-ZOQ70q8fFs8nhGB94osZ@kGHKK5SGhr|*$?Zs; z<|ohIu^T&hK4^!ceIuuK|FhW|Tn~~G2_+S49he#>wZS4h_hxl*wq!t5%XRtpRf_Qe zI>Obr&BBKlBQ}eRyoXDW#$myC<_8}R;*|ovpR&pfZ?ivc_Vc{~s1T>zb!21v_GND; zhb&P;*Tx6EOEP;GpPj!f>iJ+VH9MG#JJ>3jHMyaCDL^>Yz;fHhytip_`t!lqYI{(6 z@7b9)W^*A{d!(Uz+h>RDWtflV`1sz?$w6(#yZ$#43+mD7f-j(*c_B!_yY0*4c0Lt5J z&+(snj)o9L8n@6bD#azYU55r_=-;a8%h!KXIb<&Sj{Iy`Es6X+rat#`RhOB0t$ZYc&I_%H3976SR#S$gvp@BJn0E$_hEm|s4$oWLu3Q3Yag)#ic3OW53k&T!NuEdAPkH!ui5Pc}=lg1JsR*-oqd zqtyQ2+tat*2;5VKguDl_u~)F7t{A84n*VUY-ZzR-^wovF>kD~S0snaUmoct_nxDw z{hk@KVlzuK6C}f%QYeZps5^O1XBqDVdjVd+1Jb3yJ_xwUPFPlF2Qr}Ji!Y3Rr-+i; z^{~ze{_lh%+VEg@W*QkEO@P~d@GH!lGQVh8=ApPv8xIQWbJonKYY@xiyq|coO~%K( z7tlGj3-h=z8qt1%5AuNwY63G|(9pp8f|N!>-0&2 z_Wj|XBUWk6s77yhpp*6C-Lb>ra5p46?`xqCSm8@MwWssa!XKYb`sNt^V#^JpMY;IN zHjstMyxfang`T{s1)Tx!Li^!&S|=<$Y~LoArftq=uWRJQ0$ujm?GY1jbSXIw6uYWn;{tcMQd ziy??EqZ3YD&9j=EY=6#JJcL!d?|5Te((BHWTg&sg9V~zV=CpEo>4zL=xB+TL7{vVq zI-UJ3W~+AZ3Q>Eb+()6!QW=ySJyxV>MCI<4J;(IvNEE*%43xfp)FF{CdIA}%0@=#! zsT_Fd)RZ>C?JC*0E}G|2)pXEpTPzF66xh{#ZW#Pg}z8wOn9YfN8|vUj5_I3e^qMF%J4o z0k}-X`sc|6*fvj2i7+RhVOVgo&EjaikggWJ1h_S`?>_p-d;WDA(Q02g_Et91;IFu6 zQ>~0-W$n;j`5G}5!n??W68}!OI9|Z}d@p$u{H@eW&!ghN;CY9FA2{gRiqqE%@Gyo& z?4AN`yBPP{k&=UyD+WHH%9sAGb~o{x4A!aVGQnNnN4(x`;t22XqvTC?qP^-H?uOa3~g0K01im(~L1J=hK& z9NVTDxX;UBiPA}W5>fM0ph)!F@#3GKU6<$B0D(eiGcTA3HLmw`V%%kSKUl_q$Q|U2 z9?Z$3cU}S_ECk~Zx(Ak++?J188Hr`8E+&C8L3a#exvkLL@!2`?YoAXyuC|1#^?$!5#~@Xl1M z7V+rz;FJY@(I_nHj?%c6`(R7c+|kWru-NEfl;KO&Wv#D_mkVfap*tj2JC-o4zT*Dy zStH)2)IuK!Z*jDUdpCsb5yKKaCV|NHGBqLtlGY|`Oro%9;#l~Z*whLdWkwT58b=zEykfI0rV>)w~O+0v_lCvp$_cSPI$RpFOftxPD8B zG&F!rVYQmjme=ZCQpc`1k=6?D+Vd67>VO8{J|oQeyD0PP@jc`$j$&@PJ} z(ynWpTH;VY!DD^D*f2q-K=rWCQw1|OKexw(T1 z+kp8y-)(Ae_aYh~(>a8X54XAFT9j`GLGk6kkCZVzHdVzo%X=T-b#=)Ui3eQ8pQPop z^t%?O6|lpimnkv+%RtQ3`klmHF*fmPNiS3tigX3CZ;4?pOcEzaz@=uaN8@xb7fD&y zy1cM2g%9mJY7kF^P$f9}jk_0Hik>ZK5m*i<-aEwiAXhwLxsPE>W{oJCls-n8qSuFN zGP8`-dI0veYbjO*d2-t&b7_#KBGZJYfjtQZDfPXR_`PvGJwKm#YOA8s`N>T-5L<3Rm zo+u*^mfUe}K@{ov=E}xe{GrMCyV(t9SC+MPqSqJ45A?E@42Lx1`d5F}i(O?4XJ}zB z;wn^o#O8|Gl5J$vgx~#WLV%&k<28%p^Tqj1^(RLanrP}*#aA;*9Jh3qR7?Ig?R3*B7~i++ z$7hGk{Eurjt>YjH>|3%SDt!M_FwVx?QVvdOnWU-N)6-W>p7RNc>bxocK%|bPL61JP zxW{5sPB-vVW+;4HXPeH_Mq^! zRl`93!mqCG{=Rk>Ddy^AcmBjNEsOcCC8dDfAdhoJU zTW{#Bzm2eM8l?eS_YihCyWm75-*#u0M zps5?A;xDDeeTg^nZR6d#F^T{ldz8wLo4Ra5w zF|`V=It6#@@= z$Rroh)gYacZ%8*^(iMXlqz4_?ac1#SGq>PL^yi(gMax)3_1?wHbFwj?s-hUOm#>||a11-?4u9*l8z)A*2YNalt=LxKeglE} z^#OMK?Q$A+`~Pcw9B>3h({aH-YyYYHrgk@^Ay9}`?%-Q|fUq#;T0HE|DQ&5u?4-Hn zA0)GH4csWdu6^<-meSrox0ict1o4Gb`f|#}XZL@Gu!h#qGIx^I{xqb2izO;vTG#bv zOAhc%hkQ`>HCTH}iKCIpt=^g!Pg3~GXP|Sgk)g}+kw#^9=C#Ief2~oy$grXY59Pgv z+Lk9OivtT@ZMy+kIfgl)zW^X|0pPw-FIw|etup#&nDPSd!HD<%Z1c9Q-uocz(LK@f zybSN0?PC+iJwCxb2W@WC0)DTC{C8tjezvpeQOhDtY@mqkF3_U%Evbt>dzi9mQg`69_n*Y&?H_7fT+xE1sty&9 zpo4n{0Or9nFMR0ifeSd*vx=U)JwY3e;opY#=00vPn*g1ilfw@_vj9HEs#n+Uy8HR{n}Vj;6m%1+ z2)7IVlehQk(%ZyR9~YYj+ASk?u7;>I)ZFAG;$!WoAUWq(sNzYa%^l*Drp5H6*0S8x ztUn&Lu)go=r^T*SiL$!FZ7NB8z}@~kQ?A#+>bv^%^o^TKxygY|cNeiHpg)0=jrwUR z$T&Kr@civp*S#rvt}o(Ur2Lkc?@e0xvvr9L+|@60wPd9ZBzaSlGI9YGHqY1AN}1DG zq{%GPpiSHecZ{~eN}y{_4=QY0kE7@sd32PwJGB18lZVCq4Gnb<)6<^z^fxay#9zfe z*?X$RLI(|b2WMU;_O{CiXWFoA+nNJ*$}T?g2Ia4uV*CQaEkxy{-v%8sbgewYD3?Zy zvqT7YK(>otCoiRosKyoE6upeSu)|7~v7w(!80-1Rxm`lB)ik@*<(B13;((0SD`(Ox zVA<|$>{s>Hn2HzgkHx4--RW6lJj9`29~cx{s_)P~Q_F8Fh|;!P+RFZ>vGw_aOwwgD zGF9Q$_J4dQRi8Wad9tnogZJaZ?uSi)G&t~Ee_CG;62B9S7B%6&+7k>!--jowl{A!eJp>d z|982UrhtF$z*>_r?jkz~NOhv4wJ(Ey{C5ZD^z+BTW~upReM22o>W!p%htt*zE0d8q z?1$NgNM5@Mtt~zx>Wd9E(wm`{ zJUKh{x*$c#KSNI7pEE-cHQVsYGO*#yw6gaWV`oSyaRJNqg<6iHWCPicN}a0L8(E;g zk~w0q)utR4U0z)$JWI4t?f^F9nHK83dG7~4MJG5xR4&$FS%`#H#j`FvJwE+t0HfT? z;iqBHEJ=A>v>HV=i%&@C>OZEODqZCBa{kv0quZB;GRrfgoNjo9=h<_FQ;r=>KfD~B)@*@4`sXtd?vh!U~xDfc^!V{pH;}pCi z@CGBwRd1)AnzMgVeOMNc$oCn+A)YxGX1}N$14#$)Iy=}~IvaA*;f|83DI)!uLf1Z5 zI759~XpNDs%Wa=J$_6&%h^vIJmv)(Eq!t|7!$UP*YF`9YrBqvlyO#dhr1CgAx^a=- zlHxJ-ASeI#8lEzE+QXXx+s6?Pq~=F7L7`0@1b*hT1bI(xWw|mwc41XcIndCjVArYp zRZ0V)0Id9KG#nIq@l?_Q<%STByAAw*Z0kP3wp0 z?q$SA0yjhlQ>dZe#M`otwu&#fsN^ny9`(k>?o?s*K1JzR0Y-EvQm>9|vN~A)7`Vx) zr*+$_+TLxtI7I){&7=Ts)00IrR{1!|Sb{>_#2ZyvtF5blm-B0iGe3jxX=bK<1_=>rar26v8*j+W_TXg7*r^}H8GmyR4NLIy)sg(Rp}MwFhr&rS^# zc<{3|_3`%f1?)~{Qy%3UFtJby?FHeR(!9KF4;87;kdCByS8pptIf_ss2Jqd~%ixl} zR^QvMI9o&MH;)=D`_Op=iFO&;s4}^uoB1+TT%cI2;_j+zi_k3U^ggA3;Oe(PyJJ-% zFZC0evFCIQPIAb&mN>!>oh3Y|a4PsyV6_<8OzRJ^pz|dWYM@wBy{$3{onck(zW7Pc zI$g<|Xxf#vzWmS502bo1H-!CTOc#P>6Km4AUsM?4?512&SAH+_zsp&tmY!&oqyYB5 zWf)n?0SwBXI{81c&m!gGEv+9uaZkSS3PrNX02a6?E+Qw?V_ikoJ|9Tj_0rchATtX| z_h;^Xr8*nB%KdKc`U*_$q3}iMcXCDC2BY5P#e8@p@|DwJ=QhEVkycgQ-=L^vx7h#Zs|sM+#K!Vjd@h6RAk*8w807xnV*8KI_`x z@Sg9blb}Tnw`-V7DUoL23-$XVduIuvc`3aoRM=0Rh-2-#R*`AZ1I3$uK8pVS;4M$b zD22m7#d+scC`Jclq!fab6wjV-bB4td#FWp{=>~IrB{L9IY;t$wy?%1|`d}l~cw!pw zmOk<9uh@~ONRB+~*j@?7WXRgvu9&UH(*ovEC5+;^m$WBnwkun{JH3qA4vKq1)`KmvZg09uJ z)rzwsZfzT2h?y7n+wdo|y2u8l1?-NMLD0WbMyAJfDp?&wcjmr(}a(o&8)5a?-ZqQ&y@z&CMGdu z?$!CH@+#>D!~`zN(EZo>XR3-#N?2uYXmR*PHAUt15!! z10Am4i%tUiNP!J8npqsD9j{KK7qLi&>z#KPewKpwOaGeMv-i>{y*|z0LTY^Gvt-jz z5Tq{O@O0PUN4U$_jukR~vUOv9cuUk#X+ISdtZE3jp#Sfo0iCvY(!+_Zpyo4Zbbz8k zk^0a1r;_aAd8rqz#Q#;HIi!J z-Owqucb8(m5PL}w8BP#K2&fsw6k;sIM$8TB=M3w!A1z1c#7Dg{*V`08YhAxzIx__E zXnX8)cp1QczGhQJb)d_Ps~qj1L3fCh*4_aj{wDE}JNHM~<$gKoYxE@_Xg~#Z|A1W9 zlx6K(mrNhbPwm6R7&yq+3i>#ZWSM};cI@%fKj9}X)YihBwMtA@+N{?X$4Xe5Lg;e} zr_UmkNKh2UU?LKll_Vz+UET9*MdF&W_SW2~RVOt_Jyjw*!s*oUR03cSZF5ok-69PEV z$0B(7Ae>?%oglvgQ_ns9JWxO0N1qq0Us)qo>XUhJN;|lmowz&Gj7vQ0li+TFw+m37 zw2*J~R?0}hoDu0*1#x792#_XD+=Vu_ffL>){VMnIzy3hO%0=ky%vI1fw*9 zr+zmQ&6A9L{yTh&2bQF0)8EUVP1gSF4K^i`{`@v9Y=T!~Z0@bfu~GY_1o>R9xTLod zgC^}rlWu~0)l;C?IT>GqizjL-rqkdSzDn{mFhoAnydm4TK0W{~>j?MyQ6_74&MK56 zW-;d^Q6?4;V!$?qMDq}#9BI0E@DYymqq!;z9wqBlpJK!6+;zK#wos6uVliMO@%97ffpCcZOe`2wZW+o4%}$|oc;Fz> z4xZ9aoYDd}by1ouR|@!8c&*wpj4z&(U)yf|55AYG&|kU;SKRWh3OJ{ndK@VOC@M@< zcH~gK)8XDOB^@gG{NJg0=cGgD3e)7R>eniI%fx7Uh4g@gYkF+jLK&#x`~*tOjCWL# zdHyY`E(;4+Oo6k1mq!4sT^-Nw4pc_)bgNCz&%0DzX|I?FuKI`*lQ4xu&|u9u5Pso3`I{EEh(b62p zDl^a}syS+GCgzXIrMHrm(!jI8LO8m;6!%eFM1X~I&;OTh_u?d+AXOnLn$EY?wlP_= zJYZgS;4!3^$5>6jRPS4*RV{YSr393GkaP}hL1-o%zGOKF_z7>4Shdu)IS}2>#uTUt z=f$x^NL3tehe2tLE{ixP4WK(8fs4n(#ivKlUwhu{dCKGQztj|dnaCS_kN~-wq%MIu zpI#*i+oX4YJ|U$lP>+U2)gi0m}3ZRPpNmnwrLM5&mgZjXYL^(N~ zg784a#pJ*c9Y4<|_{0e~(aq4R7bgP`d5u-7MN6D4lYad;dvBB3(XmriBNt8zAoDTm zw`0;0ShmD^M(b+lrr2*uF)^5uH?mzOtWg3t<#j9y^X zCwa+-=Iyl;3U=#6(WCv_g@h1)>ocYS;iu6s3_83M=_-t+rzjE1q=QM$M$yoBzf2lb z#J12HhmNCfx5C*C`mGjsf23$~(ZEHz$=pT0w$!iY)o0W#he@&?+_gL3;7vr(@i z6G&>lrq-pEfup{rKbW@8WdmEQa3}QM00Q)G89}FCX+TMu{L~;OIKhcvbLJiO2wX4E z^q9!oGNcl2ynwv83v&jDv|O-#mTJWitqj>`*av<{NaQx7(@<&*^FK|eLba9FA^9@N zDkc9Y?~c`M94K#`Vhz_5%JLAG=N0tdDx0VViYMo(PU1O{(DxW|F2Rv*6&pX~8rmoo z$s$gc$+rNYwR17-TC_Y(qNEJJD}UdvMu)xrO3f=0=Odxf@2z*xu~$(j@o_?T61rVJ zp_+R`aBuvAEm;qdS5U29FdeD1X43eq*LoUK4Nwv2;x2Xxt`qg22Ve4B-N7!xkDB5b zD+W)TVs&+fB%c6JV^gS+a#e^s7P~TMTSxuA8(kUE(fw{B9g@J8*7k_>^RMRb0$J|# zu?+*%zVE$0pnuUD15xF}r9_~+U3ud_xxzEkk(PPz>gf@WgT%ipg56h{2WiO}2@f<@ zlyAowNn%h3uVj4lKcqHacUMobX<8%CU%AtH{M_Qrc%U6K*Y|6r=lcqD|8dvsL^!n5 z{@ygf-yOH5Ag$5K+&=_0QLB8FupeG_wLx3rjX-=f6|rxRRo_pn(+PwH&tsK`;xep+ z;(#l1evEv%mKoN{ku5If^qVFm?)m9zVacG@?@S z0m=P2c6%fQiFh0?iUsowvL43yQKz%rzBoO*1xGc+^fXdc%H!W8+_PvFfQ#Ww01&KDDFFXihp1B|NY5N9ilxLM+l7)eLJxv9XmWpv zP&6Z`eA{(P)o_ibJ8R{22*UN%N?tyQg=Cnrfqp80L_<~zlCzu}l!fGk^4 z{*m&S0JNqe&Fa;CV{lI;|@EkaW1tz3w?M)Yd_z%-JbqWslDJTU|R3vd~2b+(e=M_JIoGkH^Oze!_g4-=wb_oHpG$?%>ZF7z53vj+V&Vp^Wj`$zhycRT7xH8T3RV|#;f zV3YWMCZO@k13x>JNOJez!j@knkDn>czsSXed_X>0WIl&`Xr9p(908xCEqu;r?!LP33?nq?@Ubk8Y=@EdhK|~L zqsiH@4=m5}BZM;MWbDgwN54jLBH`5M3&wX((@QGhgl4K4hhM4sXEPlCZTtW!;a_3_b6`HP4< z$K^!?abGILJ!P`^f*vJV?{@0kUT1@&-%n3Y8y}i@qw?Nqa(aIY(NqqjYrUzk&D{;z z7bsc36#Zbur$viF%wQH!9kr)t!cf6R--5IuU!Si$4Gg&}F`OX8%RPmMXNkBQM0qtP4_$G>^4$B22Vd0+}G83w%U2%pTJ7JgUNS_`jEk| zv?kJuy6(b14!)?9z-Jl4TOTDy@pXPS+}nS<`8<`6&s_tab(e_QY6$l-)||^g|89%ns#=OH1{lG-HG#X-+51I2IV>iC7VHrkD*s3JDYI&Vf>c^(~O>PeIKrrPJ5lYbP@a-ktg~XZ{)}GD!AT-R0U(RoXnzQhPKm zpFjzl!#age7kQdz|Gb)mbgu9FeU32riJ@NGI=iPqkT1V{KE`v0S^A=_L_pgfBWX36 ztA$s?@OfEOwFJ98ovFQI;x>{ldcAWp!`HI-@>B+DueODR=w(~OUSpMV6Sxwa=TIUU z%#p)5SI-oBQQZ7s_sZe#E8G@dGO5fKwnxIq84%(r^0+aHSfp>aKL8^R!oV?p`g`Mp z)5F+veb$bBhx*Uu?rUqm5LH05%*9{v)Kd*^YcMn7rR>%dS)DYa0!{N@KDRi%5SLRt zA4~^#{ZSu`4^)aX4J48e}(v|2R83LAg0l6|N7Owm+_YN2z0e>(0>^> z672jD?84lCc_R3F7_;*`H(r!H%}tK23v9|bGU$WpmzVZovQsZ~LA8-{_Y8h5I_ZyQ zyH9sTJlw7)ojzW9YoYsd@uA zei!$WaqW?DWebtLuf1o8Qa`IGk{LzZg)5Z3vR6W~Bht0EY}b~I8$veW{yfk5zz1N##e60HQg!XIwNaGS@}TIus1jz1eJYVj>1S9ZS8S27!LlquOs+7xlwLM`vs!*5DN3s^F0UswDf zkYN49vOhhl>a9&ymaY&RZ7sf_BC@J>YunR!%le+(e$S5RQ+xdf@!6@-(wueQ}=8HGbS6)3 zZ#SD&U7h1@NUa6TAGkW7#b0B+;(YJ^9d|rSzn<%35$sF?%qTYKVRevB*W!Psyy~V@ zGAq6RTa~(3XrmCwq$Lthp@=x7mGa4@PGz7vTHuK^NBxz6tddgP zfY>peJ`MmuL8800Zpt{m+0;2!mjd2A? zMFDA0y@_d%_(+;Z!V30%cOM%kW@WNV+~;`zz&0k8p>0Ld4>RE0wdbOkw<`O~6Obuo z5ZyZWSovm053`+*OW_oi{W;dI8;?m6$-z`8m;<**gcfflFWP~ zB>Ge)=a!n9?(>f~=*9q{+lKO$b#7ODNQm1!6o?|M;R_wAY2od5u> zyU&+SH80=P*|~MSz^@bEyk2~no!^nSr(IHe{%UG=;kYZXV& z)8d=*rTJGr3e*2c-7mxC4l}~QcQByEFPr`;tpVxfaJagEZ%HeawD8wzOQ65gO~v2l zk!NMk9k+P(=Qr*`114V1ql6siH~%BcQj`yF3N2~UI_d|{Opse%)PI@(^E@(mZdE<- z>(u;qV0ZAsVM}1k@%&C0G-QcF#6J?!#$kxYgX0L%oS00AURxMGqa z)&vSy0;N9ziX*_12vk>15VeGB9R!+j0__Tc?tnlKX(hv%TNwme8D(3UbXu9MTUlIN zS^ZntlA7??R*n!R+kREfj#kOu=IaBkJO{135Ur=iDC(0)YAG!UGliT5Hu{>@#~BcA z38uvVSa+sJi&pjvEs}C7owKrK(T}(D(nPj+Jg%t zLLea+;S;C}5i94aEx*?m)&mjgffyl?E>dWK0V)iRQnS#IBVyI?5Ovpj1yvYu2?%V^ zl7j1KNfr^%|KJbA*tlYR*eL}=-`%Coy; zfF~4sC1^vGOcGdE!5b{+&>6;rf&_t#h!7(b=q{ID-I$JvEafYlj>UPTT8M%5EY@WX zA|e$*?T*$@>T-3FO=6;OA))~%DB;{rhs5rXt$Ym?pgD89ISRys(26rragoBa5<&d3 zZQxw3D}3E{6Q^c?N-o5@n&?Q>s)#J%lmT@Hc3spbDBli4*#QH2tKP6&10WG+YS8h4 zS=q-GM(GE>UVM69tbHux<`#PQ(I(w7-<=3bUg!XJt)I06cgc zFsRKTxXZG(*W(T31~cHcxPCwkE`D5z#{nj1fC4B2Tu`7Fs=eb}fa@n(940iwwNi54 z+Q=Af5xfD`y|?~OC1$o(4Qccusjt6Q&eBl_sEAg;=}TIH^Ez~-9gKl?C~myYw5-ym z&Knt7VCWonJ!a^Y4i3|BF?%IJc00=Ch%4jS`OwtvkA_@m^#j-FSMKpvRr)D=C76b-9i&5Ae zzVbhQq3s-0r5VQds8a7*(3|2FM0z21DE%cM4*^qLsFimPP)3?!9W4q`m~DTfLK#?v zfE24l8~dk`F#JgOfcE+?Oqjcsq`MhYZksYYj`~4|K@e8RrcP*ZRM50f6W#|WXcv_8 zu*`$46#>8?h_c-ssYCM*E0}>Xt1i;05=(!`F#8QP(li0mxm4QId<)u`EoQ8Kv{h&^j0_ zk-%f>iH0Z|wjFfG$^OvfFcX1WyGTvgncT^DsJu6*pwA3Y=)qW-;LV65WFwk^)@D;h zT)VPKZ@VXr3k<^@)UNB==6X@87H2ibWnx0pp>NRfBS!r|743Eb74dGjwz0vH%y=p- zObT(zQV|eE^o9d3uoB24qOB~TlZ%@Q16X!iw55zszGRxDzKR18v^z|u3M{~d)*U8x z#&2X{Mh$~kFItdm$e(GQH9kyT1FUuExBAp$=g=D~p>A4^GKT|<*dfslsH+kfCBg3Q zbBJS{cCIR#dRRL-IWgN>N11&lfY)=$A#L`IE*GzFH)_}4AD&% z7+W{dZVBl)R6=>!;7x;ulTGNp#WMY3Ms+`$`|lct8V^aTy^DUN)u4l~G1hVWWB?>h zSry?r9?_^X-OJU%7`*PoW|c~uXeqnizZB4kUNJ5YX)z$8190>HkNV@vrvZy4 zt(O%^?O|~P<_40JsRbXyHZRE_PEg>A9Ocdr0ngxr5@Vndt* zAJ1VE{@h)EU@YQ}R)E~Yis;!}cjR(SoyZB_5Vvu=;&a#m=9vqht=bGa78{sQUVmnk z3vp^hpn|s?Yi9@b1w`w>%%pj?@Dq>RlCf@44_UcTTd@!eVRz^m?x1%EJa#ZsQqtB{ z1xC;~_RV2uto!|SC*+tZ_-yb7l~Yub_B-M2bP;#|Gok688$dSq_&w55F_qNU(bLV+ z#^w^KE+HQ5cO5&6S!}Q7A?F5_`MQ|)#1O6jDo>*%_YT8LKkKqc<;JzRDx*D3y^G859C$TgtMi~yHanbe7U^bhe#4k%SU>bVEqpT705vg?_%I=Wt=riI$WS=zTMzybuF z`)fB^{tUfhUeKt2@GZL2Z*V14&Zv%Cj`B`h>ocGZ@zLCh(SKL2kPy&g@ud|-l{M~1 zI-x|j7c?%4r~uE00kZmvmf%-&z{guNNe3=rX0Vq+a+Ec9T`U$yI+to}V0S4ylpz}h zD9lRo6R%ZtZOA12QYXEb2cd@Fm$+ZwNnGY$jVnjp@?W?r)39JhJ$q;;5U2Zg&b@AI zz|L&tIqT1=`l@81J8G(ExS|vJNDU@ku5p0Hx@z|bj@@WN$XRXtP?07{3@Y+ zSZ?v3>9dsNaeB(VO|M zz1u79o;2d*ulB~=PA|ag+RVYPV6)!fRc%im8mADGi-;`~sfg^uUH-duG9f0u#Xdm+ zJ*4!VY&D07t4R^B3+17)w9wbpfoa7!JxRW$jobQK*EVHq7qsAR$=-Q=htVN)f?|8d zHDub{eXqG_zrjyOG`3yt$2yd@mx_(ZQ1PqvVUDxFSoS9rurgx2b}*sNUuo;!PJIvR z(MG4KZ?qZAg&l`?=!ku*i&fl0*81S^V`Hcp2s=ug=7IEqxrw-=yY&w5u@5L3BJsgP zOUF3X)SA!i4>^qBLBBm^pogaj8}8+a8HgEQv|Yk?XzS>w54U*fo-aG=8C(4E_vO0d z@ncAx&E!r|&v68XdQim@K7$m{dA|0egvG8$YRFf0qWaInINHYrTst~~E;a zw)8Gb0q0XuI}ei~J!S^a=1kp=S+y}!_g=(23^Oz2aeo+7v^kqOA^e@rMu#@<(6Rm# z*yQt55sM8ow~o8dh>D_yOu5+5i)BmRfC{b+QgM@js_}o9lW8&$6lq`07U$x&HmH%9 z@S?>W!BeuN{cqtDfKHn3^GA~+9SgoG?QiCS-)?OTtBxG3rkU-ahiKB$g7DTvEX^8>3k?`(w_tU^CIw^^CFkan-&@=hGo>?XfP8Z=-0BO7)4@F;81K9FdJ+mHH_x z`;VC9J9%kKN_vXUS#no9Xz&=l8}?TdP8P{*lPkhLI`1%JCB{ArhzkK#K16AUEdv4Fm~~z-T?a*3 zI-SQ5RFce8sZVkwc-Z*%YTw^|uNn^?f-yj~U#`L)cfd;t5lV&Cw)}x}B?lWd8 zM)?N*DSdt;t0K}coLkI&y#m|a|;e+gjLOa^if zhnBkLnq`QsrsgP+N*tM}1)7jXQEl}KO}hW1m~^~Gnx&&FYohf9^^}!D7en=gqpaUI|d)$~|gytm%(yJd-DaWYoxx5s%T|54pSj($z|#5^<4 z{or4^OqM0gX(XM~d-AlVyr(H>*kCW9yK)4OE*Yt^v-;Vwj&$uNkvVE{9`sn0!_s9{ z)EF0nF^dGL*?TM#F-$KdcYn%ItTG&9-^q@cnKyzgsW62D7|f-XLZ$4`Y68Xp+N~k> zr`_-m(j5_KCWLWFAjb@?AY+mX_by4Q#p0r!66iX$Kg`xZo}8ee79MuC7I*KG$(N4R zbH%%$psI^i761ZP%OOaST`rU0;%5~#Kti-2kUswXr@Y)kDLLylj==cJc_Xd}ZXlPg z>`@*+0npR&@l!Mu43LOmdjCwKTirRf#1^mRf3iyHhrx03a?8Wa;Or zG}wxCdM*%6D+Nle_WJy(N{c2>-l5-R-~pqp=VAXCZf6FN(QBkT4Afz(GrWRA0A(;O!)^F1`IXt ztGmC61^wlC@Be!f5a|A&WBo9OY?H)yUlGM=G|kQ7tM~R$TLf^Q<2sq5ma$K}teX07M~^Pu#d1w3!OEuu7^R0h|C%rbxS zFTEVgzf zI8j^vuZO$FUSY}gzS_n_a;4RSmYVXw-ll030Kj(&p*FpIoxlay2vatp9yP{CD~=qoeF|vzM9w<;Gg}kA;EkkNEFH=Tp@c1H-=c9Y@)P zH8U%1YX|1&^7@Rz{QPox+mN-%f#v+niqM#p?DC_&!sY6ujkT@Ip}DT3^UIBtY0wU*|gwt+X*9Y-_68+On|v$pc=issAZ zt;_9^l*j&WyH+lb2D*mk&i4)m65p-m*Df3!yd9m}7%g0@s2yF~T29D3o1A;|DrVzb z{Mz36#$(Sn`L&}JNkb|A5ideg*0#<_;|s+m*>96B7JnSI4QRdTNKsauX&b-TTg^@= ze5&p`lwxwZ(s8-;E4#kdwz|2cxnq0p;Otk+!rqqci^3OubC=8I3qu`VGjo$|1C!ea z#Ty69n3%6$J4Uw;X14#eb*}X|A3)lCx)>qF&{%OeeRq>ONwX^SBg{?!|*YUIE zmEsJ~4KdHIreo_Q`JFKyxjDq=z_PAdGg5r;W-Is|!0U3e9{S?GmnF zZ`^hlE)7o_sNPL$!)+hnn5_{!h4OP2)?5Qy2ZfwT=L5qlN8|ShsFK|d zQdbY5uY3b9QeIwCXi-Q~EFA4C@2D=!96i8U6z=YkmV1WJh{J0$xGwz4!7@SBfJmw? zoa`VFOL{!5a=8-hE+>bHyIZQ`vK_UAz9rn*a7o&33<>UVIn>bH+(1l88aO-I zAa=!6R`hLEHZLu;4c~L{Cdtor1=Su64Q!v(7ME|SvJPC17VhnxU(SrTwRBw044e&h zTn-hswG5o~UEP#lj$U20ZC^VsN1ONd4%)sB0000O89=7uGN{aL#lo1x9C|AA+9Too zMwtdx`JK`1RCJ>yyOv$=5ZZ6J466%!6GiTnI`mc-^`}a?^ko{>6c4^v^xLo&_2O^U zg`z?8hP5Rl1qO*?5Bh39j(xBwFv>ETDB=$yvr1sJ(V&<-5Svo|Rp4Qw#(k{xL4WNPPM7ul^kK?=2Vq z+7f@LJLDPc>;G25bO!AHbX!8hgi4k$G|M`(jPw?W$r~`l5}u#6*6|*e>jmj&Bq9Qj zpfHaC;V5(!_i&K)R%sYDOA-hSzvC&g9ILoi^6RZ?9}Dik-aL!6E1UuW0P!#gp(xbk zf3GB3=!puN+WELnCfPmyxSHbd;`eH*V}$5hnrq6FwRHF5k82rMv&`Ci?=I2xO#jIz z>sdi-AJ?-(j()G_peV&ixj5FRq&&P(DJefjeuGpHrziHiFv<4m@1nHFrN4_aUu^vT zkQ*VkQBs)lbmL=5ap^{BdHu#lS!EYlY_q&}^6BQMhPBeoisqw@&CdkNn_HC~tR7oc zJwjz$)dTXITQ$RaH~-X*+j{(|BR($sQ$P1&^Us&1h@0CDD=8k^jilnT?XO$)o7+vh zT{m}{4<xBhk`BE|Q51X4ZsdPP2z@AZj)+1l%u>K5N0ke%|}A5>f~-yc#v-rE0xgi0I? z>##jL7%>q3bTDe7@aJI6LSN!=-1_db!wEaLPluBZfqxFC93v%;h_0#6j;7r|d^(!( z{PO2$*1KEcc+P+7+3|eP`lsWCkYn3VLY!ALUQOFn6FpF}xG8iw0R{=COt`3A7v_ z6hyALqBlt3&CDGt*sc-WKn8y!L=U7V4uc~sSYZSp#VbuTG9mH%g6H3NCBHSVdL>vO z1VrrGFbZN(2@3-7!9+-WG|Q1!ajPW;W|S-aM;KVxDwo38TZdu5$AGsB9#>*wdToG@ zPQNxcf>IXBYWfSR+_uA8z>eo%$mL@n9trU_;14f`cd=1*Fp%*sVyT84%*~oN8DZCW zWu@@cc>qee@)cytF1>p5Pu@=h#grH-uusrmUJ)1uB7%zGxxJs2Af1CulQKj@b`c+R zOtf0u$gH$h08FC*3}C1Y_i?AshCPe6--c3+5zG*HcB36{s98>} zYYNTB$&5x;^?)5-_IaKynESV|jv>TxV%AYbsv4-z{{r_&@H2|G{HkA?*v2{OG?uvT#MV#jQa-AZ z8v(v}aOl|o4)$G%U=r~Hcs&Gf06_x+0hDddQ zV$au3Cl3zjAUDTtc*Fih+wO0$QqDAeKHk%Z|G9YE`e+^jFDzRedc}r;e&Pw(4|Jk= z7tn6LRQYn#fX|61KIlA)!#R@yX||0t|eWR{OdZERC#F zSk9$R|Mb_`3;!o-9-DO1>o(ySn8npDbmyeV?&$Ng_@>o6ln32Xv^Dk~Do#VnO60*Q z8<{TFzozm5@_pba-o$HdV`!B95c7Qq?~Ln8-bP>-i>*|yL_N%}*0Ar4Rt$4GH;1^D z;e_>;eV_2p3BBU`gT?GO=GJ_rZc@If__lX;xY0Z$@s)F|*B!Ta%}?{s%yK`c5D{k2 z)~RYz>iE63bNn7WHT@jbKI6Cl9-3ff%rxtm9_s(zMF zW_hKtI`+LIt&3c%oUb%@ydK|YD_py`c%wRJ`*}trcQ)U#{CH~U3+rWv6-rxW!Yp0S zS_0+313i4|@0QLL)}R$tVdce-51p!m8`rUcO`ji0s(kyQ_V>2`nW%<}v#$2TXqTM8 z%IB|7IViJclFftGuag3`Ygh;EdDS-5>3zcPKRn#J-ZV5Z=r+ zF9z-AY?EA%6`BU$)v{Wn5Fv%pCo&H&e}2fmP`rtGK62tyqHQWoHu&yeG95BuRnO9m6Q% z*?fL_N3aE%7+^USae8(^Il30=cHuhh=$ltkSQs0HFh(73@(?EbqE+CZ8<08(F480L zyaY}w0{<7uv_FM4N{djgi4ctgk-fvsP9g${=zn)3UDDu6L2OFsP%ZKYoMU(7&HYFK zGK$9_3Qa>6nPj8 zAW)N!!{7iwvtl$zI#&LtzpVl$z9B};4@;{EZAhcyK8dM3c>@Ikg$Pk-chK5N?6^TJ zk%$kE4DnDvZ>7P*(g4Nmct1OMcu?deUi1k8Um6b}UoBqv_y|0nitz5r1HIc5*Fbu^ zkB?rpV;&55LkXjy_6)2_a1JSW?MVWJF9Lr0?g~H$Uatg_D-(D0EM- zNt{m-Vd!{2z}v4uNzQ4^gzg~qhBwf35EYV=H66f_8%M1Om3fJsg+;ie0RTvlK14{l z5vb7^tNMiPIziy39d5P(t@bk-)X3�ljb>6UF{=-tX@Gr`fMtsUz5(_o{k3$Ckp`p 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�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�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Ì@2&LY=$pq~lX(Dk`2` zy~$liYh@7J;(zU*WP6D$(|2p( zp$DUqfdpmp0(WO^X)-=<4lkC2xX18QPk#)B~_aXYMPj zLnfVT4q%s{6KD?4?Vgvl6r!CIGj@pfGuNo68PV&oAg{tQww*-@@7wM z2Co$hqrzU?M=DRAsF8N4bd#c?G;(fJFosE({SxU_M=`;e)7He30+=;tB3flbXTc~= zq{R*zacwp;`_cU#;_p`iP{SNFH}H>ZYQ-vqY`r72z5`{#)lZzpSJ?Ihc-#z6&ir;gep1}HkiwQWnXANbw!^{M0d$a>2CF0dHw>Vi>P*vr&Uz^$4 z$B5!++jd4Wrr)HJ7mD*7nOZ~mmKgmWv5K-<}FHwa_@tmg+L-*(b^9xC1w6P@#cUCr|;>`3k z;ESy&ITRHu3ke3L>wWAUjN`&q9>BX(=dptOyWhq5pPgfD@DhKIBh@K(H2ko?6!D|Y zB%F0@_rnCz?-CY|Gi`mb!z|kTQDohyI$EQz-r(US&Mwp#q|Ll$EkkG7a#bNY9=fb? zNxG{|(-aaO=Ja|EPL|+7QDpME+|OR<6`=(pt$VZv$Metc$d((q92RWe0mohSo4~lh z%k*5|+K#`kE;YXlm)$`s_NmUs{J;|_Oab=bdbO^-Ng~*p9~D8leTu48k8y)y(MB+F zC);A9ZUuS7bB`*)fA7m2yo575kBbV#depm{(>H`8hQBfG4TkNm80|KMQ6kDln~jcT z=N)=>oK@%wEz~$O7G>#p8~yeoVikbBfeS;`Z>w~f zwX^8d88*s-x!scisnf{dHXjiMZzqDtX^%FR7$5SqwUXy;E&~fd;O!fmXAy`?o6!ATdwz@89^dKCBobKYhV6(z+* ze`ESouw@Ih5OZPD31hr2A4lcmh`gZGjX!b=i__> zT`ckEZ1`SMA_%*O0g;8y=EC!FRPtihKD{*fS~PZxz+fvoV=6Oa3lohnu_;W<3Pm}q z)6f}C41FCkU%hoj8hThJ#6;CumW^Hwk+2Y2@vYaY&m@?Ekf~@D3dSY?R->S{(AH}P zgkie82=GbN&CMv6t&Y30K49XX%aFHjR8(qYyO8kNvuo97O2*p^sD@hotaxH@V$`ih6*`VrmFnb>Lcm4GxFIQ}0k zLv1_&-&lvd*oCZcKp^|m>t@rc&u=29?PlInSaQP5J2&L^4kNh`_`-M0x& zWFl&qXpb0;nIaBu4oxSd)|MDwtFbS^dKm@U8wt?59C#o0q6q~h&$A{C8~F5H%omb` zw$K_hP9Z+X%p@nf^qh!~eXGetPy4Q?hH_R|%(3^{e?eB+x+>k;Z^%`+@rM4c2m;UI zZ?iU3OqA0Q5b6&`V>crZ9fv?U1>ZQ!a2M41`&kElc3y?;)&h3 z7%$?9eG?>vh9aT!xsC-Wb0$Va7n6L9rsCq!WJkR;G$nYm%Zf9;cG8Ytg2^?;L>5|V z!e6Z9^d4RzkqpVOQQP=lY%1#Q4$`vB1Z-gPZ@Vw3JR*L#Kk7YzoTZsMv!UD-=gk<9 z2jfIUz41VzixPXA%P^Yt8sm87s6zb}ks#HTg*8DOO5{#y*Q3(}5TZmZV=(-MC`B$> zPXHx6Ml#Sh^a@JUp)%CX?U%Qu$TQK2D-o%I1c@#ZCs8JkyY?UxFA^4e3Lxogrxvu& ztEUXrn#Yb0RaO4B86O?5TZ-D)$O;7}|mmM-d3Cz>EvH z+$O3)A3ul>eq7Lu!sR{DYITF_w3FL1voW>qyXZpUY z!cB*s5#pqFAy!W+aKnWt7crIGgft<_`W~Xkj^FYmTEs=;G`IPTZJgtH-P?m;1?cUF zm#hG1Cb)QJR|?Ao-Y%l-5To7NcGF{1lzm2#aY-%`a~q9Ol!Z8Gcbf0~MfcAetiG}F z?%W#%?}#77!(0@DWA-52163>aQByDW?AO5rP^(Y{CAsA#3b~{dTD|N-N(HlUb3Lx8 zHYAOjLpyTz>UXJfA4?HumsPc^M}{J6QYmPty#lr3eak>kV7 zNr;#{_2@jQ?gg?$K!n~CBHuta{nGK%)d?H-XUbaQVcjp-zy>E7QvHgv@wz;m{5gjewRZQqQBvh33jxUZqc-K!t!jBp2PN}sL zIxnu@iW+~UL3e*olz9Tc9Q%w-UYH4gf2;33ly$v$S4!IvnRq5<@H@%s8_Aw?3RUm% z{s~#z&vFyD3dOR#(k?}$ZQrz^-4v?GxrMwufNoy^f0FBDb8e}bA;II(7&6ySC#H@7>YT#Ci8l|)ry z=HV6>7TTTDGSS{LCp>$pOhR&i!`bvG^s(59=h+*lTR|~WmEM;mYq5p$I~W-}|3pky zA`h)vpUzL&L=F>iBrx@8%n`II!>WJ2sswwj`I&dMhID$}*KL0Tp~eZGPy@dH(B9n$e}LO%xPE3oMB_daPir>HMW;bMM3-bb3j~h{ z&EOFytb2P8os53I!SLdlYkO89*Eb=#h6vgm(Vz`%Qw`X3%GXbnx1sAb?qm3Nr2Je8 zCq(<=BX?o9RXut=JD#zucXx5rikzD}U@V%Pb8C_>?73%6=d9xpAq@^XDkHXsruKH@{mp$Mq))_ngmZme3lqW>u3VsGt}BE*-8Nu{xaSQA9dcaokz64BA}j6mMJ`VTNQC;`6> zVvT#!ajCTuuA#5TbY#`ocgP9`O53pKepn{E zBk1Y(DmvBK@y~W#t(0uL=y>lp*4Wna;><-UXQm}OZKr1VW?kC$1`p}U7_1u)d*>;+ zG(YI*)rwv7h_e3&*JQ+EIcr2hK~1!@b2%lff^r2CjJpj(o;%=(^MYdoTMNgiFr`X_ zyJPGSV$ByO;2s87fTnw2?VH9+KQms_Hjf7c$4jQ8&epkoGJi8&Vsi>wC$bOtg2v*%j5(3iUT4 z%QmZ}p@&W*_uggdaEb^buOrXa;l-B7nFP%F_v1|?rnYur1N-i|Jm}fJaMk~nwF-hV z56CIZeYIvj<+_C|#4gqUQ)IXeUHrJfvd949q>{3yrevWh7oT#atq&GuA_MoMmO zlr2T8PxHL=564zeUgr@vfumY0{Y%jYUl{8~%fn2F3HN+>S@C7@>D<@ER&Z*@?B?iO zzgCilYlCkpmLuZcDC6xv%bCT2OX`-d>M~?VBlm5jHd% zhUj+k30qwe!)7KS?zeV)Q`B8Jf7x0lXuBlGb^xS zs0|8G%E4U(ZKbt5hujNz%sRxNVv6|vWZFf~#f_KM8#-Ub=X{Tgi4X&R7So?YKyA{U z!!ut?Wdv{r!Cp(ztHr~W&`PGkO-Q=CHK)jTs~l(C}1XWThC$E9pXZl z&$-3J;vQkO(<+9#Pb4G5XNotay$G0VCGU8d#k;Sum`&MLFe$~~`5SBQZKMR;@*W-D zMD^!jf}hDZMc4>lt8bMZdDSbTn<_Wf02q#~GzQd21RsbR8g21it4 z%yw(tU8V8P-s=AO$lZHWYNbvW+}{>eJI&J7x!Y#Bo!=)|qfzf=X!k-(@Y_VXV1U$U z!`Q(oY^E2aqC-kE-=zjg``|Y@K)DZPa8e=d_aDyfTQYbiy$NlvG2Q0EJtj_zOI1D* zC9d#t(fc|||J>5!!p5(y-y%W<|BJgjkB0hx{J%f@jD5yB_OZ)SLkN+<*q6pWgof0c zqEw2?GBXBaZ7fBFWJ^*_rIOHKh@y}rTS!7^NQE%h>+|_u*Y*8fzw^6(=l8w-`km_> ze>qO4;}7Necs}p9zc+(NqivFlJLJD0k=mcN6IU;71p-&-`-E1rwcV=RHd?tslZQG% zx7GKkpoJ6Q?aC*p5GXwQhqCJWn<3X#F=)-4@@BL&U#pazHn6^^cFD2VYJrx5*|HU{ z6L-|v0c)t^tRX&+N2T%}Sy4DicxOiGN+;Ds3{%fS(cfqTA-sq zyzGI7y^a}Jg{#J^AkJPKATX;hCq=80_>L}WW^HhxIm%hgLncDswk2Le_kyLhh|oTJ zGinhz6%DPA=~Y8) zy*f4)E_=fxGho5hIo=`yf8-)VFR05}zF^~B_OA-%yvf$d(6bev*FnzTR=dQUTdk;q zGa^>bA(w+E!Q#bsYU)Chu7Zv?nVMY~3(#P8%eTA2u8*8vdw}Ft$?V3>M0fq zD$2tX>4oLx9;Hr4)m#<>bOBg=#ylou@{oQe?&3p1t93LTy{5*f3WRBvHY~!7Rc~Ub z8W7QjK;O22fF^diX&}h&Xo!$RD_-2qz_jh$)9KX5OfYtxpzhj;PhnBNvSV-21tl+8 z=v_6r+5K=S>fQ~h!mM`8b_3l`0d}7TqYAwF+C}<&(P}!b!*Jn=s}63Je028_S&7!Z zbED-D34n*cGYO9D>_)u7{x z)Lo;YCEq+1yTbQPle<+|AxxkOajlQxtIxOwPBX7G%$BNR9MdgB-wa&;i18W)OE*q_ zZQL$XnuUi4`kg7MlkG`;LZ^_ThX33t_Y;5XwOcIU#xV;!pVJ$-&v4P~h#%nH zs*t(%YXYzSu`Py)3d04=LZ`ttkAYkKMVza3@?eL->H>XCudD*Wn(VM&^}eiQrL`OR z_pCeaqv$SQArTop7+Xnjey+k@alrA<7fsRi=vrJ~leb1cXBVnf!eXk;(MUyXqhXit z4Nf{av2vWh=YsX~QA2-$7V}E000GC7kHR93jHK$`IbNBsoA2(h)`!guIL;=VOx8jP zq^>)p!3PZ9{D!t%er?L>RFmD-t`uf9OeQyNV^S`0Q|b&jfFx!K(uO|W#(|&gqaVGx z1r;!7{@knPel*7`Fa^5J%rKA>UyYJ2Gy6eEwos^J8%NcExCO4iz}F}g-tA6aLa2{SxE0xYXs?AetR-ZAQe#W4qCKpx zdyC&iFSpE5?%y`CV0VcU0=|B)tNwlCW#tkQ0%kr!Oc$24M5uyjcedI+G0u5EPkp~s z<(#NiqhkH*!G6r^WMg|WkBT&8$ro7hpCnL6^$7UL4k2!W@H_?7{oT^K@hb~IF20X0A?P@a1Lf$X!Q-HXSYf0`88m5i-pqZv^&<28^Lvy2&vPchLZ(!j3kP+=^jb87B=~%N9eMJ7>zjrurEXA&j?r-F z6%}=xhns0y(Z&n8wdi3ew6D=~GcBF~c2=XS$}u0PnQC!wY8DIRdtt;I=mHAu{1@Rb zb;AqKJ-RPlEWVE-l~Ez1!mannSD3#hF?_xKLPUBzld9j49z>z$hRn`*O>6`cuk)x|nGA1oAz9MChTNXL&P_U_^fxnCmr9Lj zsy`|$WZ^9>!LBD(vyip5s(?q_C}XGIbnt~c0+3*TwL)7B`D z-)N__XE;d8YY$TBqh*rc{&rq$N7fihT8i*u6d)AfZOb> zJdRCHRu;3M&afk*{Cr#Y3sjZc*#m;6ba`4%@u*WzzY`0kdyoUU;GdmZ`TA*B8b;gv zZ+YQyJ$l#i{gXTkuZO_()z{ZRPjt_AGxvj`ldzO|dSHXu1Fo`bn66L{RjS?T`PiGM zpc~~)b*3Zqy*RP>F1Z77XS*Wn(LcIj5=09CA>#-^2r;R^0O@F(iw(``s`f`qUAP5NXX=e|Z}u5o;YR^~`?g1)7khKMKmnDH zuEf!f#~<)678viB_{YdSgxGj|khZJxZ7m}sYp=cjqVp0%AXEEoQs}M3nyz=IuJji* z7J*NAO>Q)~oL%!KVq1Quk+(T)}LwVZ+C*rB1=&7pjsb=Y^?&hfxZVo$a}+#8c;< zr*5;SUazPAxaaOA&pq3oco8oHMK42ruf3LDMs8kw;~+1SD6f49UZ&YzW+h(c_q_Hu zds*~)S&n-hSn{&k_9BRQTPu3o=zH5*dK2Be?Sj1Rqr4pwydAT>ol3l&?|Hj4d%N~} zyN!FhFL@u__9lt=cqsaK>ic+E`gptf_yqa*M)~+9_>i-G{7ZZS?)e;Q_6h9u2^#ka zUh+A-?L!gq4N>$B)%QJO=^N(edo;-RSd{NS3BKXkz7Zw9$M5-`X!ecl^^F?$J-Ot2 zYTGwj#P77C-x+?^3g0e6L@^xL@LuU(&W8 zRfJ4aB-8cD$(Ce>8#yJ2oEk+=OCU3|$>}9ze#Sj=W-~dfmz+IL&RHT~-X=@gL1}1z z)(2l3Ha)3}v5#C&1AJmUv@qpUVYdHu{ZAnx5CkZtz)j{R*{Jl1f6x;~Unmuh^DpcD zpkUMwM8S{u!rVBZYCBP0xw|ooRtma;63KO*qt-NwsHH!ih=jqgkn5mPR6>dU+(_~U z(gcM6LQLLIpvto$FmeEkoNP>P<`JP4Rq6h#>F{udJ?_wxai$zK#k|kmsOXRpAx(%3 zdSOR0Vg_C((<|p-_UmaP+GE)ikQ*zcKBKBCR6zYCe5O{msl>mjYM_45A zGh^&bLOLKk)Rg35S@Z(V2#qo>Po~Qk1)e=UA$;Y~#8H?%!Jjor)nbAZ!zThSQj_K; zupZ;aaS+Hl6-ydtHqhYYF0HXQn1!pZBvU^|PjIPA|7BT0Ko{v=N7H!9B#srWni6Lu_;WQ5!`Lo?sU#*?5}X7yDUMlhjRvoOHNj=zfH_JK>ArX{r`>%HVCFY#Gd`=*VOZ*tKg~?<>+M z*caB2ECiwkHze;bnn~N3*cVZJm>O8PjWi~I%AtnRhC$CieBN~$B#)g)B7K23B$IQF z?($6*p!x?dqrN_2RL+5H<~)H|`k3)hOgeoYPK6VYF=*6h#V=TNQ4)SM``Uy%#h=B5 zNY=oz%b?yl7^yB)*bXwL3+Az&mgGJfjAq`1dz+GqWbB9RaTJi_A+7V%^6NqT9DJ=d z{#sA=T#Lq6d@#gH^q4uh080XIFEH&-!pN!E?B6KFdYW3|dppOuBSGP`@R^>dFx?tx zZpWxrT!<7Cf*{XlKco*N2;?S!hN@ueDI+@eIansmfCQ$PglJXISM7Ku0bk}j1P4(a z-2JPp1C0yams*kX=Sh3rvPW*Vh{)n2tg1uo8{Z8#j?=+RRb?>C5MGXg7Dmp3E{w5aRX(q+sbZlaP#zqq>4p2<$VuWhZoR3+QrS zy7*J<-J==HAl-F_3YM-xri;!6OMZ@FnbOz#PM&ZVOMW=ZEL*Xjq{!GsvZPVxq?vUH zwL;K>=#_|9_QCVpY3`J;7-r;Ud@Ysj z{dwEh(IJ#Rgi6~<^GD2@oR5m!Bf+n8Qa54K65@U{*Wo5LQ6Ax|0;fL{f1s@2F|!>cz<-N^8^{4LxR0`F&|U zOI=) zlUYNXy|U?qo7NbnVq{RS_^5gqZ1mn0ZBxk7PKqWOmc%)gq|J~c_g_0T|G+Bm(QjAJ zi;yyXTLM$d%#4I1Z_k;jbCaRd(a5*Y@1b6tBKhF~+EKb!Sn!w{wrk*5ihHQ|4x z7R(;NBzYIF?jXtQ$cqzOEJCsohjB#)<=pr4_#dX&D~4QIvIrLB#@b}E=^D7{P?@F6 zSkRI*^ZCs@RRmLvzqKvV_CU0imTW%u_Zv@z%M{o0tP6k0vXzqFcV=;JMXig4Lz>O} zVdr!eC>-IU%U3meE?4AO5I^qHd|PZDySfHknJq8H^m8^p&S_TIi{>B8Tb*w$*Y;4d z58+T$%aYFNSz?&G^GP|`no?CY5dsz1Yjq9m)l+#_ocSj4kp;7#vy;xGrB*(kdT)iu zBn9l+;-6}LdC*X9mtA`<7^hO9ceF}YdIn;&TFIV5ezX3EiQV(1?(7ke)^Ah$%zuuL+UH=S(jz{f)JAwNczpJbT3YwtXyiuX)`XwADJl+I;P!b*VLqnwsvoH$ncuvKh>#2%XIi~7*3jKs`>k?>X@U{cuuGHHB#&$HA51yxWJ(7Y_bsB zn=d{XtlavDCcF<##%c3~S8@dny7p3SX=G!Jc3EBb5eKRTTQR%)*5rU{bOZNYo~UtH zg{?I_Rl7nJV|YEgzJI93`P7yK3PE);)~G7+tfga}7V0Tls4m=VE6^D2t<{`k3SILm zY6YYsoBr@1a?5|M8(HD_1QR;=Nm@caTaQYmpD0>+C(k@bcjNxNSv z7mp_@X3b$rQEIiFa6W}fYnOUV)^&{CI-Az1fQfqri3Rm#cE-Ap!_GhlFL(^}!;a%v2$hM_ zdLnYaUAD|1whWfnXmZTvrc&W;JI!_=ZV*+wJpcrCYnq>vrCAMM}sSeuyd4zK-994F zAHeZG2@=vJ-w{9aunn!mlFewEikWX`2#x1Av8k%3ltaq0PeSCWf{ z!#Ek&Ev~=Xpw!Jta8q}aREoZ$uzd`wHu6e}mP}7#G1RDp^rJhcB}&$lNy|3fs-Tc$ zq&3Rs6U~uvU$sK46DMdw#*o9%aLKWiGdi*$4QWyuaW_uFg-pk8`lXMyLahGCwq19R z)nG2V?uoutBz0|urR}SpmRUCZ&~&xTUNJ;cVe2D?f4KY2p5 z5*}o#$Rd<_CjkO~7!u@u@f+PCm|#!yQVSORsqts)i;OM#v%MY)^Bm0-1WQiAmUA^JN6 zgr5}17cCZoMv&k8RdRRP)#4p|ahcq(KYX;CO_)IHlooBS+@iPvE*UWll1n|Bivsr( zS9Ls$rR>mIlb1aNb`>1-_p+>ynp7IWa)Rp$1XK(lyKlMbxh~_Gl*7aY{FcvPl-VB7bMus2Q$_d07(g;>`**rB^bOx(Js0{QFM} zo!-wRPl+D2LCYC+`@X_)1nvBZ8fRiMlFwIGNc4}}rs6-oH8}2mKZc1Vrw!Yu9|xr> z=tpqY(fS9}iwp_Jw76&e*Q*&VFEn-wZoJ_xuf;t0u;>&yVv0^fhe8!WTy5@Y2Msgc z#ffxlwcDO1Uiy%5!5o2Nozr5ve7&;ElFzLb2{dJ022Tcy4ilbWBd!jzP1(^W%72&d zf4(fE*|y^7tEV7+%QMWH`hN28c+H`-E}4)MXnf{*BOC1>uwvjA+2Hc1sZQY@o1r=s zV*p`P5_8Ncd6*OAiTq{JtGNFh-5KRii#rWjj+wTP2qZXKj9!~AE&F<13c=0X6V6r=K)`_-%wc*IG z^+9=ASQ6*cO7l0h+sIAhq#Jx9S- zSH#bQs)PMeMLHfxSM4>v1Of|1AHbzxi$+Xk2kwwU_;koAWZ&vDU&ZPV{Ht9!wiz?(h0l9rEY_L|&imEIRo=mMEGRwt4^=_*8^qPfBq;#C*P^3 zzp*K@nemG_yZ;k>%zija)nv?fxT18^ySi}|>i?f33mye zBcDYA-yKA^8e8p@`RJ-qYAg`+Om@F_4k!~XPl zDhv`|yZqaC4y)mn(PY!-?)mW9Z@T|{g5Sm}+0mT_KmUA|4U0IEC%sxoh@|ui!_JQ= zt$_>cR-Qs*wTObs;s_XILz33PL(~_VheM#wbqA)!|A6E1LLw3C27?NDw&OQ$8qHto zMtHaJnGC5uVePT^L}4d2*KQ9HhP4GzWwErK93YX<7Pi5XF!+2l46}+SC{jM29Oqou zE6h`%X>@h?#FZjCC3mUAtXOUAL5^j7n-&bBA`H2a5O{dIeW%7lb1X$Fo-X1zHq4WE zoIH3hi~rF|vW47MV$1%lk0A_}c?b=DfuF4y(01uHo61yX`8_&L=1{_0>1 zc)&EY?bK?(a-cR$LqGC(M#LEoNvVa1-AYdh(!ihT}61V$s9 zW+geGyjidmRIEP4c`n=rjd2(u)4kyGlqH0@t00-|6>*DEy7I|(@b$GwqK+sBD%o}| z>SMWzyKxOi&hgeQk%FI;Hni~N)^r$ceZqm=iqRHMM+rD~rIZiKvFk4TfNV>56+)pEsvT)(cfs$>Yco4LpUvkJ=?-2t!Ob{MyowdGvE$ri^8Ufi^ zZg6FYV?lOcvyvM2n%zuSoj);{?R<9Z4MBl8)ZZh7=-6VZxsC+r8osl_m+lK-m~>P+ z-t$9FiaEv8_fJ!NN%kXHPqipwBnoc)Xxx^a7OdBHKmmm2NlKn0*s!@%Su{Zqi+Zezl%W(5v~Lm`-nMQEldlohno$t6uvQTUt4!V;oVjWL zn{o1j3M*SY$QuIuW9(SQJygpKbWJ(BmTBAIA~81MtKBn>*Nk(14 z&DyFTJs5}caZR}!**(}rz%SY69w17ERtbV=jb!T*W~-a3t1rd%2=>kOWf)RB-4>uY zhP;cZ^X96>xt&3ZC4Q%T@-r8*tHS!*db_V!GK4f#VUQu5-m#l+dxz_D*9STIJM9N7 zFeWFtO5;S2FlR+ZXV5YiG)Sm^qWH)T3>kcVAS|QWotUSmE*L{SH{Y?xsDKzJm&y3V zZw_7vLZG*jHN$;uP}Zd%!1uRoi;gSE+Ua~UcB=hAGwqrGyB@O32j&dY@i*5UUk5^1 z7>cls`!-am4d&d}YAH!Kd#iY`p-{ZB0~E-Ii`<84aX}-J5IxGLm|O~BE2)85UkJDXLN@}+R_;Qk}0)UMBQ(lgju`6G|389ZsGWekvF zvduTvbByGGX1r^ZU@DuPph3!G8!iIdZ_6f1qPfvScfNI}1WsbxOgkM6gUC2(>xN8Q zNgELYqR0lO3V{EzlM zO$t%gAiAIjSG;Z42BBrd8@RWh5%wOgd7bN#8r(bC6yi^& zjH9!hJL=n~uzzladHa5#3FmP?KHZ&US0 zT(!yF{0xR!y1OJA;&)EOW=y00p}nGlGtC;K$xEJQb5*mBiNH`-Eg9dc1iY3P5{|<+ z0|r!!Ke(VF=6&2@8%Ak1L_z)QbKxIbt1=d{;z8v1Hn)EKjk#yb-b1qp9}r~(2Zaef zMpV@S6kDkkOKXYnP*(-*n9{dy$h1HtQ$OzfHMh$ra&=5{G?mmraCfI-Ko#=-4Ay>#`|GV3Htt>gMys;Qb~}8qU*vZEp?v|wp(?{F$6Id3#PtB;r{Vj)JkLN}ItL|!dL6yE^B;nn5I)d86XR=|% zapR}q-*5dS!!(4gtLv}*Hiz{V+}*D=G#pkE>j@Kl^+4&Tq`58Q7oxLrH+S?{%&D1c zd*1j}=fo7aI~hBNeC&BlqHwB0e=2%$nVhUKcbK}GV36Ky0yrqcjT=2aL7dUnivb}( zZ6Kj>_}!S$=M-4tH#BRYP4@9*7u%7cWIgAM!(YaD{)!?k;liO-R@^-<#4@9R3oG?ul%G$1!n%l0bAgI!pON; zJ1LyrBga33y>j+Y=bftKc8U$*)vo7H6|7bL_70~pyz6p>^p5$m?~DFTmGN2WamHs< zou#21rs-YG$P)pYq3%*cU$-Jx4X8nWMCbhT+i0#26+@Y+cXYFBm%fiVYzKTp@zFP$ z^b{&}o}H?EYNZwHIJ|b0`A2(2Laek%#4kD<_PZ<;q$kWgX2mdZt(7=nWLI$9p+L#? z$k4|pyEX8jAeLinQLOOC^U7ME#Xl%Kz8Jd=`_;sAW&2<2lw zSMy)Kdj@;oLoyAtcN;8&X`BRc^R#|W*+!_Exi4m27b0Ifb)FbG1e);|H6za7`DK~( zyhpmtH0Sy5XU5wwyQb=MwcBBe-gIGtm>q?xsa;Sq8c=bEz^|s!ZZo`BGzv@WNB zg^&HDuBwvgMK`=!^w&Y# zz>cSu#8|bh=2s8@oaBh6Vt(S{mHfEJ;J>ZZyQ_tYAZO`@ckLx(2(N5qal@CI9AN2` z*Eq?xV6L7lO}fc1e%7WxTqXYRX&5$_IF}3IoOwdkr>CIlVRv7+qr+afNCau`ul-|o zFvQHdE9K>Pi->PqV_AsUXs!(kk?X*H-oWV*GXK=kF!SWGXJbI=#t%!%3u^aY@$2JS zWSem1%AxP;BF^tak{Aas5ogT2lYuvhhL*>&-98$@K7KcNJUi&$7BKXq*?CWb;V%B` zY5FsV_UroEd6>k`bPCu^j`SP08clcf9WSKDj8xKjC}t z(DU=ZHl-rT507+THNGSr74YQ5r>E!sN}mjTes+H7$t9Un!OfR8e*gL_6HRGN7LYNC z$DR)D$dui8ehYghtUG^~-_v;6v&VXGn8p6yl8p)PuW%|fNsv2t{KH-UXXm%&&PRTF z9P#mKf_&`B@fR2V{N9$2i=Jvvm)V!7aN*3?H`n&X?kHT0nSX!B?^&YarSnUp&tw1W zD8|RFeCaOSm!y<%@z?iH&tmyXiSZjh=RZD6QcgD@-bo z)rwGzN4KFgidk)f`c3F|VdF7YyQt+Bnv1qm$mNQ=SztOOeFAekq=Vuyo!Fzm|JL|& zQB9av3g^ahUnwPQVY+ZB3VB_s*%sp6>Q@5ux;0DU#b0Yz73aOyxz{BAM(@d3-kaUc zTjD+V4u$+4!(I!CUZW3z`MoCN@e*%MzZU1eHD79ycxUlzEdSks?JbEu0$j16&ql;j zvY#jwRM2m)m>@aes8Lcd;H=*)`QFudyx_gN<+kJ?$xiXgpr@Oq)CX^$perAIgA$~M z$VW@A3TqbbrSzw;D?wL39V6br}B^jgY{$9xDX96vvvAQL<|!7QA(xYX>YvzI5bmj=@umT@i8 z5(hQBQK#^%g=mPA;UWnFa&Z&13H+CACj5FsXqIku+?Nailm2vZi@2$q;?l>$=}CMi zGMMHq?U-Hk0p~b-J?j3(7x9qwZ!_td*rN13Mc8mi+S%oY)FkiH_J7QrNt=iB`39SF z%G-(;7g8?zUKgmlyPTT}%Ujn<--9?$S9t(;4*mY%TxCcOtwYH)tzIZ}beIbI&sYl& zqz+<0`H-}$5f=d)_BZG9@2>8x4+YPTCjxwKN=nx1FuT9J7C>}&yN3aSE+FCph;EBb zARy=hd@f+;#uYp`oLT#Jc5(H3T~2BJ^u+st&`5yKz0uhRfVY61`|H~hB_b9Obj|I< zYHRBOJ2$7TXQVnFP;&t(_v!t5Z~I7q)dk>OAUFSXXDcjGHr$iNr?>HxiJfq?t(9wuPt))h4Y3Hz;)KABL-2=$F%h%_CHa*Z`@8i}1=2~{B!J(}t*-&lE@1qgDW3zZ-Rm_yyv`mVUOzZj0QkDh zn&RIryvEX|>Faeft6l#Bc;^Oz=04!x^48{nc03TokLGqU5+{LLJ@9CICMJR8{SYAr@Nx%o^Z?5j=+pxV{(%-I(5au61D@=MFXu!9;4t9m zuKq3nLiqp-3?RBdpdW~}0GSqmVg~qJzzO&-2j~riyYscZY6JhfSWD0unnY*Ve-~@f z$xHE){{IP|tG1=}?^p{ZF(L5Rzxdq2SMq2jAl4!TN3{Zcu2-k`Q9bQGh*DntB$MB? zGMW5uK3925f#wn ze?rxm*NzB+>ex1?vh4Zm2=6SzUuiIlBJa*rZ6^@;Y6arN!apY(d_kbSbsSEp*;Vt! z$37b*?Rp`z(Yz;&Y^QrCRKs7~vrY^&U3xYnr_l!Sx_XF&J>Xcj{K9^NW8cUVH9|_d zz?uIvZ=LzthSc0S&&f=j5mJ4+t!eQAnFzVv3DFw7UXZ?~6S?AjJch}OI_}s#sQ))u z&(I5f(qgq+`*z*`W_&K@mTanGaa!<@VtL_?4;Fso{-!VRB)pMye%N|*q2BKtQmO3Y z{TJTndHH{rIa*g77F%UriYr|J33(MimjBUYtu)h`au9!RrF`|>Kgx7zU#6fJir+A4 z{d?xfx)|!qqnYF)nSviXe|fL=@c%#Zxz_c(FQQuFQsbQ63vSsln=PbwV_)Zsm@mo(ajnuj zE7X!F4|2|aYLn-$=vX9zS(h^v4eFVWOKDYTRLO~q<3%rsRcUhJhuqC@7pYKU@4?H z>Qe4vSZt?O3#ugglIpxK>fO`v=qta}M)=NI4HIWvhq5F`Pe(M_5r!(UDl$SA`!8jIAp<9jcV*ljoZ;$h06=|6+%vQiy@NU z!=jMe2YS2YY*?^T%~!%C9bb=csz~0p8Vpym;M7P3-(Kjv1bKKoum?AkdXIbX*stSJ zscR0ksT3eUUva~8bo*YL`8t1V*=j(|Ap2J^cx4iq=iopNHlL z;+$kit?-e3wBFjPTBwsa_5f6nCS0g;aCldgKYieN>Ev_5J)xNWXCYR(=bzVy>^lGE z2x9Q$fAG1lT!Q-^dB6BNF6-`)5qzsjvdYTsML**UUvLZ6e=Tfz2D#}lrMVg%PftpbT}g9Xl{=qgmTZfo7bkg3O>2qYjR$bx=En8)6)(goiC`|6n4a+?%tSO@RQLV zQbKr3xLb<*-VAwv_~erZuO#Rif6Ub!&KqYQHjX&74-7kYp#FNHzLJLWH8sjk+TE8H ziv_r;zTL~wsLI}`6{AA~3RCB9D~?V7ldt|Uc=FucePbtiaW_6=BOvR6ZqZjvL9nm0 z=U$4a|F}|M1|JP>06e?n>2DSVgKHW(Le7T~PHkB^g||tWY@hx0|FdY8Rx>!OSZVw1%#!5|JeMh))5yF3U)zHm@0!alobRyM&ckAZ@l?L+#* zz7X78qG1B)%d3u(`zhc@Sfe#mwk~Sx$fK_bR?-i#|8<4@-hX# z|>x+5je0@>s8H#y+FtOEIm>6UR9mi9VblFS?c z=f7+y4BaIzMFPJpgXOOjLad7z5iVc)4t(F6W=6=?Mk^Yaq1EOKrJ|wqU9wUHhalE9 zAV+cvb)LXM^3k~`4AS!N$Xio08(|_#61ml1VV7lPtol%2aMu@XfD6Z0j|_tf{ap@n zpeI>ke71%*m)7Icb9*CD&CvxEaC#rTwxcuHPW$YnmxeL4c;s1Ll|C3IiwAw|QZm{UvnQv^axN=O zA%ISf?N!$uTy?~P__i6+TStP)zIvfnNoG?_<~UspbX#!>8Ybtu!N((Muf?sRZ|{2u zZHd1AeX7#FDBS>Et<(w)&a3KBFXNLcbsxeopKcjp@~+k@echBgsvPeGsQNVI0e3y?se?6Q-mZ-tZ6Crm|aoY5Ophz0Hn zI#+CmDwZKwi$7h*eFojjgFZ~Zd(iyApmsHK>TaVQCV-`W4Rv3=6}f>cZrcUV4UfOg zhCZH``QVH;UKKN9O6pw~6OgS_rQ|+vK!is_M+D_hvTJY#$S;KJqr^*C&_m1%#j{jM z01vi9MFmVkzv*03;t4fwDrV!6@wl=bHtcM`r3h99mOr8xbi53n0hzw&68;E!SI+d1 z4jP>45>G{S(M_QvRq=!y@soA8*GdKOPR#f96(y zCf5_}aEReXdD$`t;Y$f&B-s1s@-5Ldb6p8~yj!bxkQdHm@goinQ6?cDkw(745z}M&Bkg%tV5pKb$tcDwz5}vr{dqP0x%o}Z2 za!o>zr)wJ7@-H6ke^D=Qa(*%cI88j=mib&)Cg|_e<`?lKUQMgKOvogJbEqi_3~##I z6y4wSR=#;)fAip>=Alc?!#A2oUNn!5Hc$L*p5)87Oz&@*0r*^9Ii<49Z!e?{Lz=(u zmm>Xb@jMOc*1e!&=Fo;%tT0!J4R4{=DD)#td163n z3`862FsDLUk)5J65Ek7n(vL95!<5Q^pCw|4v!G-=JbespiiiEB!egi)8=hEHUk57- z9{U1)S6IOd2MN&aZm6@qSNulU8dTBqhLeSfVZUk3LX!zS9r5UwO{8f2!_D_yVwE+2 zAB%O35yEW{n`*ERJ%nHKRyI_)61y`7iTuj~>CDTR0&X*?*HQop1;HBr%9!%HlIjXv z%|I)QG9J9H#jJcKw8**z_IqS^cVHhPp%2yTX%!TFvo%Tcn~Zb_%i4u=d~<ztYpHLCCfZc@;qgv4jiJu25Ub^-bWJJ`L=_LJY7F z!P)RaVyh7z{cx_^svnUgJQzMDAySuEtS5<8927|S7+3P~Vh~z8yKg(NPlNN0Pxx?J zztetP)SL>BEgnX>fnX~in-t)-png-fmVLB89XuJ;2O$k4 zl=Pfy8lbv4L?VrOYmX?gm<@=WmZY~u1<>GDF$^5Vqu z^3F0(>Bs8-#pkYzDzxRi*=D^$EdAI}0$w>nA0LMzPW5RP0YplcE1TD$!0<=I!kMWe zH+#^h;I->wcTqhuH?Q-{uEP_cU=HlLEC|m@75fZ2(y*$1vq$yas=jiMJKtK^n)K7; zaGw_yvUptmSzw>MK2HF=Fl~^kOuiHXf~S@(OjxHjJUdYER&ie+B+>Me;b_Sh1F(E_ z8sX)yz|X%>@D*HApD`8TPg+4mLA_aU`I|jXj;l|XY0jpVc4F)BQDIk2`ALb9txgxmvMjLmMffa*Zc!vojY&_iE z=HK@_;c}{4ZkoSXdmx(G5Drf`C=O=*I+4@{w-wIjtKKQfcxnxI!oE50_s-9H<0UmC zDg7@B%o7Yjk}Dbes2Rt8pugA4WG5Ak?_)+_|7wR}&N)ITB(Ov-9}7HXIsPL-Dmq#8 zRuTpSlA%D=Pi?1pKAda*W- zVIDL!prX znVSQ=Pd$^BYZ?O3xb6Xwz)2L#>lGZ!C?{cOxG zE58pQw7^iYGf@ES0piCjc7nq~&AovwWb^GHut|(?-#2b%i}qF&FRG1M^14qk3S#*co^Lj1AS(Gl0_zNV&iuVSO;v#`7#7 zygNISozqzLmguFu1w~AhbhUi;s%JfO+5% zs|i>^+Mad+G{uuQbI06qC0K+0`t`L8AAcRdvQAFmV( zD_^v3zq?6D7F2Yuuj#LF%-7L)PO)adUH;poybfFI;{!^2hN8>Ux=)Y8-`57cd3ta3 zS@f6JB`(kIkH3ifxiJ3b*@Mq7sZb#$*9P`fJM;hG?mnZMT-QbKKk4*5A#^E00TC$y zQE5T|l@0+>QPCL`6|Au#DoW@G2%)G5hym%M1`AyXMVc6b6%{ogN)aIxHPoEj=~;8H zz1H66to@#MjCY@R_{HJWFAkZ;`+xnei@0)EXJy%^R^~F7Y}cxLpP!_!I{2Zp>i%FC z%RXM+t@^?6^P)|+c0H|r`1Lh=&y#Gon(}Y&Do=d<@U*64{39nqQp3Hra^h24>dHU3 zwN+DtyqhjJ+#gkc|Jrl^;79JGnxEtRrg)KtM_ujA)X?)=e{|J7n)@mE^yG#|eci&G zaO&&FuKIegQvSedI|f=I0IZZBD}20}A!bT)rLPxr2$$PZ$zv$ar}o)MIZSqD|o{-1M zk7vvWd^RTDx2PdXaFV7*`^vbOxUdF!ga5Z{? z5K$s{W%&Z7Fn!=I<#=RQ8l=zCJYd)NHVHifi?AbgGMjGnnSB zS3Er3?YR8*^fTAB-=?2a9Spv|@ZNg(`%9nwx4*ygJ^k(bYg&xKj~@T4hkv{YD7^jS z?V*ZqKi<(>4Sx2Xe0})m`{2RbKR<;06n*>ok%2Q5@*@?G2>W7}-x2mFtQ`}6VmcVk z45V#6GV?io|DBmHS*OQl23awNvqO1Tt*>RA%krBY*%vr0|CJq~9pO~cdSq_P{d&VR z^)tO=mRrt_cF&DBZ_;j1mzBF4;@vP(Ha}(Xt-*n9T%6r1;O!6P$%Kq*5n-~$-31|9 zp8R;pDtSQ{B7`}hQ{# zOuOc*sq3j+J?z9dTdMV5Zr`>TpDSbd8kIbQ)9p`o%}!qBNEMJ{oH}=_Or^KE7MNUZ z@7!lGb&Z!$U|I+&#rCO;p3Z_b742>_MFNep@TRrjH?DL4;jZGWQurZS7asg(e?9oQ zryM3FMtRK7Pw?O!e78`9BYoL)v~h0#$FuwG!{}bD6;|!V#l0|(R%ZrWTICLI~MQpis{YC-)9@>a!U{u&+3yg+PUHs zkHHoLb_ORQ_Wb8caZ z0_EAxvdRpjTaCjbzGJLcDi4}(Gwi+|&edkB_I|!4R&UQL6_o-?P^Y+xck( ziB2dtIB@CfewpO5PEqa}T@3rH&2bvTzUSq`uDFGl8w&K z5xx6VFm-D&^mF#RQIbBa>bWSc=hR9YNDfmlE8iAkZWjhCnKg8KYV3c^=i@~7_1z0% z7+nS=K?~LvZ+o%7Ux2rzY?0bs1P=RxV@jbujZ)D^He`_dh1Y|1c9*;i*cbAtWxHkz z((jmSKNYei4dhc51Wqd_F^1m4*0%^qEl-eSlr9J97l7NK^Rkd*P7@Bn_C9g;c^o}V zBpE?AzOv_DjLlGkR$li#$^kvt;$)c279mcJldvz1qzst}Vs4pi9YcoF@2%) zbC{!Tr|z~UHKoJD1^d=MgJzL87D|W0bj_c8^G~MKu%n$L-cpV+8&!7e7a?i0S{RDG zwCv%)6>YOGaVd6+d6ciALJMuBTZoLO`lBAGC04-NEBRg`STt+y(N}n(;_jAm@HM3q zVj~~kSeK1dJ=3dr(Oxm(_7GSnu4Sh5Kn{)Ajx7sCILDNBLZ$5Hcy9In_$0Of^MF?^0ofG%2E`0pskJsw2?pRte2zXg)&b#OC zKdav@jQx2gm;1ABZeg5(ujS)BgpErU1+gBr{fb?}7R$v6=Esb53&PCGb(E`@|LFd_ zN+S;!AU>64!yLSNSzWyNot^Y(bic>k+jomUD!U$yo$i|ZFt_-V zgRc|Bc+B@N5eeHo>L#yt&3~~J&G3@yrVBk5Ms|p1d%Eg=RCFzjofFOR@%18MtHe@xjkR?>|P})$P&0EbOqRv|11Kxgyq& z&lsP=rG5`x&qgePFsD6{WF?|D1BqZF_X_Z;6ve{J#3L{yrYm_WC&Ky|vh86+Q4WF{ ztfI$5Z3X?GQ$lO@z~QRb0)YSrd`_(xVSlMz{81FE>YDCT!U5h(4(s zy=*ZkpuErGhNs|78C7g>L;Qjy((iwRHRpWOhYa@ zD1anQmNxf9x`H;ade?Hg_6}0)aguElS;~YJ)7B8@-wjfM`>j5Nys6X-SxS8wqPj>$ zX95*yV%x;Sn=s;LYp1>VwH!|6EAxrkR-iPrKHbgC}YZ*Ni$_*n#0c_RS&ZJ3MPugM$RcI zo6w+DB(pb_ZdYH3FNm30@wMNYB47$cBQrN>LN+XHr~V4}%7!hu#EFc9l20l! z53mzapR}K)2a(tbdVxl%`;$$7RSMYvkr}KToEnv5AG|paoRa!X%s0|o(DNroxka*RzgJc+ps9QY`G&kS@-f*TS%IP3U9-%D$_&h9LORkfJ^GEyCfe2?saMxh;xox= z7R;+i;}lKJbSfo1?HDo`jn10EQv@-A4$yJ%_QY2R@xNA6hMFO{UZ-sh65wMuqm!K! zgQyulWYZghktda)6M-&igVMVu-LDJW%Zu?X+l+QOqPGY$%cmhc8B=&BjzG^SufiAl z7_AZF*Rc@VRHQN!(IWD{K88R`veOUe;OBYBoXtdM1~S$0Vv;e6D8QGkFf(T9yqs~L zoV^s|korj7(!=#3t&^HOA8Flxq{3*^(A<>LjKr0dZXbJw&_BlW;#*5G%~{OeVl* zq>OKV`O_;t)he-V$TX#tzq68g8DKV_*wb z%Ig&wBT_;})caQCet)B_K|q;TMfe5!275|af!_=}Hi}1XYI5r1-+eB`v7zU8b4W)_%i)S=_T0B~2?14J$-}9rDyj2fvKt^qid~yIXNs6F9UJ+W?j;j zL&+m%!3PTJy-P@f#1tapI5lIIes7+{sy~^5WMa}H?uaO?H;$aS+ zT(ShkMjoKE1+6F#YG$o%`~f&_5{H$fl|1r99NC^cT>%lDORM}K76~#IKX8hSS;cTN zcTozWWcj%$4QJ%+U8yt&Bj)*KfiCJ5fp@Jv@7*d`>#?5K>ZsZ4lRIjD(Z4OX5ii%*GHTo<?rmv(vv#cAbsT+M) zH#S!%SW-V}SwFp_{>Qm`VP^g8?=!h`^^htDX~jYNao8fH>mUylzJPMH8hY)2^v<7>Yj}{odj?xS6@{Q?rkKiHknv8nff-Z z328FRYO<(pvg!rO%xga#n>J_K`*kOpj9AeQgoc$PSR2YmEj1(Gda?5m1ybi+)F3=2 z=p!tsh@&Nq`viD04QWkj(r2Rs=9?sPpu@}-=`gfDMM9P(F5+WMYtdkrtqUJ^0r%`_ zSVtBlA;53%ZFJ!`ALhdmB7Id!9zu=P!qm5mQfS?ySG=77_VEo)Wy zj0<(4>=U#vd4W7m#$ocrQ$uX8{;XZN2^E#v-W#>CgW3vr1n;Ll-Z7`RkJNTGBy~TG zCbBk0l%e;}cPN~}K8Mk5`byUHmfVn3SsruiWJXlfV^>jRe=*UJ-)`EwUuBcmF`>@u9-n!rxBw2vB;J27} zKiwO`Z96FILPCcZi6g%e_p#9%1^5`%`anJ;qPEQV5@QZwpIbc}{R({$;uWtG_tBn7 zFj|j_*9MBn=-^?D@g<@)4g8vBY8iS{LhVsK=E+f_Bh^yvx2y+O6SNs_&nBK5CjcggiIj96A5e zn*K^f?fJt)uiU|1m)k8Koyh%%LD*>5r}xfSE5kvEa=-# zf*qNUP*)yW$2T-?amZ@^>1J+A<;&%9O(`h-hKpn-FV_H`SZD6e?pyi1PwfstbCuen8wqiht{iLi8_N#IIg-(cvJ2vaG&dCvDgJ6nFa7h*aXbM3S-k z8S}+UhVG;#lJDFX2dL_wy{tcbA7Q{}p4g~jO!(4ERog%Cup60mY4dWj%(l%Jz64}{ znUSpOw`yjERQ1(X9k3q6Ot+o9Fc|#jncR>MgBKPDC9EX+=5g0aU(#wJBau}5Ioi;H zDX2%XBS@yhN2c9`S;@FCoSr?LQ8%3RVK{qnn590FdqK9}a-`tGNKy7k$v>aT{r8oE zqdwaBuT_fe4KK2M?Y(w3_garRfMw(9VtPz5mMfb43_q3_-z?USfU!dn@oWnAniOW2 z7?(;ENs6P0Wjv?lBVe<7uS`NspLowNT1Kl-wT^>Ah_)XYjWLcvF}5 z3;b^b))L3zst>j?tI|Uz?JD`6G2G>-AH?DQ9K<>iNs{zONZRX(_hFDWKYZZBl(lFI zpO5f^$Fs7KR2a*KxH3kPw`ZR2bQE0ufIGs967y&eg|P@8YxCWft#>0J5q8Hmt4-|h z+AoM+J$OrIT-T~diw7&cbS3@CQbKFlNL|TZ70wY}l!%8vD!}^J6Q03XRxrnp8O3JB z+z}G|Scr>)p9@PiI2gef@v_IA#?e;xN5GH|bDBGpP}P9IjtAGc%KK4~d*SpVjUSA= zI6qdD3R&vnjFOkq+*Dyi>`_An+gNQDdOh#w@_HpzNb-tD=yFD+Fb(ckC#+|pE<%#) zMM|nL){-ww9LzGXMY}SPQc1*2ef*^qjr!PF%)2haO%d!Tn7)D+XzADZQ6>7viBX|Q z$E5h9h|kb05y~!2>I}v@`<*0e}NY9Ph@bfvNx?;zwOGU=9E~0O|1SIsX~^ zS0Ctq@DIQNbcWx$0Z0e%qW{}C0NwEAT|F=n06Tyz0LlZ10F!sp8ta|{-SCSx{An40 zwg?J3`DIrY2ozg39*Vo)2yWy8=>QT0AQB*J0OJ6F17Hrnjl*BS;Wupn(Ex}BuZ@R5 zB3N9Q0|^48gUQKRz#r_~>AxGoU(&(O`OrqMLttpXrLqlJ1MrswAO_&g9(V!Z5y0zv zC4Usq0H6lI`~B0fNm(S z7JxbdA_G7lfG~h+(Og+u-W&@w2WS_7P5_;;(VYQe1qdl1i2zmcqPevE85_6>03|rj z_=307e=`o#fI0js3Dy6_fY2@T9|wee$2R>_KnT29d@29$Z_>&K9KZE{X+YR^3HqNm zX=iWy$*)3+i=}EDULVVklrz*Kp+q$BiNxzzPK(Xq`8|}_>1!zC@pD$~>q=U0W}*IkT#mgGyOB~lKDIPPZpEGmAY zXk>V%n;Qm;_s5Fn(?4IEF;{A|k&rOm>m8KG8o(RmBP11U>U1?T@~$n5G}mBnSx=Wy zX@;yl`%>FfTqFki;tk|G2J_4g(}wAqDzrQ=YqmM^I`V0X7uH%RKwd`ebPC+R?1A3! zft5TFyHTRf)_M0558DzsMTQk;AX+td8bY1VJ1|<3li%P9yHjT z@!-sDJ0rT!VV92z3S;HB4);5-2h^u=(W?u-j(KV-3HoXU8+7k^buy(U zJC@w)oP0w5Zx;~cZ*Ob0&MUOuB8QyFnf?05Bi^P|_Uj8T6;4%pdaK`gH)gpxgrD3x z-t2N2XVav3H^l3)PW}AUioR}#CZl0)%oK0G2iQsM-!1P-Yj%9}<81`r4!YigZnQCw zb6K2ID&XC5IeEvgVNUpe77)m*=D`^8Vp*enHrvRsIYKJ51wZf-KJ)0Drj#fw=BwZM zzWE-Uy2#6Mz15Jw$gne&^YdbI7Oj6&ocY_k>JLr{++#a z`?dZ#r`6iFNp%Tv>bpD>LN3~N#eEMiWFQjHCEt`^EMpjdg=g#hLhDmXtx#_5ZZ~sbBak*yyreH&@B0I$nUi5zw5MEq6*3)D&O-(mz?z{P1 zv)I1(nAiPS@BDMrzzzK@+`0#H`Q25;c5N(Na>j;2CCy@wVwiaf0?8P6EmUAi4 zTK=%V#qsB@CtI~i%-UD-+F~nj?cAS=lvE9q5>c;6j3C1m^fk*w^vE3^yCcrZtXcC# zV7qg?qDLWC-ta7$A~`#Y*KiG5Wk_z_ZDg4ycXH&esEx7zbN{OqqOymp6vSQ}-u9IU z`>-}CP{S}|%fp`E`+N)4zA`{n6E+rX(G3$i?A#;>tM++>C;d9Eb+Kj76`@R$sOIoJN(hWU7Oo<*z7N6!fz!zk;n7d;1lr}W|P;fkC%NHV#6X}o_UkkHC2|^jSSl?5spgtA)ynUl!oBF z5qYGBHG5Y?t;l^ZE|$~w=9tNrcG5FcK%d6`#n#k6axn(JN+L}$Yqpcf_IX^uRxv~D z30DCv#wx_fbf1FQZj2p9yHfcnPOK?&)8qikSJ-%5?PAa-9%|j`!Orbwc1mK%aMK$D zVqrcHC~5RauN@f_f2ETJqOu{k!bq88X7;v?)C1aCvy>?7@QZ=+SdUaR`katK5VJd| z-81_NBY`S*v(nYBoYgwX7UU9l9`sKtY>)aNCZD#Dhp6ZUlMH#iugqN3;GGB<;t1CQB4jC}3UoCJ?2g^M=5PG-k z6}+__Va-A#m+a`nS~Nb^zJoct$9Mn5g72*-Hbx~b4;ff3dKXmgIi_)GlkE%ryYAVK zbB}44nt$o;zH>5)HN_6-ePI^L&5QXk#b&DTZP#*n;&#*d%h&x8=oq$1{P<+psBm~X zLx?l|k%agD`BlTrp0)JS_rst4mTyvRvyQC{T6$&S8$GF}ckq835Rz8w3u$f%xMw=< z@@GgwbHUA9)0-FfA`6~oo1*OXV z2;V~dv%+dqST;DUt8l}-z;REq{i9Tj7P!yVqaf=DZ(~?IJB>TdVF>r&0)K`=41^L1 z8D#q_AZRg=S}e#z6FSkb)jAm?Mq?P6DOwB>Av)4BR>e#hneT}L?QD}6w8c>c$%xw4 z6UEj%yQ@gqMJf75U!;FjZ99QS=qUG9xkQczUEA@hnZ~8`v6UR*BLHAgNO$ z5p>*Xp)UfC0vf`R52rLszJCnKQBH{ z-e}lU-ofx#o=gcDA=E?&Hgt?k2Kc#I5u?4=e zGzVqDf*dKz;Va;nGwHQjxS#gRtpxbB>`U?_HFZHclJKh`ET!V3&p6OnsOT9^MJI?x-f<65#SdoV&7ii-wmtV1t3;!Q*fnWn*)hR zzORU$O{`Xe85g;4m>ztj50xc?Zj9$3W%($riwX;Tydissv2D&^F_cSFl3+pC#ZVe& zS?XcTokTJ+IeWr5W$~KAA_MbHS=HDgXLB&ZjDvpNPDs6Y)351frLCA7Ip4$>chjIdn;RcvA{SCbzD!w*g40Tiu~A4uom%=H-}u`4i}g-pci;7tLK4KyXxMmXcpr>R%;I=d{_u#44;8V8eJP2U zce&*Hh3#eRlUZgoCnm9YLG%V~r{Y@Kj=mpwh)TJxFi)>r4_n{iS{7hs6ik$k|FY$gN8gbnR4HHrIOk#zWhvin5|e?M|r@G zVr_|v0pol_cFCH)!aXh(*2d-48N@v`4P2?z0zdFQGROrRgPRUgr`WCP~#&jDdc zZKGvv^N!ku}?b(syMZle0v1`?rh zX(xf?+o+Pzs4fC4NjurDG-hT=KDd&wUlb5@am*;+8W9* znS#5=L)z7v+tL$YaZ+36d^?Q5Wk_|P5$Kz)9jLx`_&d&xj>n+UcMsay(6Hf4C^Pg9 zCDSt|8!4L^PZS?iFKoiZSW(XpVvC{j$Zm*0MjsAIJ;Fh);#7roLq}CxxV}L8JY^VT zj`nu0d4r(cdKyYVP-v}1A>5%X?ueBg6-Lw@;v`t14g0v)GFwVUi4+)N7={gkpBy?U zn;p`%7J-`?mG$Izom|;vKhZjyQ151f))(N_XT%HpiR`)*@IwB^1E)Eu*U%~f?oT0@)p8N5M#xZ7)T&`!p~1I zu%#$;b8oG#0lZa!4;dvMf?Kq%E4WS8p?fab=5%R4;_g~_Nju*zeW~MvYljZ+sfhLb zM(HyzPUp7H&LaJGr01)vR+z(&o{f+(`sbS|RLu2xd;sg2m|OE!eauR==bNQF^jW+; zkJ`6Nb&Hr?=MFd9Q{UVMGvSxG;_w^4_iwTn-pr;-%(C#?FLmVI#{12+-@K0Zywown zN6bRc>gQ5#UgF+*|2D7lZRsU$={mHB^jpPbiTYJ2eL?r`=@oB zS?SX;UEk!r1h3Tuk7wDPmzt0&;JP&FcJJNi-K&^yXx-fu-W4_rulLj}t`h|w_17$z z=u}}5OxhIX{Z;y2FCb}eWt4q9zRll^%gfXlY1kR`^s`K#GUf6 zcaWhz6mC5fxohZ8<}k)7jOhr4krOT*X{Fp?!O(&G;`=T%cb(-PO>8zI_gwrP5UfY? zcKt0NEE0SkjoeurVXJ?=XZ`h`nZv&x5dNMyZ0j3Nul4xa`Fx;m%zhIpXJjnLx#KkC zkslBzVpKWkBg2LGdI8=XPPkDzK2kbfDCPla2p}Ft;)cMtvRw>JihUGh>LUq8F|1y> zAR#ifvYpCc52k5dnQsr`jf{T4xiQ8+XvTax7ZM~u-*Htx(H0^UQI>~}sV!xrPD54g z6*}>^)MV*M!?rOyo9XrS)Y%JyJbg%lJU-+aOe%@izT51nFSe2U`MAHtnxKu1aN zCZoyItcZFTs;51u`-F@W+O5-!d^9uugg9>DFKcHup32K0m2Ei9OqEiFJjcf+lD^9) zW6snMBRYS28Nmz>!f6t6HS>qK)dnOpG6*m0Ryi)&yAeTxknl1KChB_963?W`@cmD> zB>$8UAj4OrrzR2h@Ba=yxn$SRUet_?TM_DR7Js^@nJ|I#GXh5I$3?u-Y98i_MyLfn zvZO6s9sw!Pkw-L?Ge2Vf7(YYD6Cn~-mo#Cf2}$VNz>a^^?_^(l+ePTGAzZ^lBK3sA zB7#;|HAG4OYgBN=63=d}LffE400O$hnxDICY!L&V42jGDp z6az5RpK*@^VggVEAVGlVc@Pl5n<9Wq$zmc%B(CbnW;H~J5%qlA*f~ux z@G|Et$FkhV@k*->^tP4VY)vBB$EmL9{!awL*v!;F1%!4+7#jQU1p*BJUm6ggBiern zgsYD?hy4-=$JQ(TDG;doB41_>MbbH{VzsSq8e=WNcrb` zGuUNXuz{Oqn1p1tQd`pg#8>O3-D<10qZ{)B!EW1)m+kcxVkv_u+bVlfa?WoVA&RHV zZtYKY?!Fjz6Ll(BR>}}+$d99LpS>nlYCWVP@3US+DLR^xsD$%Q(bEpcNSKNTI!pL6 z+9`UI#UuMSJ=hpd!P2@Rg(oHjjft&WYBm#!w`cOC4y!y#sfgNp>)Sy%L35-M_bo^B z(op#=Z-Qdh*D(sCQqWKTw39~S(Q-p-%lVMIlKShN z9=BW7UrFiQHk|vmqnqpCd2T3dZrtZvL(Diw(c>lhj4N$^(o=d4JH@MLcF<^Qc~Gt1 z>~MF%)_l4w)zyYm+4^HZF_Ixiw~`J>P3m2DD(xtxh6#JnQrPe>HOiFF zxMKrPR9_2{v;%Si%z2dDNyn?d1cLW3flyV|=a(CwVEREg3)RHB0)a5yD}6A%{j*=! z-wT8ye;^RdEk@k<=S%gk%k2w{@yV8fcPamu1%k_MfyOzdO>1Y$o0TP+Pn%Ppnaylm zAC@rg^0hNFF{mi!{Y0PNMDr3tS%oNu^d31(P8M(yb+_qeHnENp`F)3Nb+j7x^&F%+3CtyqE*smlo2o+W;b~`Jq_SqK0 zfPjNXEK|k9YB?45eZvaqdr<`vkwr(t0;LP)uF19y-{n1ab8GrrK)8GM_1Em;d#3|+ zc8(aLO+MuK4o$c+T4o7GRN%~q&xA{ou;BrPT zM~)+c4m%*>aIC$L#VG2wlHQ@dvVA?x%fu;=EX=^)UysO|O;BAVB(=xcO06b<&z9%l z#a*Wx+xncTtn_@YqZ|PA7=u~W@ zO&!e((OD^jJT~cqhB2}~eMBYn#Jk~|_o579b06;l?@~}1E|rPxL=tXMMtD&c-hCRU z$oDfn`~4AozJ`3_H^-6MD0pDTGC=1+I!+d2dNWcwnE8N`%kviOvY`9{o91Y)gLOmC zO5`ras8(~L-EPcMj+0g=ibp{nH_(WHqK2F1Y8bDNDz=_Oc5*=rNxKXETUXOpWu_h; z*%O09AmNDH#$Gt{w!oIi-4VB(`oF2Lwn3YF${)5j`5!*v|LSS<$vds&Y3y<*P8`=19A-i8l!@1{Vv-%!3@xLWvhVhHJwWpK>+`8loCMPU`02FGGF z9N$11;^|kvP24CmSJxC7%{*-;;eu~v?LYKE_yq9+v)AbCm)8wbK1%M#v$hs;MY>_S z&Xlcpc-4K@Niz8^Y)k>_y#DH~LC;b*4a$mbC%GTB$k(ts6;kFWu|xOZHk`?iI~DQL zs|AL(CfuO%(AT3={Sm$RjF>*{qfw_~YCin@UkZez+JWUBGaWm29eoPfX&K(t78^>L z(x0>%x{pQ}p1&|Hrgz@l^Qfj6nfAT35Jrtv6!iRmDG+3Y3|J{l)+|iU2V581$TU^( zdO_WL66>v{c!syZOl#v?MeI=~&Y7-|g|=MKvRCd=NT-BLKRoXTW92Jt7_@CzNAwal zf=X9hMvpKtMy?=3dQ_|k%>UbNH=n?wps*3DqU{h-N8oFht^(1ciYAa&tf<0cGVA$K z1rta=-ROO0OFV<4ZyHA(P*T~_6HQl&IbNxHlJ#)#=e+<*_y*UD!DolK?CN4Y{pySJx`!HX)V{#X9j~aQ)DICUF@Ht4iPc zbuV&TE+*CtCzmG{M@BKyRqy*Ks40ExF)*4;3G!JV@kGYZ)!{9+89+vN;d=!rK`6~b@k9p=By~-ozvdw+5OO7nnPQAWlTZ;;mwM4sBp%q6 zV`d3?ywySw=n##?+~^5ev$4<2jA^6|=a!?aso%}ZLHY0z>+{CfJu9T(+se8_?kdzg)sW?fb`M$J>uhs@5- zQI@Ai#@bQUeBetb%Wm(`&mmFbwvW$8%7HRiC37jx^+Rq1Qk@k{^`$mz(k6wQWo zDnC8V4;eg-Dwx6BP-D-YhE9N<06+pdqb3O$zA);hYb*ETWD=hNyQ_8BNzhwdynxo^!uMsi_Z|+@| zSKWUiQ)e<&%kaq%;-Q?IEE$Ici;B$iO$g%Bl+15_ z1xj&<18)f%nDJfc%n>c(r}lgkPe_JRAP_ZVuZ}~=G4fRqXfIm+)4Y6HMv=iW$U(2z zI4mV$M155GCNzuJ%OGqZrEM*`@-srQm7OP|B*~Hs(S7)2rns3NVFRxu`Yr3zX$Z?k z)t*%xEsi^I2KT@MUgRN+O5o?>h@X3kf9$z{p(I8fP*f_h!cY(mXJ7+oU%9^Ajg$Ci z7P4Lz*SW*^;_Vkqz|1tQ@LWzY5|Ja~L^>ccPmproohf;W!$Q=X_calpon=pxFvzpF zpAFviP*Oh)i%XxwF-e#WJj9FFsJjyE_kOHvZ}E3}3SOK>D=txgZjSq-F}GjOHFGPJ zGiw&xA@g_u4@1cBX6DCcGxSB(H|s`R%4YO0N8BB^$QJwRa{sMV{P!!Neb_lDui6h< z$;K+hhgu5Ioo|=aRAP&YLfy))1Cwy!ZSs68r2f{o))wlfLgr@<29Yo(45y7dluq-r z%-@xVCf~*BRYV{%#Sc`B5VEjei6_Xo^S3H4SmG9fi4DpX;Vwk2Iim}fI^h|WW_=0P znpMmlRaeecrPrvhq9cbhrdrBk~4f!5-B^Axd z)%X8FApE<4P}@;c+v%b}WIcGgLw@U$M_c{SQ7(@*+d_9T&#gl|`Z|EyC0YqTr9V3Q z2`5Vu6T6HelO4?|RXv$5 zH#R_Ol(IAP?b_?xS%+X#sP$$@2eIsNQ3$+_QFrSQ92epl%C5xvWAAj@hB@PKrZTx% zw%IZ5*auKWt?j#&6k9=?R!{JLa?5_e65#d%-?aTCv>adw4d|fWC+p5&?~~B4^p)1p zTFZRXh+ZPh7`p*YA5gwX$-T zF?S(>=*&Swk7OOlJn(gGf2>XNcKwAGq%mL#yjJrv?n&RSo`bTkFgn&u1_msFgLV+$ zuZ-3Q3Lp`s6}OXkiS2qSZ%;wR zSs@<#BudhH6GSJF$9`f(W;?Cz1NpwvL-0#d8a6)z6%|)b*upKnyvM6*KU;=M(zgz54f`cP)OVsejpR z{bj9~$787uLo(_qwXvvI>IfbE;arD@F@odzCFqwx;AAf8^6cg2N_G`w4u)q_vaNmx zgzTZXx}k&*Ly3!x-=QuBEN(K&mEtws?8F`8u2EwQ-}D{28}g;l3NO zf7+Au|12Q<+n%Id%G)1xtd)C(dpI^(XR6AIG@{FtSaht1md*3V>)Y^!R*)YQTVEvR z&+ed7qsOH%BTSS(6#FVxu%3f|#1dQ25@VTlpx800cLjw~kXn7rtxo9jWt2|nWKm~3 ze+HifRCjzwC~s^^vbvAf9|E#s$(VG7$jwcDNQRbC zBc>LosT$wqFpwWB;all1oIv=6H(NgWIri(=&L$9Dv3MB?!{ z|3E7*-Vfau#BFVOb10s7Gtyx|ep``@KTV*O6Fb5jmzl@<3juz-7}P~bhp~l@4SrDc zBXayhF#O14`;w`)&cf39^81t=*@BmNngMSo=1S-d-OC({x z#GRUxUBC7u$s79j@cQb>gRYUo`9m72CivCr=m-Dto+LvQ^4|=@X7o1*3BJXUDU`)$ z!cOiBZgmfrE3~6(8E#a%lFPc1&C=JRv~XW6a)o4yX$p&_ZcXoKe$i6>qB*a2mM0`z zSf#Pj)G7Ya8N9lV7abkKrPki_ywS9bEOm=Ru^G+WYIZb>r(;b|Psy6^m{xITm39nt zmJ2(|dpc^_jn%>*+-U_Wi_7KY-l^;?XV=vVXSsYODzk&f$t|gDsb+J^g|p40N{;Ey z*1Da%2YNd6+&gWiCRQEQJss7o>T2Ow$0+QYzLBEa*-@*bOLunn>0lStRS!*fG|O41 zJ5oR|uGQ7`-$?bTD=(@8)BEK;*p2Cq7Fr$6wYsE3j*dRPoF18t!FJYK9UXZjtwR)7 zA8xg9h?5|r7i(p$RoB5b(^D$#VB1+sX=(X%)Cz~vAIO-wcGh+}Q&l^-!sa}IsTp%u z0y{SQL0o!P$(^>2>Y)~{aH$C`D1p4uQ7bkfSjW|SBUSg_1Mr_X)Mtz}uKoojZThC( zrWP+c*ute0I_NfQ$lkdnZCTk5u4H$nFdn4jRz_Mdw(EI2x*pO=U|eBU@)}Kf`V0#7 zkeQjciRmE|3N4LQYGUSUs^xD+u{xXZz|`NHcZ89}VwJEfE13_h9sMgS86_ocypabK zNBVlQcWG{I>4Utqj0XjGdNxv3qx{mTDpQnXsc-VCO0&Yo=6J-dR*q>FlnnBjc@QaVRSzOUulvu0vpE zVP)s+&#mj_*3IYMt5vr{{-#=3MaO<-N{czKwO$ zU|W~7otCqgR>{3iXD@GOJN=Gl)4BILqqCcJ>`toN9Wq(t9i7`;a<5s%D+}C&3r97!GoIirT7A`nRu*zs{I|ZCroh9&oV)$ew>-JyD6d zzhw_NYy$3<{5Cy6|NLcofcOEd2eiDump$NGNMgg&-@XS_GjPV_H}Uxg-va^}z&*cZ z4`4muPswlD1M=Me8u$EWJ^eo>f6E@QwhZ8&T^9boPnY~9d%$l>+`U@hd%yxRKz#nf zJ-p!K7w!Sj=O1v-FR#_?}^W!yThjcL*Mhy zxW{mf_ix{0XyFZf54dLnh!4;z!0tqr&_QL3%+?2G3N$s)l>n>(Aq*H75a|Hx0oDg_ zoRqf@fXe~&2aq73c>oUrq6MfOfO3H50SpM}831~K*I{N40gD3~763iJg$+P)%9j(8@DDxZeT@ z6c9WB1_5IPz!T6vAlOBRumKnXz6X$_$PhlzNI?96FbAp}IAa2!&+khnzrHs93wiE8 z^F3$&+4pR_8TkJn-;?~8@0s!2tnsJsIS)0Gz=E(MCPVz!f-qml#~6|*VG55RrZn>} zipVIxlk7e(cQ%v3@K`|Yc3G|`>}SfCbL^7V8SceNf&m+f$Fkf! zlA<_g8B97TbR4*X;+j#f?3KgfCH-b~`)urv7wcS*v6#XyL2XmsthT~boZ@nT5grw8 zoL!{@{;`4R!j%@oSrJ%mva5GfLK8OEx0JBQC3Z1zg}bu9g=z#^OZ?@{(-K;)&hou) z!YSHUf&=$!J=i{cK(AB83eS1uX5pvd;Rbw9xT!!ZMlJUR1!*IdHd2yX+^`*slqRF3 z^vE?nNq_ntH_n-FcH2+-7%=1WtG*rN3JIf?xP|0Ckmt^&iJ|?6kJtu5)XrL=!plgF z$uE|$q}u4nr;0^|ke5i~xF?yIU3wdk7tu@f%e>{`*_%fX!lXj4^8(9>*3IHme$AD) zyIWNT)nB%DkSg?Fs$8i4ZqW-B(rYR{ZuaC+;KTQJJC4*f3hG<*EL7t`Lm>T)1NeqLi!|TmNr@TPy-fx zcFA1D)ha&|Xq@g*|0z283Ln~=6XjSNfnQkKq_11H-%vkyGOeQdk$+Brvs6FvR?k)a zd48N|=zSMse{KJWfgbLH02tLuh;1c0rm^ucy(O+D03g zkI8+=$HdQm^1Z_Z)Tbmlu2WTAAV3*!oD9DAu6_L)a)itAJHhtTd1JGF8$H2-up-xd zy{+xe-m>`E+g9)KYXm6emXwqFZQ7|voT_bj6W+D|+JdmBN6t=V@}&yR2KTf`j~>rD zW2qrX6QY*0P8@x_GJljUaIftnokd;Kf|qO&dDJ{6;%(=5+ALssVLuF_J*4t&-v>2^ zDs)|u{7)BzIXFK{@meKwR_Nd?PK+^2KWT9$xNVK^fh%b(-uY{9JE~4HaT#@{7S@~F zlqT8F?xE$E85@jvq~454*V#-m-{45#=V13<+JN71DeyWu7Yobz6+bCUxmW6Z)v9(} zgG4V@Z*xvj2tfqVFx+jQb7QJxYvZHE-eN<5DF!_K&qevXNz{?%to_NXC() zLv>fOPa+zSDg2&LUB5fai*4s%mrS`@iu>FSX#Rf?_ufHGybs^*rjUf11dv_=qM|5* zHS|zKKuSObL_{noAVLtOmxPwkLs5}t5Ri@_h=73fCSpKDM8Kew&;tS@EpNW(X}{+@ zbI!c)`RDwT+1YG%GWSe&v-jt|KGy)xQ%w}Eip^5GbG8-i+Z9%6V7Z{Pww1ucKI-`9 z{xhZfTED@1e;>&K7;!gaF!+5V069J^3nRP}sT7!e``j`Vr%6X0fZnV*Ub|%d7L-tG zxPDB{tl9l^GFWMB-@EF+%Kl$JjBnKaX<)^@3T#dO^7r7ey!BnOKWX4nLD692&bP>r zJihONvQH)uU3iXLPQKO`h#5Kq4MQ zXY<(PAJnJKLfV*1hDd_v`X1o-4Rs!eM}DcUImpGD!w8(1KqJFjg+MK_;)GjiH5YAO zWb1`_@PvYEY%%=t++s{B2lWB+G~!xK;hN|bZ2)1`XAWv2hKIz;vGcfwTXZIO5|rNG z=DoV9qStbCLi3b&{yy|^lv~Dh>S(ioFEiTt=TT}0PdJZJ*YAw4B{M1ZwSep%%kP~0 zxG;-GA4~i#5)HsYiNwA`hxIT=j-eEUC%(`Q(#52-=0Yyj4Ia+cm+Hl%RO;(KR-8fQ z#xQQ`lR#hEzO=$AIMnt1pyB3&sPw2^q}&dKyGEN^I@&SPgO%Kysd zoe?g)2nH(z;`&k}y=yx4;YxQk_RqNgPOHcily{Wib97df9vPkT3(4~lyg--rwyp6x@!7|T$A^03L zN`0s_{`l!!S`oBx3m4hD?iaBG3EypkTa|miNhjgcmABup%7s*T+GLH|Gz6#qx8gaD z#^i^D*2SlP<P(Dgy&lT#vEP|Yi>P;f=){Rz^6F7Y=|r!bMXd?U+MBb*Tumun)kND=Owcn(YE;67SLK~uu>J9`^+6dPA?>ch`Ps!kldN$V;C-vaES&8h)oO- zT8lnL=Ys9Gi}sfAXahw#>(*~Z3;sHo*T1j*HAIELb)HC0u{^5o2@faeeFmMN zwkiOdqp{MKentD<+zZeY6N1-K?HnoAy(SsQu1V1yuwPf8ZqjM%RVrJ8}k>r0Dx48k0mN% zf+ter)l46h>-fCpz~Yc!VxWO$ z(38U`MRCC>HSimHYVaDD8a^$u%SV;SWlKZePoz9Al(ZsPbhhzqCv(gsAT|}6rY5?D zfiImpp23R7QXzqgpd2;RfyV+I(C3CAXzi3}(`?%2%}6;%ihuWoK05R$Bdu%}T80zN zVH3GNsvXB|BGpyEGSu+vHKxtqWUk{OM}ce3*+?fk)XY)HkOTUZXHjd!PGA5wDw6~O z1|qo1a28vrtfl7gE&Cox7LZEUexIc?c%%`FXI%3kyXG91O=1R#Hy`j?e0y+U>5P(CYJD)aO3U1z=0WSv-ttf^x!uO$gC~r01v5 z#j}S12oAR6EP6UEdZ7!PLqO%AKx?xo&Z2yI>T@d|q3oN%V3qh3qUhpZLlpwF$q!Xl z2XxM%-VH{7G!ZN->yc;_-WHWo?lAUguU7MC`!FazIkcuG#ZLK06vpf4Iry8}qChuQ;*&1s{>9{%u?ZDYA1-|KgJhmkd#p|qyIS6|mTpQ* zzyD3tf;0AZk`Pv+L|w>v9IT;MveuXDZ*+(q0s6W}GJ*&kp!)CGK+e-a1x1n~I4GEq z+}r^+$3V0KszOy?T;zRA^sIb9D0nz8S{lW5Kke;=XWmg((HYMQxB0i)&D9P5LT(yw zBku`0p$>*05prUfCw{Lv(XX$qoAl&eZ3d?xEI_7}9`pRF=ol(C=bq^6liK+jq9TMh z#hijL4(?M9lu5pG?FSXzd*^;bqwe1d!v1GG_r1PvyPnR`n4}9!674=;-4_(lfJejB ziW@HU*EO@h3Tcm6JOZkhMjBi%!c7TeW-ix0z=mB8N}F#m;j9t}I9q$w;^2k%JKx{$ z{XZ0h{m*#rKQ$(l#2Sp0Ab^J&YP4{0pY50yOhwDlUi6Wjmi-egCo<3+#7D-n72AtG zsN1Ts(~6aA!Tkr{^Y8K8Xq)*?8(ymYf>Vm2_-USqYhXP1OgCCB_up|JExbzVyu(FW zaT}*=C#pq~z!gEym+Xgeioit6!MEJnbZLP9(RQ8*0AJq8k(B77xCrIQ2Yzilbch98 zrISA!uq;Pv0dN5h??Z>3E9ulCyywyFZqDZF!&C)I^~l`rei4g;HFf1>^l)&W+>#z> zKcsY_2SVx*U5A@f1rURfj~pxv4~x~n?VvtLuKB5Z<1S-*B`&=KE^#FWwnzVgY*AFfQ%5#9fz3)*?h<|V&b`$+nHb9^O7g?Q9H}Hvo z0UfyxX9nPJ-?0E;@caZHBn$_}Ow4 za+%P>Cc;h~>&HuVSmHlxEPg4f2Md23%9HAl*hNCjhVfF+tYh5Zim$6~Kw9b0%b<~h zF`e9U(P;IT6JOX$PVqx>t3W(Ir2zmy>;$7Ac8*=zxQT>H$a0YGjvJ8MASmq*5=Fh2qY22Dapzd8)Q>FgoTDNlkuxu3r6-*M|Y z9FH-`@=J*uurS9QNgw^Uc<#2t^c?}tO{Uj!6lB!|xtVY4HTC@MNAWRNDRQ3P0AnafHp8tyHI2x1qefe2E;LD#3$ClRIPl0d{HV(3- z{NWnG;xltOPZNAsWxtv)tV*1f!t4PjKq~(t;j%0_I+vjA${pJt!6vfU^%d0f6$#lM zvlWPyO1j)9i@+Ty|Gc>=xaWl%yxM#@kO6%$FCED4Y8?aSOfZ06YH4pSU?MBKI=u-n zK?V%myl$nrG1yMQs^oOFf}KP3w3@?r;~|y_nwv}d2b;rTNNyxjFosgLZwKPUWC_Y{ zNF;LIC4h4qo9^Nxca>3;=jgjC;8UnS$iQ=S}s7C^C~0t)}8G5NQ6PM?B4V4iE+|MOg|_-Xga;r?6A z#Q#bg000F%E``JKxEs3Z z8D5I78!UfY|5N>8bzN^y-(Xf&Ze8DCow-}l(Av<_nt`3~6K#9fthTwHL5DjJ2aC09IbKx%;$_Lw+``Mc-tp=- z?cgeJ@4%Nocd`Z+_C^-=rq|4^-LwtuX}D zQPk!gUBc0_^b9NvX>+Q?Zi|QrTU%Wk@Xnat*x}TZ?QQI6n?D>;cH`JdmiBg*v;zm` za#y$hF|1GT?YthSQg?`5T3XxM+Sv=PxAx{3-`9Tb?R0GPYFhgvMY>cWWL!_~corPYPa;bDS$#^xq{Zk4&S z!*oniV9_(CPvr_~)8naaTMJAE$83_s7%E#xyD26!NTe;{(yRxqw1=G|DdSZ%hq;;& zcg)<@=J0s0o`dO#@+fnqw-Z9ZKl^Z*5{@J#RQ|VyAChBJlj*bR-Ag+{h^${Z}2y53L*e|EeRg=j9Fu z{g;l!>uMT%%U8zCe~lFCAD^N8f9Xg{ehWu(bR-U^(TFXAyj(O64Y41V`41h*A4OfW zX0P>*WB;Ke*}vh950#w-SMsreR}KHJBl+=wPLkpJQqcW#`>fx_zw1c6XJ?qn>2Po! zszYt>Kmj9^?+lZ1^Vi_-1O(Vm1-Bo0r(hyd%9|-?C0K)M7ZqlDa)l}}D_Y4%tC>Gz zR64MUhB5<;z>V3N=S0{M zCM{gVm6=UBSj2o0duEQALp3}yo10?oI-8ezr)c(NhWFfTJ}vUdTtV&=*SW%imql|$ zCDn6t#pNCBBl9IyL$33sHPc1&W%V0#^RJplUFndz=grDc)*4;uQrO-+ z-h@d{d4*ew&bU+$KE~=Wme+}O@5av93)izxVZ!)4a*yKcPY|KUd(YWStXl5xB9?u= zAi0Zd`ZMRY{zdaNqgh}>YBd3>jT$6EmBQCL;@9&gbRidvQMwRbL`5s0@@nwJknhZ{ zV3(jho9`vgXs1%%NSMbQ^uy$um~NAhX$|oGK;E_?0^2DU;NJ%*}G2y6`2Z1Ho&m5|7}e3nWP> z8&Q?VQhx~ag9ahjufc}zCN!*FopvAS70TL4;9EcNCajhhrPwl4TnN?vgZ6A{1~lyQ z8#Y*XXSGc9Y?s_;OjdBKBJ?n?lI0+U2_pPp`HRW1VA8 zxTgF!Xz@U$(ufsG?8FqhLSDW=ACkIh5VU?PnpFHV0ahL==jwU=7pqCSV6$B`;s)2n zqx;3!x-hVGJIVagujJQFq`K}zyga1^yQH7OgHWo4xtkf4bS9g13G@2lxwcaFhR6X}aRQo--6v z=e3;NB4i}aWWZvPCKG0K-q@z6F!_$2+0k9H`j&1&>d2tgt|D38z2~!WJ5lEJVj*5S zf|4!q#e529V0k?$>H)A`fY%xjUGqe|(rt6v*(4uO9!S@ZRW5SeCn%`M;yPOg<-S1R z(rD=!l0A*#gA=9n1BD`+&|PtN@{EvJmC*X?^;gO~83=_Y$^)zdcs~m8o72UNfi@~k zOunicrd^EO!CVvLas7q)@a5uGPRR9TGN*$btecQiiODFj&;+TBOogs)zH z&#vC6K36m=r=S9#nViNv-7USruCN=G@G&x%J})xqN)_9IROqlG)gNvpC`tXPGfcao zCU%3G>0phCMZss?V#Zb<6cnKG^70F+Cqg>D^IDJ{^&urcN|vcfuc8! zC=z=FP7`*kv_|U0zF<6jf@<>~Wln|GZ&b784Umw6FgR6(7<~IOhJRB({Cq|>^5oBl z>?X1=_lf-D#6vt9Ot{N0C{8$evt#G*d1BZR)W++#m?;X#1%AR_O0rf5q)gUwPP-XF z&yo76ax{Sd0y4n%udIpo>nC3?I;9ExDTd9GvBbH-2SmRQ&V8Q}xa0*6N`|v#)FLX= zD|x2+*MIf8^i@7G6depMc~<>m|4FTZ=Gz+IA$^)dl`lj;G9UdF{}dNrm3zo+*5vBe zKx$vr%d;P6lRiGATa3>>g2Ybk)35uyK>D48Uc9rQeFby!+TBm^k#FB?*}tptVcw17H}wuwTM5p?PX&28XPplg-y&NbWL?Eb0cH_N~3t*qy~E)D7mF zuNPh2owNN^H}tk|y)58gr0H5OOZVXN!2uhU`}MvAT5Ej&?(S{Cu9MYxbad?fQu=ud z@ekLB1I9e#oSWZX>rIHPocOqODb4@yml}tiuX0G|?yLVB()4F(t%6`QrG4iHcO`ij z=b}T7q-+0iD%_QFwb+j7)ka;Q2k8d9r{XB7f1Ql?4DaX!PW|!XZur!Qqa~oeFmFU& z=Z3P^BA%nQcAC|Oe%!?zu?axK-bbaji_936`)K-Zb)BH9(SJcl{j-sR*W9Z#+~<> z2ozaGig|PWu@MvoxRew^nGXROd?*`-@m2w@sDKA%M7#(j4Lytvj#z~TiIs<)DmW`f zh1MKGK^);yR4yZGh=<%wjRz7SXGu;L!BZ9P_u0mVhV!O!iQI&TI7Zw@hqG0}*?4%{ z3Pc`-&;cTS!vfhvkX$$@kP_rNEMl#wC&mh^Y9^CtWbah-_NNq)1Htj^E(%pFDhcsu z@lPll9}$C%k|ziF9S903h>{cym&YNX+3>cr589pvJl_lnA%b{LqFz!$A_V>v8kk}Rs2~DxkdDN-BaC3*0x;-`5FZihN=ul-QoHWP z_u8W2EQBIX7c&F8_r>hCiU5RR-`Na&OAS~U*8aAM=n@KaX;lzBIm*bXd-K4#Nk;(t@X{mDMusn)e9 zLtoT`D-m!KG^j8+q!7H?pGM=p{P0;^TGX>8=f_&LseCm3B60M?*|?W}>6t(*S93yo zcXCW^`hBPLM&q<6MyMkMP7%F?`l(C-G#19DX0Ykp-=8Hf6~KRNB4^I0*&a@gDNG-H zX1So4%$Jxj5bB{Fh)C?LMX7faqq;Nu660k2(RSaGAslY336P0Shi*K>m{`eg z(Xics>Rk3{Vft!+s$NkJ-$d%_$_scl?Xc^Mv;LQsm`}fnJ%?AMp6Nl?d`mN{%`~&i z)bx8sR6{YI3HIrxK@#%}>he_Ev$8`j6lb9ZCapjTVYP;g@nz*Vh%3FBSyP{&u zw_<2_jBZr%JKy34|B^R#C306vmV8P!pO(<=O6!jl_obEgM3?qRlz#Fr{p=qw=33e- zku+jb)_SCjnN~VlSN46RWK6yE$C1)$^|Jb{vh}&L_K~tbb!9seDI0cWgZ{5pd&(yK z%f4;}wAPh>*eY)EkKV&_eodUz0w$0R;#ZAN#-k*+(PAGe{IO4xqe0iNB&-id`}r15 zY{C=NQM^}+|B6yO#b54^F4AUyOUp5NoFnnPR=l7f4P_@GD=~u7Dts9il`dizyZQE2 zvqXXz0U9Xw#!BPOrK@i)D?PE6d}Evb#{T;ohkI|XUU+jO{f&!nU{`10dZK4T+ta|q z!EP}@Zjx^wXuN$W`IfL<{rc%!pQ~@Z-o5p^_crju+kpAEq^mWNoXf@cw^1=~W8c-p zZ`UMTsEO35`3DBNT9Xu0OAM&Z;H?e55ER4vcKGdEcMZ6$2I^*QMMTu=g};bfG%8Pb z*21$oiqUKE4R}#Z9c2?PMT{)jh8JsaOKTLe=@ouhl!D~z?&A7VEVl#->Qr9eay5QX z@=XW}fyE~{sWkXeAPIQbEjIDpg{!focU6e3oJ;fkSNlfj2zjyEXif5h;4>hD4(7e#ZT>>+rHU5ctTv!Xeex)w(!{ZMD&2|BA z#E({AzmO`a`NmeGT8^(pCbs#8Qj6pE8(ZEK++XB*lrL`TZqpjqc~oeiaU)jht;vV# z?{A-e&kw@!wV7OK)(mXb$Y?z))powVwY95F$F#92waq*u=v+zb<=(bSrtR3#R>Pxj z&%3o>NN>kyv|iUtvMOoUj**v^N6om!(E z0VN%_njJP7o$kHuw@Ny!*@0b=y&ZvponA3*5hb0EG$}W~)yrr$d=6+ZyaIze!f(-| z4<3DU>uSqQKVV?HxhlOr&$LHjl>AMy!4=m~s8nyO)Ewgo$Ce{LzpEd9_W|;^qqVDb z?A!aDm=}r?1wHk>y%{;@i+XFG^=`j=Pb$j&&>Mfdu5w2s6PA(OTKjQCs*mK_bcCJ} zHDBE`-(!_dmgbbMW%NmK(uRx{snMQNx1LK%0lLP;4g0v(%J027)9T6!@}j}H8fspq z(n&NrpIO_3dvwq5bioF?fOL(JR(}L%v4Kf(gp8F?+j<$*&X)M zf@jhbcZRQ4btmdNlahnJee6zbAH5p*Em8X0H$Ke~ub>g?{cjDY9Y1zAacO-U%zf<pYQken9&Q-z9hDYUv1UJEyw+59zdsq#SV%o-JJ)pF8$r>DrHt zpdXu=Keifv?2P@`+x-E^Fu>Xjr~?BQ%s}-0V8#8IiUY0Q-!Bk6F5ob}w#%3?8|OD4 zXO>P#l}X6#O>hVyarf~rx5h03CPdsPBpZpD-I>CT#I=SWhm?Qfc_$PaCq%V>oDLr6 zEgL@+KW-5Ns^oi7rO>yXSO&i&`Y$4O&>H^;_*fwg~OvVNi6CnfeKq-7?q zX8pQ4KH(_vQ+R2@$zj@VX~LoLmqg>VtNAbUzVWd_q|8g-CIh4;1FC`IQW=`@&zcF? zn+cF%24u|yE( zD|q&0)@+9IT&%zxEq*RXdoHhUE<0cMoyGE|Eg@ZdVDTP zdm&d~p(B2wylf#qc%iXzt~q{|Za)8hoLTR%*d@bs!7NfZ()6sEA2Q5eOVH^?h=0~n zY~Sqk_+m)bEGZbY>@d6BxAaSSX|rsmOJ$k31a)C?&15Z2?=AX`FKW=C%)TWvD(pue zYt?)ib$pSj&D_ymQR!GVXR$=ttW|fGXgNeyXJxh#B7c2#Kawf^W0|?fT6SmtP+ne@ z;gV$|fAg_tB%ZH|yqZy2UOsBEqHQtzTYHJQv~2LhX^3qbc_S#?biaZOV*#5COYe*jdP|6D=U37VFIfGS(l4o<2BH`vv}lslXRY!%YBO@qbmpc`;nWt zi-RvORKMKtc<&UTgSz)kL~!7b?2A7>)%Si4U2qbS^Vhi{s$d)%>o`Jg8E;x6q^=KK7t{!N(bUl;P`(^ne>9x|>v0>M5rr}8!{ zK1obwZ*l8xdW)Eb-`T455PxTZa;-hz^?tkS6{_<3_Iu&h)aQ5hpOdP8FE$E7j85f zp>8DV*B<|WOB-54ehKW88tih+d9VwM6QUG6Zz4%?g@)S>Ot$*}mo)ui+DWT63YzdL zHdPUf#GBw2T_yau%nQsi&uEFSc&kj+1W&;RG_HJ^yIgYRjr-KdmwB7=8}0E2?jX%Ac;Iuf8Z#43B#9N;gs}do# z>hbpK4lV(GClq{7^w^#=uL(V1HYoyVZBThsh z56O6ah~?R*UGECnuSW4s{r)Xg^;Wdm+)&(b0nh?N-~vMO7tUL_>b#r%^6Ku7@!jpZ zskXxK>p%8_cK)s}&n#ElCn!P(&OZ;?0ueHF*RoAQ5%OF4WKqKQ`A{8&&6@xk(ZZr4 z_UN4uB2PU z|I1$go5M}vhqaw&!VWj-I>Pq%7UswCj3~Ac*~=$Xg!`cqW=)83qYK?xA-yMG>6o7U z^6;YhHzj0giu%4sm#i+FUca>KjRN7gH91udiq>&@q5Cu`KYK!sn&dnee(-$)5Zw}I z@W3lkH!=_cSZJGWJPlKVKkH>YQgwp&i`_?NtH??B`70AK(ykGgt-1YcZo-57nR21W z4OqC&kekC3HimnLX8@k*yb7FvB2BPTcguLgSj)h;XIEq{)vGqe+T2uW3iqy`1tResx(ukK zHZ3;`X%5H{1qF#vRDM@wzHEJg-Xm9bQE|0IV{n%FyldIJjjem-s@IycJpC2#Qe|LQ zNJUu^5^bqxBdRZ4gp{)L?s4U-I`jwPeuM#ck$&$`KD*J zrHsy0YrwbC+sYy(DcH-s(!skwi^2>6cekuiSblzxBE)*g_ zgZ~l(;PDtR`psqj!#P1}{DF4DOFdHlO&7LN=Y(s-J6T zzb<8fG#HHEcGLVw+h)n_g7ZRr_{=kd`vGYH3fA5xnGUvAo!rFv0vsQ>7>!FO<&xI4 zO_oXfrMK^|h=ldW;xcNC|E$8VZkMcp&LbR`sZ|D0R&*B4JblLM~3I`x8k zlVvEJp+YrWz(`LKz}iP{qC9SgDrghHMkCfVMO!`hXI=aJGq$vuX=`VfEA#ZAKf}aw zbL4;Sl7%u(#VelDJz{XF)49G`BHA_=Jx=Ws4aA_n8{Ux2INa%Ed+R%CRKNbGTK9!c z{g&N$Lfy3i@=3{J0sQ^_{Mup{O-`UA1l~E`iY-2`?#+OZzqCqDZ$_D%&=scbuy0GL zb3f2}C_kw_rUKh`PP;#KKH;I1;Jd5SCx&T56OXO`9w3ehK5HFy+E`Gw`ykEl6?O62 zsZKRFiwNUOq^nn5#=a>Wm-b3ujQy#T|E6XAi! z*qgNTMtRpzF+BnB!-Qt96|+*WlCTpSBEBAjqb@=DS8wi{sqweAvk(0G;K!=>?Nr^} zaxV@0vF3*A+T~2o)PSfT*2)%7G=jY{Y^IK;8yZ1hv(N(s13NMARHz~9b*UDY*|%zI z1NkP9Muz@Lwc{~`4Vmt?WT`1z9qg9|hZhghCC4|i{*?XDy0dd;vBqy`%fq5^N%QLN zlxn_4!2 z*g?x&@d<|Uc(4>c>_(^V3606s6G$5Iz?@1k46_TCDR1RYwt*KOdIhg1Z0lH|g4(M1 zS&gquj7!a?SMX=22JZX)(X|NPeEMVk*WIA*C=KSi{+o@PceWpHzaM39S5;qlZ&Tdx zT?0p{)lhm@Y7(OZuRle7fkkdYPGbRqP4eJuA{qq}QzIV00^(v>ObzmN%dmH~^-5x( z_cx2fJy9Pq)&9;(z4z+i3Ndyg$6u+a-Y?db;j8+jp)ovPzn!e+Ep)Q`J9f@r%h*qc zel^Nf16y+Pq@zCW$ivzr0TM+hvdpO7sW71|4T>-}RG$&&*%gXoz%f=~N?pb5wisdy z9iroWfQJ$avjzDMK#(+*7Q!i)L|yzZJ-eT$v_DsB{W^s+K-|8E=nOz)>$e#^q`ne2 z_}cpZK-(LK;+%i4vsfcWnBn#sqhzl~P6&z-Q2*Scu zjWv0&P}yuC#D!SpSL{gx6lg?$RFcdJvFmqYl&XP@uv#gkYFESXoh{kNmRyA4JP48u z$~z;($d8P|jToJww&XzJt}t5XpSd*Mz{BoMFu|CZr~X}J77WLLu?6Q3m0-z67?NZG z$%xhEV`K!MpoH*sk$hhn)qwPdUwQe$pPGgT^;UZUT||TAN1I(f&f$lJor>@{n!`{&Z z`=Ag=TqvjuEMN-`Dj-7;knqbysL{FG+n`Ak_TSLl-#U?6Hyog}l0+0iyl@|j%zC~e&$tWZ6Q6r0|d>}zgxIg9-stK&4(vvO)mbZnRE$?|` z`)OUlSdS7eXB&o6G>IyHy?NtI`_cY$1A6{VaQxxyqxi7bn&JBqVbz5Y1)RPB)snLU z$_o_=q1NaQx5J$QQAF4WuV-DnrUH(2`@<1e>zWHcR~|G|_3bslU#d8Kx@j+28Q-RR z?D2r&`g<4ssHgAV9yw`QCq|Y4KC&+l@<(9FN{RqB2p*_FGQc79P{czhK!?T`Y76J3 z1A+v27zG=T0fBwt;m*bmgkDmCxmrVSSa>%}0TGgdZ51c;-~T9eyYstnf;Mw4Rs$Y^Z@z<+5C0YE~ukI9%%Mor+O*l=Ddk(U)Fg(3a! z?c7!%1uB3+P2jaQuoFLsv&78lW2cc3RM05YC5R|hfDhU+a#{akR5O%!6mS;?UfTCD z>p_Bw0Ehw=?E;Cix;)#5zy&1V%R_P)ypJ#0fFA0S2i$81gKCJpG$NeYRWxa;N9>kB zkfdnDV`C@80R-y_I4*f z1PL-CKCE?i+jp1xcD0%!#)XkWOhC29$Z8!X%t<0=y4|f}27|rNQuhMD|SE zm?Q=qcFyJ)f+Sf(lByvcuK{EU@aP5$pHh&|CTP~+lN5raYYXQgSP0_2f)S+CM)YV3 z7_>Pa==POY@k7)jh;#u7PVba73gyLy-*K`jj2QE*Ax`p-gE6FgV_nQ^aLI1c=wfe4 zz3{f49aoub(WY>;q*A>l8R1OiNr@Y6LWMSUgy6}`t&$^M`{d|hDId?JiUD%?0l2b( zvu9DxIpud^_AeTug&i0b7!W@K*_aRLA5J0c$h{Q=Q9u(2Q z7p~(Ah+@Khzg+Y|5v7=RHyVLmO(^FY&btW;&m;0OxO@oY&h~9F4v&@uMYD#-e0=N#Tl1xUO>2w^wgIgNdAbDLJ?%v`YHty*xf@4G8WF zH>d&kXc~i3P2s>T(*s0hR#-?mXOKV$C4?RUI56F5fOGAJ+DF*BI#7rQU&fYr6Zdz_ zftldrz`bG=;@jY5uxafR?6s4ONUl;iMkUOi|3t?k(Y_sKr3|hYd5yXF{N0&>$D5bj z3gGK#Nd3|wj3bCFrGs+>sWNY^5a&fyzyL8^XOApjew*dYvmXH42ia4^;DJ~&fkF1I znaS+C_#nkpeEuto{xWx$6U?=rke#~sMN47fGSo!|`* z9G=bWBT2Es^s(W7!}po-cOD&V=5>bpP!>G|8TtiFtEHqzgehM!xDSg=V3LQu;oiW~ z=PPg@tSx6Mk6=6Hi|_aT@D3K}0y(sAjhMX8X|_a+48cB$IqC8-oy8FsHLo|f*0La9 zqf-xpb&R@BU4v{GQhW%+wKw6xHAHFZ^danc16P!H5II;8IEMNDXp_W?an+5(UiT&{ z+Zux~M3ka)D1)oC5fq>a7exUM<;3bcl;zUS<-AxyA0qlV;)V9D#-O%f;rsZHCO?UA zd?V~J?moNok(h@f7*GU@Fkt&JQ{9hlyJ5{eXi&Ve+o=^8s^GHwaOg?V6MC@4R0a&F zfe8&0RK?phbO5xFvhZMjPrM2xp2o50{ziIl>=3S<&AFzYKJ@J-$3?erFZt!8Dv67Jxqu zbh?qh?riRDy+O_iUwTA+WO@G1BiOR4aKGLN!Q(v3WX1NyBbUa4*8NjSJozlCURGV| zjT2uq`PtsZC}6ad6s~y-{ru{SGLRu|X|Wo5X2_L3c~O)Fb`T5CtM1(Sa`8nY;1CC8 zE4lylw7tjMH4$ob6OJJ%%oyrnZ|PqCsE?a70EWPu;84xg1a?eb8A-{}L;26E5bS6h z*YBsS`Hn?5@&rsoh4k&A7>+Y2D6-yhu#5@_)f9P;LDKjWDW(WI>FoAI>^2Go!C~)D z@FAO(rw$g+i-~O+U#~Lij66sK#axQY6otraa(fH|A~dK7kz9Hx-ujM{4{g~EzWlY? z!|Hv2jo}?;Ai? zBdoxLR^0!{%K}RyNVNLTD8RZ3#$Cb}RZvPgK75gf09qKFgXRqy(EAJkiY$I8bNem} z(-O|pxqHvD1&YCq*Mm;jF3TInj@NkpwnpN4g>cbExlo!Bw>xpJ`Ch*O-B?KOs^h(i zW&~>ga9Wj$*T-9i83)f0lzx^|&SxB;-f^2NMerJ7$n(*(ksFUHCcj=#RjXT0PcsNW%^Qp@92)4?|{GqbvbHlIielEMeeP(I2&gb{i z;`Uh+4~@S}_-2>~o#A0(>qlgQ<;X^GOq z`|5^sEyd~^#n%rK+PUVjfBT|grTPeAu)$Bb4(BgFQC5!0W}HKy_bTIg!6-ch7q}jh z>wzNzp(euxlONrNQ&^vMh%7u5#R1fyfMJRwG}Evq#rU~L=%bHTw{*k5&>qU1vNpoq zzS)v~Pk(h+{ba73XV{O(q&(|Z|>#cR~d+a=`@waLSd-|RNbpYZwlSgNh9=_5(A zMemf~E!J%-*RNFbUe#IO%9joJnSE9{X!i{K$NQ}o8&|(Q?>`RBOfF4UlZeyPdo8d( zlH);t%0yS%&>!BZc1p|IWJ46B9?+3;D0E8~JdXX_90V>2n}}5PNR`u(d+Mw!zhln; zj-TgYfaT`#j-bSPJnh$~U@S@x6jknm5*Fc`hzMQA5@Bb61@14N2w3*>CwC$4bF|0J3_AN({|C8seFFw3a^+hroEcXjR zg2^SP_pFnBaP>fy5Aw$SZXdwZk|U>khoGKai_y zsze!ZrB%Zob%UVH&_p8~z-u2Td*>mUB7cK)+Kv_hyCF`0eTgVpQ*m~&MjB}@nskhh zLmeyX8XXB0a^rxC&u@W#v^k|3Q2T}Wc(AgK9itAj}?+*a&;AmEC0=btLJr${xX&}GG6SajwRY=$ON2N$eJmf0e-mjMVzQQ^9$QXX&P+t z+yr?zlmo{vRSO)v;S#Bu^DPqPi9mCe0&+%B4a7>N&mhB4a!XI}QaHkZ&(m3{p=LYq8XkUs-T49+SFfc#Qy+H@L5s^ zp4;Y0>}LTNAD-Yjn+C9fGB+ZOA_&PGg+|#h-91^u05}?7OTY2!63Kwmig5`F-2N%| zOyAz9w@5-Y72|c&`TWcoKr@ZmhcHwjGb9r86Pt2g5iZ-K73GG;QgZy`)b4D)EKGeD z_{rw$$x_G2XAO5nD${zP*YD|jHFQaR4s1PQ?RFr!{6T@f8o!qEgXH4TslG^uQdOp| z>V_^?%gHQ>K*xvQZatrsFL#`*?SpD4>LNPwiNck%MnqcEjzPIL+e*>vmqv?c#x1D@ zv3`k@CX^HWq0U!Ncd2onwG14iq)+B#9~jQgepdGDZg@?aYfnXwafUL7+qRTHp)Zc%uzP2IZ>{YA5#TVNPC^h9gBi^0Yt78+C1!prCK?&3=y~A z>G=aV(Dcml(5V{3oBxZU^Nxr5kK_1lXWgB%$8lEi%ih}=85!9t<7`Ftu5@QB*(9Tm zD3wAXWpsxSva%B=Gh}3ha6kWl|M`BupU3C(dA~o;&+B;&n|*g?Ei>VUjui4ynou~l z&vJg|#cJmRo!0iymuz_~WSz8y`3~vq5J@)&b*>rEUzu*NXMrE0UT+!~In`G_Yq0pv zX(W7UWG-w^u#^4Cc%ez#n>fX4Pf6+UNB32SKKp9_jrg&)R`iie1A88I`($&2b@E3m zwqs58Nyz0a(yt%LDDmW)gEe`#w{1$@4)qy~=OO$^{{X>0qhJJMRjS|$DW|Ay=ym2> z^~!5+d8YbTQ&=!P-7q^@X6AGuAd?QDC0dvHrw=M9y`w_qxLq|dPrluP4Bfy3&C^G}UvDIhZ9p&}WQ+}T?#xi|%6T$QV^LW8=zGa?Pcz!b-8Aq1nEPa3g{4si8Brle&<;Mbr5cedfEQ;d~w;rPAA=H$#I)rynz0^_M{XM6+P<^F)k_3Ks4K(hbn3uR>q3 z_~GO-Ze;4nvhk>7K8K~>_B`@aWP?xCLRUR+ill)MT5 zMTUDd4>!&Y6vaL&g^cS-l!9Yv4CO!>*-J zePLH)Qzn1;TWx@of;iGGYntP~t`{sYtwd)a%Y^c8e$03oMu_wE6yx#^>!)zem=^1m zFDX$h7COU^!AI`PA^dOnOn;db*EjIvn0bY*IJGNXH`82S>>)42&0-=wf=a*{i3EZ3 z&)Jed_+^t>0=LKpe5bXWZ^BE<|zy}T!h?n`2G#}iwa52Lr z(gV;+GJ=d%i8|FeT(vZU)Gvh6w((|tf)l$=!O_f5Hka3V*lG6ZK}nb1(isgz5qDLm zG1h#p#cG8Bcuf4fqM?5|3!x7n!{DVa;a6DLfS^e{6M+FfsJKDT_ispVq3r%erAlip(e9S6 z&<*Qopdr@dVNifakl#{Zu=mYqZ-Rwr410=$ZlxoCCv=num@&%V))gX8UTy{R{4fIg z0j8Y43Su3am#)R{C#bUlSfVM4K0Si)FFZ@>5+mg!;X3e=1mmTv9rDNci{*ywTm+AWSC>MMBd9V|Q1{)=~ zodHSaer}_`$Rs!|S!%&_Ws}W`sXoEfrW2O<<5DL;q@JQzh*Jk(=BZt59F;t}gBA3m zK}Rcx)$-0=Yvy8v?K!>=FmUxw( zF$DxNVBuk)-kKxy$jwN2L+5~hqAR!UQIQ&JWQ|2+D@bz#Atsz2J`E zQd;H}#b_axb#6tU$umlHq>=***dMO>SpzNGY>5# z_2p_;(`st(tLB3B#eBU4V`c;30urSIbml@S_FQHm?&SYcK>uQ{WWGnjfpP@YMcUal zfKzxMC3HOc2N??o%nVIwpH~kWZsS;EagPmOr@5@Ier7+w!%;F)!B~imz{u3j6j5m5 zLJmv^o7MxAJD*E=uRVF3Segn{F4WtwF@Lg=_FC)WAhWxAQ`((P=cU_@VeuUUwqE0x zf6wowagDC2#S_9_;#n1lxA;bzQaLV_B9R@Z=^{{GC_^rW*~yrZ$$_m3_bPH?Mc z%bG3h5B3lAY9D!Uld1DRkpRahUf8*}rOXDtQeVejy!~;BjmI^qAncfLT({kM>>jfc z5VfYy+cb8|#2c1g@BL4%i@&f)sYJab z0b+sUjIc4{<_g$0cs9;(;vqTbgm~@+9UPMu3wt?+*uG$o&=|t~^Y~pcyHdGk#kBWm zyb>$Ib@};8wd1y>`o>L$smL1+&LZ_=3KAbcYeV?4q^gj8u`y z2-h6@8+?-t%fsY(IVmP}KVPFa0cQU709O`esTp=Z_uw#%6I#-&#lwYRu$G@6=bXHj zJg4Tj-E}F`=En6X?{eH6Kb)m|-AnksZSBHOWCDe$V0^()hrZ&}(X12&Zs@kS5k`n>ZF$(4U~{UmY8>3XvcHhPlV zgZ;YWpIy?Ni?eDgFUnuCmu!n=I)Y5X0NRQ};{T0Xy|et(er1zS#ewMhn~zpN;9ea0 zw>I$4{6^TCr)vlX0H8bpVaCp!s{*$b{yCeIxx=&~!rD9hesu{hde6*PsQCtJh`e%v zH>%Mh@C)1ci;;=^k?spJp7ICmXNoIbbRm-7`ff@-#k5?T1HF$wAM4#!@=Iy-%T&7F zk?Z^LfWtRZrL>QB9miZ%XkGe3-639d6)eOQ4)r6F5|6J3?}9bC-bc)BB-^Y)MRd?ymLWk#7q(zAy(|CF+hP+|tV4v^ zacTN4GKZHV)2d`e*Yk0f?b{Edp5-Xt$yF9PB;V0n%VTD!fQh*5%*-NGa(=xwPPDt7 zi@&4yQmuN0{%pBmm~2r`xXr6FmxC4A>k{1S@ahpc4+Kztzl*=!b-g6nFFZ{7>Kl&U zQslQ%PyMT^_q$}gb07G>iSOFJi_mRv8C-eUn1!UpoMye*C|An4_#sDHHTTn{8>}Cy z^Yv7tI}rM0)xCr~-xyW=hrFxv_bdtOqkRPvjE^3Ec=YfH%cQHy6#pUML-E6-qPsl> zmnw@MCP4PcNDV-4=m)DCAD+-+NDVQht7 zEK{nU@MMCD(Q!pL?gD^-taqt2QLD69tI)miA~&|;TY_d)M`$NNvlEy29X#>DmZ<_< zZGBu3rly&yci|+KNdZ(F_3=$m>`VLD3x@!J5#hOBbVc*WSKm86{23{-Z!U6cZg?J3 z{=nr@8QG#=2>5 z&ah)g^N@BxOH-=JZ;O-oYQ7+uliqz&Urov9S0^eCC;cfJJ((H5{0tDA;zX zWnQ2yWYkG@H9>0zEPMzMUQIx*b|_i2E!oqwmQC80wY66C+E&8amVMh+ubnMkjazwg zwvuwTR1>%KuuY4kwejj~)wgZEATxkwf}H_+7!^<*Z#CG|LIrM-F-vaX3D2sL#@4Q+JAlT z9`kA+SZfQ<=q(I>I+>uId}~7_QwDxZ9mi>OL6;IFkAKPlye8;VH z9=qF3%tDtRXx)~X_CYO@Z+$m7ZnDo2>CVK^H-;Ob7Ig1@_p$QglD(i`%3)umF!^>t zKSN9R%u6J1fioYR_8(*|o1~R=#ci#6+KT3B{!mt#VP-OaX@ca48S#p--LNlX6ZjMG zD(kgfJ+tfuQ-{Bm-LJwJUH8uY?#>SrUd?_@wcU2k)#vQq@ZH+czC{zWZ@kSN=JT~! zHR|AmJKS&hsqsUlJ3J9L#$GwTk32mNSB*Vg9ox0rnp2rHRsw8FKz0W#qcdx#7uTK|DBDSNW-vR7AQwsvf8oKli z$#VV(2Ld~1V!kw_j~jmY@o;XGzuJ&Oi-YnP;)jmsQ?&cK=2M>7i)K*=5-c_j3P0`@md7)oWV#_-JaoXv zYyEX)@!Zc+r;@pSRLbD}I_GUaoXoAp@e12HJB*9{UCe>nzhCq5I$d?0Ibl9?AD@WL z92v&L2mW;@yazud0qIXva1a^|hMLcUXC1aDNSsZ%(+LX%&kJM>th>|wsOY_!C5Aep zJ6SAG^(bM30XkEFv*Yj8Ob!QrX92hIA`G|W(#lzS&UHWh9OtLzSP*B=SA-A_ct_$o zU+Q6dcz`nEJM&M*vpT%?UWgW>fji2KH&H8VKNCVGF?)#Vs%yzWgRyv+flRmdbQV#G zjRL56SH==r2oShHhIjp(ipR353!kmY?oH+nXH4o}6uM3)w5r1<5)4HeQ9-(erg$h#urb7f3Qyc7(D8&2;0mt{S;(&EDTxNsA4zolMquIC z+IaZ6987+31}ME1vtne9)fER*Q=)U3f|KXE5CvPbd0M`O__5&@Psn2>u4i}6LdP? zInYuG{9A{7ET$v}AwM#3v{v!E;1+;+n~b%3bj9(vA0N`F0|b5#LLZR$+#G;VJACt9 z2L;s!MmoBpL-3tKOSc*x05mcfpwFiZ5kkfDNF5OppOIL6!VTF*Wys(S>J1NxYxnT9 zCE%U@Tq07>jcw#Puap(?v3m5R$kSj1t{(sskRj8VS^!ZM{QxZ99%R04`t~uNL0)A2 z5|1T_H!mRhP)NvoZ^^t_Zq04IW!?2Cwk)JyzTl%HQR)0dUMiMQdD_oAD-sNV-orw@ zgcnqJc#|%nYzip73yAXCcz);kr^jL`baoa_vEe%zmwNEIL&^Y$hCNG!KMt}+es3@C z4yJ^mr@@Ycgp zZo)kK1;1zZw&VTwg-^i((NIQKd;+q*gHKX{4tvfTBMBr-bojTyE0HB zexta(!1M6~!V<$OF_kPkMBo0d-%DVKuuZQ6*CMY1GbRWM{g$8`xbGHvdjOX>!Ghe; zlz2&s6S5>Sm*qBfERpL<#Huyudg==l&(N?~o-hNQgO2dzO8jEOo`$w&_Z6BfJT@2* zbcMmyulRgu<}Cm%{W}@T*;VfJysh`^3)LLmMM7?q;Yu>(OFo2U2p0gQA)~`vl%|JD zoq%}A;OMa_1cBst#$l2kM0(8&Dlgs*rP(R?f9y$x=P5O42FDA&t_8u?p38}^QY7}1 z8!jju@OhDWQg~&%b+RmPsbf39VpL+NJ|1LFt>WKu_imxkVskG|7AMw30dAiNL#;^t zT7EJF)6T7%LVo(Ji%|r&y;wfV*I58pq2dkc>vWn~WcIZ8zc8j)kmw!q(`r6oi_S-S zOMo(W=eJe>7a8zj+klm-nV@uX&JUj}>6m1|5HAqwBv4*?4G7>FA_FA-qP+bY{8BvA zlz?%Eo#L~PFg;)B%}M=l{|&=0R_S3LHBAJrvqCS5ta$L>2MH_1)#>#7GUB&4RD1R= z;ns=p0!HpbtduLazKS4>L0!OG%I=YAfCj^Pf{{uIDg2atvPv`RjvAd${30U`v+U`t z8KczQt@5PPqKbHDyha4AhVtP~V<(HpQa$Z_1kV2nG-CPkBl~Dwby}*lhvVwAT*$Q~&&T>hopc zCf)YOPjzU1D;mo&i1~ZwkAkjq$hYAuo&}xaFP@0$S!wC-1V6QTi#%jNfj-llpCkF# z5|obT|4BV{7@%NA-T>DK4>}Wq1MdfnyMt-@-kARqwDY`o&*u5Yi2({7yh2pK8Gv4l zr2e0bpBxH!7kLt!1-vr^G`UJQF=)08B;T{3Hx|a+03Qm8f!<;nalv$2Lk@-iLEqs0 zT}2tJNr*Qj#JCO!Bo2)uM@p2@iIET+B)Um=M-d!@I3b|0l0oq|GC1p`AnO|1V*7C> zG6jXYm3hoCbmEgBH*yupDl0Fw4ItixRR7+nwh&OkA@!;LQdB+-5)g)id*P6831tTc zuvk2wGk|`N3UI~2gx2}u@T_!rwiOwN7NCb1zL6~(AC0>97I}is;*3FY(ue4*0_jsO zaAnrXg(D9m0rch7YA?4LXyavc8XJ6(IAps8GYqS|+y(ix1Zbzm?qPc+hLH#zW-EfH z1`c`bj!?ZC8;s)qD|&J!15esExn0NN0tQ@NY%E{ko7G8m@J30CAaxd;8FG}TMO9p7 zu!>po3ikyggoT(Z9GJW?a0F|1Sh^r>($0;nP-~LYj~*l_pneA0>}y{9)*ib z!~;gK8fO#^`|G0bl91dPz*{@~m~FaUS=`wU#%f#PfJH?r2L&gp1`;6Xd3p0J<>e$^ z)QH-~XL*{(AnzOCkcyt*Iv7<8c(B5FT#zN* z8zVFi3nWXbzJtw{$w@Ep!F3dPbL7T}kU*@0rMx0*8SDlYjTS`TAaFYGs*RVy0;!nf z8+=+;a^3T?p7K~==Lz~V>b(_9lD<_r_`;yO^P3ehe=B4Lz<-nszF>@;H7Iip2JB&( zG^lVG;eEy*+jHe>b;xLS?p=K>z~cs8uLNkbRyN<-_5=k7T57Ln|BGGVlOls|03^i? z%&u6=eUnq@BBCPYS#lQm_;7S5*el~iMIE%#v^%(o;;q&Jmt&I#>gY))@J0jpX$LvH zy7gLb%AlrrJw0L99%)y;KLSlah#DEr^iB(wh%&;_-N3RKy_Gb3Cpm>y5w}*NpBK=> z(kEllvpUAcWF#7hy$^-avLU);WLCWq{X8rOM@Jf!bgR8GD9ECVg*dP(*;^|R-|^Cj zx)TKB>^<6rcUO$Jh3SDx^539y6y{p0X-B=ttvXTUu$E(}5&{+D_Xm{@<@s*NeCUpW z%|pLsh_6#~1}JQ|$e2If4UtsjxL?j0{!WIRCxW;uBMjmJ02uO}I^`tRhmr0Lod0r_ z2Z$hxa|7bM+;%ulwZj;icRUWnuKeL6QR(>r4&7xiJ~A-cI+!LW%)&DcgPro!Cik5UJ$0Y}MhHRCbN7lK8ER+k-hfl`w`R-%c=+Q05E3ko05Ku~jIeZX z6r`?OD+yTB>b=%u0mM z-~hJ|49#{W+=5k#_b_;CCCvi3#y2xP9Kaa|m?0ZFep9&`I%Jb$D_ncpF3no|jF|YG zLHTuih4jTJDxZ~YW;q$HaSn8>`b!8hoF`DxBqSbeh(%wyCohJB)&KFov&8vVUgBRE z=T?aFIFJ#Jg}`-o?N~!Kh0%Nyl2?V%MmT$RpIxOaloS~#Me=kH#*nBUl2k()5hWxq zA4mXYJ>hdjEac6+)YS$pCp=@Ijlr2aSdHLP$L{i`4D3*k{Y-+GQlp}x zz@`LV04{b*)a9Cw>wN%lQc>QC%+yro_y)}DMBps~zySkt)cGhnI|VrK;tvY+oW^9p z#nll&2qn7Xytq1?`veveN`h65V`SJFlL3r8^B@QT^rk?b5n%Fz%v^OKs|R2#KY)#s zU_A4T=M-BimcEuv3D~4SJ0(LS@fZlzl#u{u05FC|7&1~Z7xcZ5CPq-=!fn(7pvMe%I4$>sp2^l0t##kIi&6WY2v2nx*?;f77y-Tq;BQpC76C>TMMX)1QBLHdl?F%~r;FYw{LR?y8ddyFw%fKHNC=ftKll98 zfM$uDt86qJl$Hf6OPp-oPAEFvsqg=rTYcj#y&{kRs(q6oB+& zd|k+x6C$Awz}SZO<(z~fC<5&ijMm%oh;e9i*<)D_#!xC~aSHA503uZYvXh7KC z*iD}?K!?*=J7*YHiR!nhiw>7De>SVQrPXFBKqE5H$MIF^Xc;T{SH z^LnW3KN2q1Awhz}^-aj||G>OB0B`b z5jdX01EWdv#be8dnWcy8BRm1feW0P901uXt{GCppUSa!cx8Sybs60BW0ih=E&PWEe zknE*}S$?AHtR2(zJR`f^zcdpdL(OutM1TZ=6MmjWP4?CMi$beIb?no`v5+D4x5iZ5 zpdk7UE|u=PnIsOZOO1k4KyVzuBdY2E!Qb^hR3U-a2>`ibD7&;T2OvUiU;vxF54Vn- zUV%}Y{odiXz+ynOjLpLj?jvSCmp|w!r(+vyfMc|Ip7DITV1<$x5`66`JXM+1lw9b6 zeY5~Z5lAsp_6meh>my*>SKd$a#S|Kng850rC^%33`i@~c!6=QjSN_1j=NWsO%O%}g z=%O+0WdiL$V97A#9tTmTp;h;@1u|Ly9gW?W!T;6d1`QR>hsf5_oRnvrK^YCNgUCP$-q5RAs)$FB z8DOt$bgycHk~PX2N(PY#PPF+fcNG>0bwRW3+|CPO0ECK@K?)_F4@8Ibs7jX-P$V1z z<6u8g1{lXX4N{hasPs|*V`Ff=Z`XTE^*i=?bWQW)jJdn<{|ftwqd|GYCOFr={M!pq zxG9-W_u2%o408p;Pg7GU$$1KQBG3^g#dv_U?}#?K_m2x;q`}p9IbY%#TXJ=~yV6t~uQmcb=9$nX4@QBVO`&0P~%5pRcKuwC++BqH*+}>uGceT<k^|l z-{=d8%y-k8!#J;fy!}hgno0P#UqBB8p7p+9;fz~c@EN3$<61gk?Ux)3AZ$9{&_v$} zK?Z*d9#FmV;{*6#}``}8Mt!O_wCNXSVivvt}t@&GRjWQy6!cWLE)^bG~o4N z&^3D2-~C^Ie95^+kMeU@i|gMV5{%mX`GL`JO2E6XaA7B`doWbHH~+_jGd}6r=AkEs z35cjwdG(=s?FGiuzy|N>LOuJl6LZB+V;|=B-Dc|wI1(pd~d#s)NVUD+FSo#b5$DC`g?!*X*tt+ENQi;!MNJAv9Mk2)b}ZmF>@JiitQ~=EOY(b2wr(H~}n~gC90o zD3K=-5W(|w22vlfIH5FD=zuD_nng#72pgWrxz)6QlHNQAR0XzftGEO89k>=!6upE1 zEGSj4ECCL9?EvRFzm*-E*SX!R$O|AcD_;&?_-d|s>IGr+ZSb})b!4Z29Bb;|QDC9Q zww>k@MxtHj_t+KY=*zF#pDeAk;BmM?i1&Rb8{juRV;LrgCCI^KBYV!-R&_K0Tp zDC7%>v&etkPJJf0@Q%r~`ikLaH^X-LtQIbx>_0V+-1Az&w;s&i4{AH!?2{&*_BXpU zZyrq)*Z%D4;BDG#q=;DXI#^^o`|r4I035P7_A3{CQ|9t5u%|Kr0lze6oYl=G7lk+ z2dv9#?EHU_Z2EqV1jhCL>^nJ8o9kBBG&pN zef}~8XQ9mw%v7~3D`Nd|c{^);#$ZRy@%n;6Uy*PBh2opFPm-Q~f8T})dhOfc6#4tZ zf)%5tcS~Yl-(CsJ+*HF*K5Tk_g<*^D=5&bf)yTu4@4^2zKOyg){3-rhs3(iPy$;v3gzy#t}=bar{X*t zl?vm({)O{KWI_bw0ZKFtk|=?qr{hb5z~(PQENW3tG~Inc=aA{-4g%PO_lCXDN&k}#1$>N#2=L_s8j5dKLZWb9?JqkdDL=(C0=zZJrYMysDG)~G(_@uS?Z{*wx@ z$!ljaXeVxnJr|WIYeyyGMOZrH$Ij;KP$UBAzdRJMSOq6WB0!`V-GSwkI%p9g1`8`o z&Qy-UbKM1KOYC_8X-H4|wqQJKi|&2X(Htq!coBpVM4I*{m?9N2599PS)E-D~LQu zf#AGii`vUM7j0Z1sF!kd*5c=7T?>>KN6=?-0E`>`mn=DX&K7&cGGoK3Jb2l5tH>O7 zZ68q0s%qjv+lt0GQ)Yf|82{l7VT~=#%;*(#bsfS)%IP@cR=Rh!6lH>s^5%Zw>Uy_) z9sTssy~z5_FG$VJ34xK9kqy;1U&kAffW%oW#I8AB$B2rE&LIfe1mMAgf|8Kbm&f`E4qpuBFD02i^if2~Ta@DVMNU4AYnkHwEJ7?_CuH zerSwYkjMa&6`SgSJi&U>l74h5?L37XOfZ|sr@g;6Zj9|Rc_`G*EFGGsJsKc|=BKy> z$k-Z7{-)1W9%T#*9jG>0X7|5E<_+)^4{M7hzY=&H@w#*?edYAdiU92;-HlzQRUG5- zz=)Ln+_{g~Tu$k$)({n)C2(pp|X z#>E4n6)x+S4U4$U*YhBKhmN>UkJj|_*jPGLd05WTs&vW(XdU7RzoLTpZqSYvQ0#$D zhNLojQJ}9r6skic_#d^zqrLKb6~&U6+hivpFeO8NM`w^)s{_+-v0BIFoIs1;m6&a= z#cNasgc3#X>QxF%C0IkX#JQ6XK)u3BsGX>2Ody*|);?}*0t(|ir1a+Rb2X9!`6eP{ zVr6x5Bg$FFfNyIW6zmILESdE5j?_M93)t)NeY1Ar^Ux{o-QETNKfR4dd4-C*fhw0) zLb17b5~My3XU36YF0<0U8wGv*_9UY=B-+$Vw?s`kWBKXP+DGD2I5p;pOqL#2*B>3* z&C1c98OPTSg+>n}m;JLMVi)-m;pb*N+y=#lnI!K&S%2*j0Q`7-jIPG_JJ_@xHj?RF|pZ@*S`nF1l?elA7kPb_~ zdt%WsTc{+}>+#3An;oV9_1wNZh`nHKV&i1!NV)&m%Tr3ivEXXH%#)4x=54KeyL_tM zF#-M7ZQjLD)~#|{_&hLzE|oxkh@Q*VIz`F+6E-`{abjR4vZ)Z^)66kvv5ZR8D{h~Mn7Huj2Dsu9uw~H8 z3m~^zSO+`rCIAY-v6##8AO!W|vSgj#3#tWM&?b>wkK{~$Mz7=G8acf0LV##8W58s@ z>Sw9-Y4vS)z56k+&u*DAmRlAY&aYRjeU!yy@5V1@-c)N@?O zvd#+f-{kfc5k@N00=iPp^RI9$vy0TCIQ}2tMj{QA%ePMUgY%wWm z+_=Bev+jfmzoO?cz6aaLT`R=jj_^O08MCYClVO1ok<7X1Vn*HK`YGwRUx**3GMK=b zf8|O#uk){vg|jVW3s5(WQP6dAq@>X06SDv4^->PjtR-Cn`RuP)P(3MURM6kjd zJBYNbib40@dX1E}i zI*%BZ`;03pHKdC{K=`Y-7ZO3Bd>e?YMQToaYSkpf3}_Rb=!qLEfJCZ7AG-e@_tZ_g z*bq%ju+V&2##5Bd+Y^St+3-z-V3;c^1DeqN^-PvKK0m3XT9hlKftO6U1tu5qp6_N;3RD- zP%#UhauGce16Q;LKU+gwh2H=k3F=#c7j3Rv5rm2PkMU&21Q%<-w8EWV8%`Guq>`05)b8ARjUt z^uRid_@5rEW}Bq;imv=gcdEl^pmveF_P%wnir6-pSpwj$WlZlqtTsbRvA2xNa=+h{ zDn%JB)Ux-nH*naQ&^$7HJ5GG$6&YGM759woSrYwSHwMRfGUGbh_m+V#VeOr^iP;+7 zl=jCLeqM^NNwv_wFzKr|Td%*$*kF07!P=z3)}Pj3f4{-;Rl~Ku2AA~)H^xR66U&p@ zm>LFsMGL^yZP`i3EUw)bi+e8;f`QyqEX;|eZ6i2SAs|E$NV8E847T9?15U&~Sqje$ zFoH(}L({hP=-24@0noIp-2W;Nb(dy^~8?hYSyFz{HZP8O*V;7ulh*{ zygvqb_L-5bR;b1M!dVtc#WDG;i6^QL$wo;2`$gFEX{0?CCGai@NdH+S#>%ewLROX0 z-g4lfQ|u$W;iaNG9@tAUMy!X^?9fOLoGqS-(Z-*tm8X#lol+BB&vAY^ENYaU#~=Ez z(`L{lNM$_GE5OjCq2h0m`>kB@q2jIr`F!PhX1iPP>b&%I&UbB;!VK>8Pub}Yzc@&L zzds;mS~8<4)*VV=3ws<8+LitM-4`0>Rx2 zZ8!kEIIt{UEUTKI>|-u}v9MFWp56b8Sl@EHf8VknELKo?! zAw5;7g^b2FmN?Zp7JsYmZCE9LYI$K|A}|=x-xolvrElo(pCJ6`|EeKCseZn^lQFR5!VBdYA8)%3$(h8$8I9hVTO~8) ztYClLnUNm|n>6O$*KB40c(-Pm#(%fJwO*Lloc(rlc8PhG^@iPO>uerM-2BVapXc|m zf1q=KyyX;RYlv*&FtU4u7MJ)1d5Gnnl;eS#Q=iRqJ1*OghVu5H&nrA?^ndeDWx3gB zc?TvX=Pj;WU$L(cnXj{)ADp-6{6@XbiQxP;_f(LoZX!*7JuZz{pf4}F-S}X5rud=S zWP@`<%!A1tor#wl6FhzkA|E-b!NMW|CjDHezd8^j|S_!7?d znz`>1Y3WBXvGOG4zBBV}>OeAizuzCNYN{8}TxbI*Py zsQuEo_p99ud4QsU(`DAyWPVkh{bHP1w!|ZsP~ZcLUj=8#1dFwV8|wvu$d%aQVer}ketnI+(RjB0&~>9%tLn<*^~A?ZQ?4r!Z+`!1`~5@9d*$M0qWR{+ z+3%Idzkl&>+6-*|oJFpm?}KK4FP&{Jx-Kni!H?c-9>4jmkW;lkw0!%*wygO!)D3%U z8T00PhM15t)8O{n{5G@n4rroZfB5B zhV*J%@)Q3>+O|;84!gkaDp5!xbN8KTA7b#2*xV|1?oZM2&PC)7PI|}GBGTF(QS{W!aeHgj<3)BW@8Qghm#`k1(x^ufM`jED4rdeVWtG#0wBFZR|N z^p@3UZqHo$z;XU^K&JiGE#?QfgR5JbAp>gmc%N|iBe8)3hmLNCH{C9} zY5%p@VpbSA5F6QNnuD(Z_A_bwp!TDT%mc@*JhA5B3p~ip6oW)?AA~9d?(t+1nud~RV~r&vbx2EAhBsR&&{?lw4Xm=(x=}(@$0Hf z|0E%g&y9+~$aE@r_7n4QE%V$KcwYOAT66lh_H=#l?04qbird-Gt7r3rr|UIm-|y1) z=T3J%(U|`QodH5JvomiVZys%jpvu}4;j9RWCNr$g9kd{i^XfLWD;czPc@Lg5Y?#1j z7J1ufq`Wsv)bjC+ij|1TBPsW`>JOnllv~Q~%KWVnt9x}=ihFDCj1K(J}(Nf1Tkt1%g+6}CEM3#-V(gh znKno3YUNjX|7Wz2J?e>+t4h>X>r&jO-?e2wfQWia9ZsE+;U0|iNidm@rdMYSNGl296G*78HA25| z4((;L?UW;jXa(r&2d!Nf>Ah<=6ViXTs2)9_JaHm3z}8xaCTpxLZt1E1-DU08oLCI{ ztgx>7ZBU06_N_MzE}3hg^}ZtOiqePkyWX7Z?$$%1h841-GSU)t0=HHvNCR7Y?*?0% zy<3u^oulua{Kw&|Wh(|w!hGyT92fIgzPY(GAAE5w9c3|ek4Orgypp+>Pd2r-iJX$E z979ivuxi+iU3{8kW$jO-4~iSl6U(CJ>KO%wqK{KY@ApvOfW}p7NtdIU7>jo z+ZG45e(zEbdRSL!>t~kIb>#T|&a-@*HWSI3kyF0U@7%m6zFB`zd671=Ij{tC4w^f4EpaJYFpFI(2Twc- z6ZC#?;?l)x-?EkQTzWRdy;w^1#?P3GJVCQ58*<0?WwYqL1!XfOyyv^Y(0yy`*R65? zwz{=x4F7Z*wi=>R+_tKeYqugYZ$oXKjQ6L=W_!P~-{-*}GaV|eV1_)0pK`9g^le>d z&k8MU=ihCq*ZlLE(dR_>$sUgb)p}zSd++u54~9}2XipvD zLJidcuq=Hz6Zb2N!cO#gckz-}*+{r|J>6r8_$>KyKzDaNqnu@u#puHiOk4)P%`EO= z^vfA34j&X7Sq#IiM|i$geLPehPX2h!Wy1Ndp4P!|=3N`Em!p;$>N&4dlhTV|`Gp41 z#{(G?!h%bSt6Bw$=Gg-Ji;OFNlqPN|e;ExHzCN+{qU91HB_RF_Updv_fg|zR8c%qD zU#cz-zqM!(;a=ai&Tu>!dH4z96F*A%6vrf6@DzmKmQximFp$m5OuPVh|DtJVRhNTk z5oys+l~&TK$t`Gk#QH?lK+ft_v2ROWZCCa{m-|jyu9t^8{d05YgXfRqg~w>TFB-1q z>oB2~W$3Vg`3HA7Y_5QqS8>pfLw8}LyjD5oFG9IY@7ax(xAqGPTKm0aQVV_JoJW0z z8}Gkqcduet|Cj3Nc)xV=cCLNBGl$}@E5UcZ-5Wls8c2=mGAfYsmE`GG8=$<<;VTzl z2d}9P$Rg;C0Qs(b?p)eqTErK5z6&al`!LEt1Vcn=g31lmY6XP;w&#l&Ctu0#9WhvJ zn9oVS_^WD2hEO31UgMX-vKDI}xIu@Cm5gUe-)4wr6ReF_B)UG>wssNU0$eSg3E(4bxjMbvTT%4ETFG67P1l7GFM{)c=k4FJ6Gp9N5c ztBhHuicE*wX26|1#V8e0n4aMwvN(r!4}cyN2T10?)jOT+PJ3O}@IbqnX7S{E?-wau z8*ZHZES(&Z?ycd;Gcnnv9OtDPi%P0=(iK=(HAG5LAB5_c6-uQ7%GZVyIQhun{nc)O z^H3(^WPzR=*Lh_122n!6gHDPZq99P45VD%Uvc5q<@aWjl?gaax+{Ao}knBkk0wj-f z%^6^zvs^=f%3#4yfEjBP_@b_XnX;p8&TY_m)>aQ=xTaibek| zNH`2}>f7;IK6}b?jQWYKpRc;hMTL0a^kC#2qIe4kc;1UpJsVDDs27C6&Mo$G_;|Mj zL14EzfhD<1ZDQwb=U{SV{WV)h&yO!}_s&;Q-<rp{tFQsBziiPcXa20zhIF^r9YQ*+M=j|^tF1Btyv1Y zMlJZx9v;e)+TJ1Rg1ohXr;B{kEr`GYGoRBM_|;bp8J6$EZ?Av1I6VZ?9cIH6#r^`; zHOVSsWTHeW;r;&{-G@I_|KkVnbN4l_d#}A+n~W$#$h|g68ib5%gshB6lJ32((J(^@ zpG&A@uMoP|ZrMcEHHxedQk38Q{{Dh{@8jHc-jCPo`O4B-_c`jCM%YhrD=`D1n86rv zW1=XlrjFKQQtsJ1L9xh?;m<3H=v*2S!ygkjIR}Mg(Zpt!0nQVH2^Ew0b$;+I3`1JHhtz~ytqqp2$BFbw26VHANv7O`wT$~FizX949h2FnqY$n zIpJ!veEYN&6Bojp{<&$sD1wv3>i{TREkSJuc;zqL({VKT2S~u9k81AGLBc7NVCEbU zpTLTVt0h1<-~Rn;ZzlC)^tdf0EmM3S{9SQ+gs`Ul$Wd)g=+lvT;6!?WB`3$clj*fd<4YrVVG$}LnI>(LyFp@(de z_^PzSgHEw4;FmnG0@@^}V0}G706SjF=OB;GV%Z-Z_l^Xqk97fo{l-x9_&t$$JV!jQ zPo>6Y{U%&aoIq{8&?W+s+5<2R@-kS)X$BLWkg22F^r~G$>KKZ30vVedD(o3rox{ux?BLK)$JGTW{{6+k6mipP)WSRn;!Ivga zV#t22@(+9G9gMm?Q`Mzo_yE>LqB?rMemDs-jUW{s#3QpviBIc52_Wlt@oPrs6mf6E z(kSvBC%77QjQlDg=Aaua%3Pr89Vm!mSwu!^lax8$H4No<>h|0(%Z$}M^}Mv>o)1MF zV8x*kO33(N?Y>K8h<+=`S)bD<2J`_n5_B3I>O+C%k+t^Yfn^{R4?wa&P@gLH)&VjY z0BB?YplH6z4pxYFGkp5ic#sz*cj>Z*45c(ahBwRg zHd%4kSVd6F`L}9rzCg$;oY$Bj-($L({k5O;e=&c34xM#Spe zzwCVHv0Qch+C17OX!IVDYPBCP*m$-@Pd&_L(E01A0sF@f9)c{b*39Iih^^I$yU*W0 zW|7^huM1n+M=q3t-}lvg6m3fX<)5mU*DHZFyFq>aURmDs>!7gg*kG_kvwG*91QR8N zbHR-Fh9LZ%_Tz@xpA}^!a0{|g%>P`zeva;v;OG0upbo;=4fl(zJT&b*&&f(XvQnTt zFLaah=^7JW$n+jK4eX66@G1S@N@w#K8dP-HS zz~BA&oBN11GA#bGpk{IR}13( zc9ZLmE*@*#bt$%--mE{q37d^{Kei=8K3(M$zGIX16F53u8YVgEJg#L~-MujF zYw0wb=PESJn`rCenLQakntqH@BcV){aKYt#*RS zpN1vbsd?Fn#r3(a>4pZ2nYW7=?ossJQCnogypjthhT(9O0OlNs*~@u%PaBZYrht4% z5N5oRF$IF5z*BUL1f21OU)`^%sv9@Bx??rQNO-@7GVtYI&v{*}CKS zFPcl)x2ep33vib`VicagZj>?fD3VGjMWbI$2|X!FNH%ysbJWZ_zVwtgZNJ(BDq! zdBSzJ(&8GdVFbbu$LxtM@wfgKzdh?wF&HKVHKcv((uohVk0;#5-cL0~8Uu7bC;-m`?mH!P2a7ES?@``1MQIZfAXDyl^8Z-{r}(3bJs* zTi@z5mYW?v#&{OaTh#FUFOF)S0Ch>2ezphU#(@gXAWE&Nh7R$@2~^e~reW_&*&f6Y znG!m{@SP@Bp76qm@HvlXd_Z3|1$Mm=SzVTb; z)Pno~Jd@m2$bEwe7m!p4fcUfK3QMEoqrH}X+>OW3DV0Zir44rx$_gY&mfVeWC2e>c zzVN=8R-;$36qLIc1hR*4_xd7`AOrkLXq8u6#YPwl zucm`RNb;7rcfrsNzvxvK9nMmmS%k64T+Thgl4CAwm|M)Nb2I;Jj`eqYsB=M-QURHu z<{iByj(rHY&fj1Xq)(5*rx?EfiZ<|?a032sp%Jf=Kd>z--&($kU78 ztXuiW@my+T^0ClB1rX1X&xS$;f(IaH9Z!va5X#jASEtA2KL5qfhR{JCN4|&A2m=Mut}X(E136WKTn4Ul13%W=cGkba2**j_sz`&XRl+T@s7=VL zN|5pZs7B%rJ2TeszYwqhaLPVDXcO`3Gss|^toVEd90YNx3N7se8Ki7-ytv7o05urc zU=h?EW>5iU^>{$pO0m1==cxV+=D&zfaNsW5hjrsb`fGFDZ#Iq>xO)cQ{MY7h{2>DC z11#}o@0W#Z1Pf))x&CF}j*#dPh5r}%WU~8}_hCa6&y^4tX1oZnPpcte8bK)r&}f?w zxgOr?%-@kmFF-RD`@v(aq1L{EiSxn(n+k2fj5T$$@|!}k_OaK}x$@lqA;bcGRw4nY zwppDCrNGA!dYR5SGo0 zUmI`yKM9q~m~V8GDzSvbfRlC!izwWb&(3t}I8f-SKUGJBrilm+F6!*o5#d^***1n8 zT(`YF*-Wa4U@d6k!Z{9Er^g=p<2P{wrpR(d)5pxzZ_~JVDo?*oOj+&H>CCC4g?<>C z`SrZs>iz;4mV7ZKQ+K~Ch8M;{Krc%#iH0N@#N>M12VlH7y6zV9aW;o}6VqBde)Hv% zm#dwTNQ1e}h38fRqC@?X$0X-@B@c6@GexMP^ZlNxMcA&&!$xTbZCb-lpSFNGvrjja z@QT|9zFYhB^wFb-Pn(tspH?)l)=XBs`O*F+$LsH!y>m--@Rgj$2PtB$LaP`0PUn{Lp>@-;!x%a(qK1Cj|GgDP8@zSQeLSDWv)#22 zDto4>VZc55bj@mGskT8KkJtF=+f9GCs_RVeH0%C*)K+Nlw=A^uRaB`4E)2c(M`t{Xj`|9AWwm+-2Z=)xd1!m41BHkW1h**%R+=-1R zm3&64{9F(NHR^sbc}CFZOUdm;KVdZKnBHFVhKJ+r*I@dDix{VD1JkSJkSgw#_2VM_O2OV$7u zF|T^iPxkYY%%LK_Ckdltm#GwgR%2)RDOSlH$}h+eW#ybrTil>HD^vo;jVx%1XT{lK z5ICGi#PW0bt6hO7>KE)bL-@+CqK-#|60GLacV8Kqf3U5j*v|ifsyIzpCb#o=Rvcz& zUm<+B^^VlIiD#aS#dXUw-3yq$$L9&{d=>WwJfdoHWmWvHf4Dj}lmTaa zWvpF0b1=Gfbr&M1{_wihz1{6BhE>UlRMP|zDvr%n?TA)yMKPeY#E2c@r~nO*gvc3^ zzufGBi3Y_88_)a9=pfic2Vm@Rc!*3;4=X=k2R3d;gM$cC`518F=z^#Vl93#;2yj?# zfUh)4#s%?HG#_D3Qb`nm%k93A6Kk);3Vn*Xl^c_OUj(mUn8sCe%D)Og5=)V%vZ&&C zE7T*|tcAQ0jS`vi>a!6X!zPX-R73(9=Di)qMydiG>th2<)Ce4wj+nCh1qmPO9?cn^ zNC=cY5uT_lYxg@#+vj)c)v9?!-R4&B*Nlt7_dk?c8Y6n6t>DQUn?*Rj*D4<^x^IQ? ziMcLuzo}}^NIQIXZ0C#zI!ZA9$pdF)k0X)Q@t#a0v6hLi9v_~xdHF6~&6-yB);+8C zDN?JJHobP5wn+sy5=C{(vNfA5KrTMR^(~Dl68i<`!mAcI3>wKWb2>-Zw*(V@ob73q z%^Gw1Kra6#xQoB2X>|Uh+qat+^p!_olIJc1OKLRT{YwCfh?JT^F(iWy+JSO}xJct; z(?$kGiujC8gk~)C6bbOuK`{97>*k#}%ntn%(()_e!jBOy6-v3U_D%!EW+ntg{T`fq7k8 z&DGGqGIi0g&z*UVuhPiVy28JY$|7V!f(v!?9v+nTNPbq$c=gfR;ATwnl`~o&9A{2E zc@tG}`$9uao6MHig20!P&$6>V8vo3BYJq%I9xJBV2>w)Z=+#oy!9B(KK9-x26ofN; zN@{9HvnC5-%3_TCkf2;x6)PnO$cpyE?** z3xN(<>Tt>(kW0!g;l592BuyhF)4c<#OfuHZN$HFO-XM=95_FTk5S25CXP53v>-hix z<|k3w)zeq5ijW$qoKRZ0b1sj zPzO(o{D>38Ff-zoqs6-;WuefZPqqg07lJGTZ7U;poTa|zn7zx=OuJj>{KHevR%=%iS>F*Hj(&U>|-P_n}*Il-cl`UQ{jOoJ`#^f z7tYuZo>^6R29NRr9g>jKT-_`7l@9t}z9Ry*J^FI)Fe6$(rZIh`~=BGGd8{)ooXpMe=$fr9j@Inoz!7u zSY(3Zk*#vOQ=&C<^jYK?CSAguR0BPwr`_{)>BTKy_|G~zW_ z$)YuSG(4*7*mi8xrq5vgtE>87TX~*awvR1jq;(1pP zyDY^5L`MMF98MBvNRtJ37bQgQJ97pZiGV;rmJyXL3+n{okP&I(V<_P%I%*l0)KjZ6 zW-R|E9@PgJW9tCDPCO?b+#>W2zILUCj7Qju0D>T7)_rcb)T+NTO2%2#deZqs3r+yg zib$-`1nG6o&FbG@y9O4SE;!qT_~T$}|7qOe?vPCR=v#-WG(|4@yYX$=A;)W?#|jHw zuNOLA5LL1qYAbekyg$G9a`blqV#gf4`^f3=_xy3NhgvsWe+~XYlmDNad-h!J5vwTP z+@l5}=Ey!f@x9k*%|_dSCYu{m+G_M5x5HcyIc(AgZW}-mr2`8*VwuEuqSP2Cf{YWM zH;9L~rGsY*FR)C4l|k5Y`Hd#>z_#x(PQnIJ-e z<}xjivEL5)!NUfiE3<=ixM#aii4kXuA!k<1#dQ=~5Y%E~uZtY4FF;*#5KOyFO%)`w z0j~N{_PY3XB@#pnAPIhS6A=WVi#)OP7;#o#*C|FMBh`HF3GTF1TPUB5^JJpqEPvt?N9XyrJCYB@_hG% z`)10#bCUbI_J2+fq*Ujpl)5e|vw3)&{^nu$&ERssXQGgYq=#xazt`ynuMn?0y-q&Y zOa3nRLS*|h@Z>~$8W`Ko<=`j-?KSYc408nQ5-)>%$OBQJ)9%dw%8ibU#dFF$htaA< z1A+p)R1EB?(=JRG{x6lsW7cuWuh%_R56n~T{i7! z>-+S2RPS}uUE^;V?$n2rV5X-G+9k}DI3 zZs(V3G195Ogfhq3&@P=)yey;aC>6zUn!^-3aB!)z>Hx#KA4~kja&BudR>-N?6dA#s zjuGXr6@M!LZ+8@eV+YFPc>sdQS9v`faNTK_@AnxaBn8=^;yBm^4ndb{mkv$( z;tV}TXJAP)<$lYa=R&lnQB|r`4PMT8bhf=is8vBw^FC%vrY`UWvw6<4dGVpILf!mI zd^Uh4c2sVZGutSFoA}3q_a%}hj@sJ!9=B&Xtq3Nc7D}?7y{hG(^6yob#cXPWtn$Xw zV`J8P=Um-P9V&2gN*7-ChOO;|-1yb~Fwo}PlajMf#?IePDf8yDdSCkEPtAn)mt?XN zNn!+ercVZJmSPSO4r1kKPB7OWvf(IR%AOLi3(@7ppJDGBWMAog!~$y)Am+~lQZIop zD*mlF2f|sTvCN0@ObMh-q9{&Zv^{o&E=-4VWk2pxE(K)=QPxsMRSN#Sw{$Esq zUgRM!hqglOcNwI%JEh&|h`Jtd-sKuKTfH?Ya0yJvrY}`?<5wAw>|8dSQ;c9x{nolY3hFA0PgTjzw0>g?QlAWjMJjNtw6bN;ZlXKd6*kOG+1u(wA`#EitTO8bP zPoG@*5uhn{stND7X@>GI{kLbNb0NoOyi7~hI^0FT|98i5Gr-=6m$5H5Oa%m{NU~0h z0I6Q>CKANv)*W+K`j!>BI(q{G^cI|kg+lV>7u@$V6>;4KlKhD zsPY2qj0;65T0S~ci{}7X6C=WPNyU|#o#w4>7C8_!^W^=8)e%7j>y7o-`bt7)zw(A& zqYhS6;B|2;&vDkO@y?z606n)Cq4c@K7S?a~72BR~jgxSF3zn1ptK_upvx6#;(V38u zed#JyuWT)v9x%FWU#4oEoDsfl7aDdYIZl9)hOFvsWH+BSIZ{VrUU zk-?7Q`1JN*lC=(5p1%w-Qyqg8d6EMC0&tWDpuiKbAgM$D@G(s(pAO;0XFWumgBvgP zg-YOWYu+6?)^T0O1`moLyyX9%gFQ5DhoX^y&34dAX6Yyi2G0o~e_<<+k8Z-=-w0X- z0Y@Af`L)3@R!Yn9>n>OTkfjceJQ}G`XA=pl&=~;0FP>gbTV4ebHI-sBe$0>)&$mdu_ng+taR`-KU1`wvfu+Jkr!C-Hi*4h<_fU zW4`An4c137gq8EOzkl<$wqi@e>r5%*9f2b2xWE~HS62cOKg@H8M>At3ID@7tKp-mJ z_WC8x;TT=L1fAaz>PurzpS%KZ1F-Q20yhchXof$xukt}s{sn;ja)Z`aIl>9%k^c=6 zU;eMS)AOsTMbhk2b?v`qn|X0j<9y9B;M`Syx<&SN{A~N-tvlQ(81-Yi)SV zrv1zEr|>0b$R+23@0Qgla8mHO3k=sZ!tiLOx#krJhF$PoN~ zd?P-8<6`^;wg$i!zm-2fKwh;0e=qU-+0GN?9e^qgI9Ds{G{WUX(if!!fA=_AR8R{n zf1@!yO{_G6`t(J5f-SnS&_xgG+orP6C~z=>2m9!$y0+(KRS9XEV1Lw*EdG7_9VS7l^NT%h3AAL@WS@GIbL{k zI>P6V?XkNQ_6^&}r>i>dLg#~dJZFAf`}E&ynd%ynIA!jP#J#7|Z-rU!7Y#zu>!N_; zcbE93Ffz-x(Rc5>Ufe~umfTQyW2*Fhd%Y~lLe~>~JIgH5OEis7v-0Pkv6Z8jyh8Yv zhXTiQ-$ZP%^Xqx4%@mN~(`-=!f$m*F2z5ifEQ~wh5%&U_*FybSZ<^4laO3e!ChgvF zw|}p$+;Dn(Q$6_GNYi8Q^Zyx;;e4mleJ(sROO>;2PQQNX`S}9V;$JoYwFaMj2F`Op z%YCkJdL8%a{;2$TmG@wY?VHSitFLd&b!X^433PsQYwO$SnBb1U#n+~G3Pk$A=XmW{#D#vewH0SLl6_k+M>1`MDFI;_V* zEG{v?Y=_FC5Jk=b0;j~9nh_WgijZJaSwj>=tG1*t!03X3;%cw=%C%)aWrN)!!5=U5 zh}~WrG<#O^r{`^X^vwrl`Ts00zLU-NA9^QW@aW=u#ntc6bCs%f z;%wF7&qGM0i4yqF)Z>{qPT}^1Mq)r25r= zf|&bcaWkRB^#9C7Y@eTrTyulgg+!Sffz3u4b)sfn1~V3mb_x zyf%_=<9BW}Um-_OChmen7viFuQt@-qJ8CA2m@n4_0N0nfw=6MWH!}t z;`7|%rP5bi;w$F)FCN*?-&d>{pMR)s=jV2#doHUayJgF3HdX-s`@bV&lbK%&3GLf^8D?3xx98|KFs7Tr~Ue@k`_H1>Y-p&(l>`B!;%Nw9gvgm+_>OV0HYk?dyQ zSN*AxE9KAl|Kw&iRs8X-(5-mxoApvo*T1}H#&Pv&rPiI?+NGo%|CfllNY&hqklVS> zT7ECx$bui;*}MmyRSeGk=c2dO6-yBf?qa)V5yEgi5mokzvi5wgpMZAW8MF|X{#p8G ziy$Gf;no=+V3+(?eB%0*zq_NagY|!X4y$zjZ^9N49roGz7va~!?O%+2=3RlA@bSm9 z&VRl=mCp}bEIyu(ooo8_cW=2mc{+USovX{8&Bv~7hr9pvh#k!IB)^S1T7A-g=I-YS zZ;8l*=TByj=AK-6`%4jh@95|3KRB}Wuq!#5O>Y+bAs)Y@I6#{BV?rhD8T%F~_tu7| zx4mA!9c8n*U3p1tZ6j50dYy~UqeRWf{xw4W`NSKI0nQLf^|sN-x&%5(2UcN8U zu8@E7w0PouK{oDwda+8__p@$a(uZ|8yrnX}TAa+hl(&)oTz~ib`C%y@8dt^(lf;Ww zh=z=7iQF&Gg{@pF$g#|ji!VOa_3}b>j%|5h#w+LDl`9O#>KFffl&r*7j~oVb?CLjO zWL&O)c_o_LuBk7hBCLMZc`N6$Ou43+rRJFBZ&resE3-;H`$6p=Nq$>LTi3|2H4jM< zhj-SQH8#im?cT36eF)5~%`-Wsq`5HG)n2chp}Btj!c)i5im_Mu0q^Z>yzM^sWj55S zWG&SdaSe8-)tS4m-yn-P&2c?x44+d{c5`T2WS6Za)vt#Zh&YQTo;azwj`fHY=3SdF zYMkEPxWy20X{`wJRadOMjq1^e|K#Z~pR#dhPQVFl%Ms+36rKga9GO??2(~mLP6}dp;;JYtFR1**PZwmr^@d;{5vfoy__rsWUFe*%=TX8a0|a zRQUatnc24GRjQ?oz>n!ED>JO%Qo8F8|jiqsBDsP$w(E_u{FlN~@`mS8}v8?Kt&U6Xktz9IU* zw5{3}h)4SwO5z2b^ULM~_-hF2WEKJZ(LY8=5cs0?0zlFSC`a9P-zEC){(Q}tcF5R^ z>6G0F(CM1@4y$}R9j(X;)&ib)N1!vBZXWtF7;^azapcO2A;5{0z|o1*aTO#zyQ{{9 zE@GXI2N9elHiPaiKl9Ze2Mrez*;wN$?CiCTAK;j{P!{#3 zN1P#2(N=SO$SeZ3aVm~8i_Z4HFm1O>xS9-UolOn=&ArAeiu!aT?08xAyi=g4;#k}F zGaQdDPX#j2ZEtlk1T=D;tmN+q702zc>uttrEFbA!(50ENJ0>PPF=NiZWAt#b%;3b@ zKNvF_p#S#$Z!x_1x5VUjt_n4>7_JbZlt32{ejAhR6e)&flFs}AMW!d0At2gRXy0J+ zr(6FlvD$Rp5Dkvij{MXAgbvcffz5w?C4P;-ukThG68M8 z`TUm|(~qPE2tEQRLRmDhmYUhbIUH|G0fdO-{Wk}(ro)Li9xhg$9tT(gC>N0Uh_dOw z_Zw z=Og?vqLqjtd{B59S0R&q=msKUO#<`dh8~U!!-VYp_>#*&asp5nHwkVc`4 z$lmDB>pCZ432=UV^kH7rbcivj5yXG}LeZmZ=6aagm;bfWHqU&?} zRmAskdK9A;IJ6GUf1@T3o1hz1R#TGrQEMoqe5mqYkqdI)wpF8J6>?wYAe z3Ah`qb{j@`HY(T8wuAC!vBe>3?C&MnxtO5+0z{hhPF;bx9!>_0V}E4xN}RacMiRGU zibj&=_3&<+?;vC99NyrA@G?-t4McmDxD=^}1&Ffz!SNI9e?Tg!sj9c|Xqh7%D!&rD zF(9H%;`DkX&P4i=YYlEmgwqf%!v}`f4W*BW;G4sHIZTeeWKJRPId z@SOh&*z!k!FH8wA4FWJMPyi_M@mVZgv;_q8lHgzf$+SKOF+SN=dv02T_@@}sk2*Hg8$#_xv^-53 z6xx5!8DMpLI8I3c!}h^gg@l z|G6Pp>{qa2NkBlM*FrqLom4;BO{JR}I$AX+i zZ-`n8{5*YLUK|gP#Brq1Wj?UETkr8n{<=jz74g(KB(ZohWhf-|mu{YkXzzN7asbrY z8db;?<-((r@NgNDMCogUW~itWjfV>l9bpm9M8uBUoma2F7FoAOk>JQJbc<%c^@HHp z*U{@w>iq*;mfp)ZYWZLH`}B_>b|`(DZTwu=!#-7j?IP{e9U5pp07xQk;ew&WOA&#k*2?T{UsM|FFt!d|q){;M^u|GpI7IC2-8A!CSwzHREt zx&CrK5%IU*i!_2T=||U(r+>>eG`&7m*uG}_+?^5^pxuL<6Z6f&_}k0Z@yquW#C>M8 zG;2M4som6=Nj#GD`O%<2+4IY7eE71_uR7}iTS0G#dlT_2siT&(Zm*a;)U0O4@_7H^ zszHTBO#7{%i!`2weF?#HbHa33-yA|gFtuY-mP&${ZZP3^Xwqj$$f-9M$A3gt#}a;5 zu#n{$H}u53m@f$v6hXhK%^gUCbI-+1wg>*u%B9%(ye@>BGsU9V6DlP;nBeBZSlmnQf{2YH|Ipw4}X8Ai@un%`$K#{IVF%pBY$ z8x!O%CUqo!dq3 z*UU&l1L<-1-hs_=Si9_Ny|-E45f53yXaWi9OL{)%o_@(8^LitE{vlODbX1iN zGz1SoX#hJDKI7)wt@B`(EKp?sU^WL6$ntJ+|FMkTn@xHB6R0r~G@>ALeJ|JINT=!s z?gfuqP3WUIi>BTpuBu|eJPRFY0u!z!l2HxRPfwIfGb%VpNM7C0?xsg~ub7s&Z*WJx zUJWc=ew2KVC^k07ZW$=5jYp1rpgwfPrMuwWI2f78Cm%0HXUm9USaA^Tdh)m&%T(`@0@C(Ot!iDP#g+M&k>`+56Myr0EgV2 zTp3K{faf%NPCX9cM2A6Xc`tRaRX9Wy6I|s0DByt# zhX>MRXdM$i@6dQ{Dre9E2%!y0TMtW$RbHD${}X*`K?8i57$^=J;TB|?4G#I)Sqcul zeek8@lj~-|lsg{9g#$|wft}1kldS79c=$ye>hw_~;va*12gjj`V{gOV*w>!^JT0#W zq!bS^^uup<|8)JASGKH61kaeU&#)5i@;-ueFcm$#32AzW?6tjHjwTXLfQrt-9K%!HS6CIR52L~z#DU$$0+JDz5e)lN-uHr#f zG)NUFq`3v@4j@mg33z~Ir*l757cfZspAx|$mH@N@B=?90nc0vX5QZLG^E^dDJfOi= zazA8b$-j?&3;iSJK8-4Kc=Ek@j02Z@91qCkVde`65pxKe12Jwgbh{Z~N%Ab&nVn^8 z)6s!6z=?)cprg!nCO~iD>_<#6d`^`8@6ho)Xfz#drC)jW%viNUm4Ly>Us~MX8I&j< z@Wl`PWr9MO98zaCok^!KM}yVGdKeuxB{?5TN`I+c0%Jb-`OzTj7p+?Vx4|RZoL?p4 zB(yMXkc-at&kZQThq5B$Etn#N8T2U6UEH!RuUP;r^bY$yd~$}U~GIhDTZ{zM2rP=Dkz97 zP^ly^lXud*W>H50)*>ULv(b}?0nZ5WR&`T)DZ68>m+aFPo580%_e3psW*hIRoEF?N z9Ws9yevae`_B^U`Y0VcQ@q$uc(oRX^8gE0O^NxeHGY`^z+!kG*-7`6zP{4jm1$X1h zeRmJHB6E&rqtizxUn|@aXW4f|S`<+`$3RIi&(>($`cDo+u z4R$tT&U+x`UN9+~TFcBBjH$vLmDkQ^E>_U%#atX`=5X@q1wE}1oOS1bIVY@mn9t(7 zc6A~_Y57Q!Agr4}GUhd#TX3f&iWbrmME?mE#p1aX-BL}@BzUHAslHpJSfBar;eyUL zGDmTlZ(c$|u_opqe(ix`!l~ty1j2m`06U(l&B|9s+v5Nmt`t1MG`i{}5(cJy&ayUW zv;o*Q1U33}4My;WAS$m0GRt^heaK*`W87d3qql-oq9x{gI{73F9j;}xh9_~f z|L!<$aQN|RrS5t~Z3k@WQSHo|B9l}j5O^mFbY9lMwm-fetMGNuT)uo~$m-Pk&M@x~ z&%EtrYlU{uqufwye$=6WXS&%HRM>f9vh&`s_cN8faoh4wTGrfcMuyznSzcsVBKNL! z9+%9;S^__Y8BfskArhpb8^4>OU`R_aNE0W;5V$R#W(si^oA=8P17E?EEjDOqM>6`2Q zbP-LyF4h#PZt#qadG{lPKIp{h1!L_C&0x`4k_fFg*O{lewz#6rH!%l}XfR4!7>M(r zEZLhu)FTquDi*Zo#;+jEhVfkOR+KkQ$-w71^u35qjrd+(3+h-hg?Bvw&sm6xH}8}Z zRwqDKdPb(;l7I#th0w-B^oT`uviI-%*SZhaHEO7aV;|c5EF7XI;FOzaSfB-XcGEsB zaf$|L3>UyOf?_Q6xk5#5UwS(K7|OBF;Qrx`(EUS3(lC_7Bf0C3Il3iD9YV5HnSqa{ zZB_Xkjf>~lC82Ej1HHxo>TY@n2n0V-P*+7m;v3<%rtM%Jx-?I@m0haUgffoUfh)*4 z>tscG1p|R^559SL%F*IXDL{wn(e3nm=iBE0hCsemIbUTDK@ki36+Z(2&OFD~w61m^$P35M?C{95^$IsbNrW6xYO_>Z$QWg0|2m~-; zl6PA`j*It;tMwEN%l!YF&>rKGyWrQNS}CS;l531TS^`*S0McDD{HdUsr+ZA3K1D+ z2M&~ZM`pt+ApNUH&Opg2AJ9cJoiAr+T@LOPUYI59` z8eRv<)$m4K@EOs%|By%0JZVsqJD$^xpm7^kvHe{x-tyg?DR_?SDhVmc%uP7QLKCI; zIo7m=zU^D4-HITCWAN18(~l^^{&C=z2t*q<<6-%X%A84bk3fDiJfR)O{vm$abzPFJ&bG`Sm3{j}e$ z`)>vaqw2c%!zG?eYs4cw#(M=?5M4zHuQ z2}t?mMKM28dt_7#k7iQF-cisNZ6o|tpDg8C0uiw`vBgDjbU%w1F)OLM9P?#!0eoZI z@Zu>5%n;-LDis6pPX&{e%rm50v^!~Bzh%LKw?(PUrxJS64sAB>5O-#rn+;pSl`Kd$ z2_Fd?bi6vo#i+CYKtbu@;5jfs4-oRt6|ku{^eRbDwhC04L7J)pY0y#6FQ3aBwH&AM z9jycQ&t#P=I@t`SPZ-s4_wM%J~8%+gRsLcSDr?U5v8 z)0LeqWZ$3P<8l5v|DE$V^Zj|fo=>M>PwOzxJHrp}{CVLp@bmTw?v78ewQTJjL%iUN z^efsgSm;=rb!e!odYn5x&c+a9+iH{hI+VRS?xknggV{LW(byZ}@g8pR!KU#>z6K_~ z5{k{zQ^U3jucR&ZP(yH2g?=g66U1Y(W}ylTb_Oa!0A`vdLMezHB0_G^c1qH8j{7>x zD^^F@kOAzKG6AeL_Ej6?&${mYr z0t0T&a2WM#EfCF{TR7<0YHniyQf%}_k_7B*@hbE>`WKmb#2R9c)^%ak3_dBp^|2|&`&;O{luU$;}U z>I{R_5!2TmR(Ux2!h@rpVBxinwx*5;5bH3zJ0_#StB`a9Y4XG^>+TP?b)P$duBhw~ z;uH=tlz1~`3~YSxp<(;tAvy7?Z!!!2y9>g{Uu}&ue13P})fKfH92=YNc;!Sed%|}+ z_b3b?U`|b^)pi8VF>M3(dTIQ`4`F8w4!$6rss4ZRY(hT@KAadc6rnqRb)`RE`;|}p z1y{*oQzsuay5GEl7VWC{JPYU_7=jf{sA35&L33BZ)Hbsbtk?9gpyvn?A|aNB36ui> z>}ltcVMt?Z|HELOP+yLepDZ*2%TW$;atIRZL3*pobvP1Wuf*L&W|p8@s9<1g*x2(_ z&(VI6RTW8+7O>fWYYm@D#De*Km?w$mXL&ptky$_*{}p7vfj|@+dAy_MU-2ZiCYle} zaumml$6Gs83>$Q22m3t6K5qGj!4vRJ3Tx1_-KZE!_xDE8)1nba*@DCC6 z*fG=OS%ApqqoW1*$tIMg7z){xhjl^L= z{Jy|MqU#@DDUo7Zzre@lulNLJu-s>|w4Dg)AJV3hz^5V)8lEY#DaW1--`0rCxrA3g zLJHndxqr6$!)CZgv9D&_c;wOGH?Aq?)$}Hng2vBYafq|v2EIGNo`MPd?9Ys`hU5~> zUvq#Ll;lz*VmE2${{08sUy8o*j7I=r;jdp{gnb1WW1*nI{0PuRPe{js6=Qd??!LkY z<2zlP875u&K8ptP+39AcWV;Xtp@!Q(B+!Rr;n=FjwR6z6!});Tbm;Cse)n8{^cd11cd*EqYpaY^hI@MGfRRZ4_h z4U70WG#BZ5qswE4J&3eE2uA=W6r0>{XB43Uu+NNxH6GmQRz`TZk})nD==UI+WTtyI zXc2V72bj<9>Xj%Bvje>ARHnC-6y;>4T-9Ib)QG|Y<6GSh4Wfiq8(kmL~sqe%WD=LUWWp)SLJcJ|aY)j)l z0OhF6Uk=QNY~`FJ^IR#&=e%GQ1j{5Ae4S=~U=e#kI-lNEbIX|cGxREsqvl`qI(G*cBu^}Tn^msfjO%X>stPNtep*<~S8qFdxuYlO zu(t4$SN-R4vTbAAf9J}4Owvni^#ALi^t^2#2z0oXi!5U>V~26?%EUb*DshP&FTZ~5 zUT7G9+A;e!RIaQ#iXw%78PU5KZgeOqppVE~vgKiiUfz^^jJ`%_P)Mgh^e6~bAE?bO z!p^G~cMc*F_pd%^L7iZLHZ&ILSBX7#EF+%JAG9Fk40-Laf?GYE`5_1eAEVTIip40^YmJcNx?=v~jR(IHj|4jR zMqXO|ZKy$>z1pWQl2!I!Lu={-YZjMoQGW(5dXYr6)81UMx_tfv00VQimg5$nxIZ$# z)6D#)1(a=c+q3sS-WyII9u+OBmPK=?n|j?bNU(n7T?W{F3*Ws1pSpB;)IlbWZ@A;fd)17Nt7B+tdjDDTD$Xa zsjE>-9uu*|t8hC^m%^U0Y7jyX3%qn4VCDmMrZVQ`JRS@u%>By_o`pLTkg-Z#6^*Vq zE|4P)9*=nreDOG<(N&=2Y`uEeuHon5CYWxw31-6rZ8kBvln;Ls3jV66JwK>@o{L=Y zG0&<>`%(Ml>wu?`Qd;L=YO5;<11Pm0{z9`Rj@~FuhM~?Apv0at3pyCO#O<^9%(RCXD4yNGK_@tg+62{ACS6H@ua`!&Qr-)z6 z3SO1AtB6WwvCo?*(Iw(u1hXlWXFW{w8Nz2=z~4`=7SN zq&t1Pg@r-w;<;@H4ee_!4}q@EiMoGPP{oidEH8wnHGWlSeF;r*x~I3Xq_4Q6<2;Z% z9XI%>n8OF!NbS7zrE`_~bL}@s8VgF?{8d>VA!eN8;<|hDO9Zop#y6>^0`=#P)&RHf z5taF_u}qE9KF@D(uF)?S?S|jgFx!Mqc9ba6F;h-8KK!#R&NZW(rcjCS@WfZ+!EK4Y=?08f5aV+iQgrdVp+R1zSk5kSfEXe0~rb-G+BW%#XyHm@a20@JHZ`Njc z23TfoI=tl6id5(C&qcQXC zu1N_}Tg?1kUETrPt8aM}s^h~EHmU3WIt^A5-hVm8Ypp73{S#I1*}astzo<``laR{BC&UOchz0nVFb2C-b@eg99r|pxAT(ba+c3`!+njp zOM2ine`_p!9$uuy{=whI=%oDHcWuvxo>J>(~;M~Q@ zo@cK$={Ey+ui@RY1FzrozNH`-w#2L0;Cs5fYv&xhmaVfMRRPx`kW>|JxIDDF{MnK2 z8Z=IyEmh^x@>;g&9`|^1k88{=GUcNB{cQR5ai3Ds;Up0RHp;fFC7>h;kI z`|IQ_g#$w3ZtCa*DxE80g9`Q3c<^wg^5{W3H)_&XW9vSSw(e*D^Lp=vkqG}jt$&R_ z>ByfAPaJ3B*0?1?4#2%Tzb*;$sNL<9@7|rp3x|9;sA|8zp6TTrxi=i{e|gg9fpFj6 zxD5(@lN1R*-CxYyKw+~ebiuHm2P@f!D+S+$)K_HV3l3;C8~;Xp2mZk#od+*p91BRd z`m%4#H9p!bK5w%B$|HoYp&6T?Z}EH3`G4X z(Ux-}M|e|R1?BV!pBEUJ8>VKW${E@D?qU*&-QhHwtbp=a2_&!il}FoISppL092sw`5#oEL@&e=d>Nq%U(14>XcfY+r{)! zIkCIf4Oa8?b>o8k>o?!-8fRwxs$G!y)s+z5%UbZq9U`_f(--vFR`_3k=f#Dwj%+>?KOA*h2>%bqtk?tKPoxCG2|1 zoof~Lkz!`|#;@i%ZKolOyfh!;P$~=8-i6P5J9n!;3s|iTsbTxh)@(c4_tM1v1K;$5 zYo}K4Ex3KYKg#{~PyDwO6YbnX;f@*t|pO}T<+hzM$f-bJw`B=F+pjv_)SIe&5ZeMEOZAwv#dU>B7SP|~H zdd}q9$>_EHx1D9{>1hr#4==NRIcge6=MK)ew()s<%!pqU>_6^^R7&|Hlwvt2TW(8_SJ1anb6yJ#1 zQI>;Nql@PQzjVEgw*0T#_LX0Dg@B;Q%k|FR_FJim4zo<2mx7*fR%eK5FK|kYeSObR zQ(pLRp{p{q`}c*vPgaf#kGc+edh~r_9zMxax(*i(mkfBu9J!Q1%NANV@w?kjw-?@B z;{EOub0I}W{x;nCuutK7Va|lxta|YH;7Pm6>jv#R-|swrKmTSYS24xw#9{P15%~Uj z36rhtEq;Fw9~jX__oy7#XyD0o`H$9_oGz145}6*OFv|2Vl}a4{{jVkXq$Xnemd8G>zwpUFuRHddlTMrW1q3G5I}g9}_ANa5tNr_i-4XTf z5`W!)Crjn0hZF3Lm99UJmOnr3l$<_~a%Od9C+BpkFNmN*(bECjc=+${zASB0LR&FU z;v?nNo;7(o{pCf8b&?8vDBo`cJ`>Uz?*6x%Ti~?*HFxii+o{LUAphR6lhfF}{0%40 zy9S4vaM9td5Tt2wWEQ{E>P)NqaK4y zP(sY*eYgezBxwyW8-1?E{(aeJ&O zqgKTyjpTqD<)g0a6F?Cxj$sLi8YM}ifeu+j&P_nrDA4>E9>zoT>}?;@Aabh53#!LN z@w5l36Z4PjQY@8ec#@)N?9(h_&MN8kDjn!B1S?AhM50Mg7{HT9srI4 zh~WV+Bmj!;lKm0@EHpTGKbT(|#IH^CEe5b`0$7)@D}UnQRpvN6NjizBk0%M?Nxlg^ zf~98MJn%9O5`q?Au96tw6C)kb89hl>!~!*nLGtQkQdVpPQ)`5K=LG_Y>SkiL1T?ZH zTa`8zx0WUPcO^7hP4>!)HD|Bzm{n;hXK>(-x@s`&-M`(<#l)~P0=OiBPc?>GTwoj( zPtq@rNAkogE|J>ZaYA7LB-IS^2Pa|;KHF)#OoEw`w#r#qyFje_^1lL9X{*fFEsF%} zL0xn4#!EoH8^EW>bm0ehQaBcQM#p``5C6dQbp`z$CjA|*PTaXmRzZJNR0p`%TBEA0 zgZ3*{%!N_?@YA+#&*OMbAE3jW@Y5l=ICqmrVd=^1#ioB&!5ChZ6bnC|TfE z%(-I-2Zd~t6E9#5SkE(27H<>{jE}Y^U!cTZ=6P7Wk^}9wl5HAjd}^BW^@1+0{jK2# zxOh#0VfXxT&3WkHetxYYvubrs(*09urqnY$ZhSzCkrNU&leCyfda zMd=CB<5$kejiV%xAUU9_KbqDZWlB10AE;0({qQ7&DJiNLkY9^Mg%FXWBzOWTy4cL} z1g>*ped(k>lLF=liT9;|%=RG~6wvLa>$fGo*;qrwZncas*#+gtM}z>Bvðg{51y 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

{EM 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𝔭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 z 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�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

fmiFqzI`~$KKV{g_-xb(k5WqZqB)xk}<+AWaW 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 PPHsm*64!vPUPm(dg#&W9Ch*`l?Il-)(P>^ z5je3Qna*89C8@ulYyIHtkH#t2B5jnO=-b^Xe9~-Q!p~S5EwgYfK6Yr_Os;fU9urC+ zY)NRq{@9x_XnbylR#hQTDkmOEi=vvOc;e}j5J$H0CvZ{!oWb2v3}#7GvFuH0XSsgF z`UL5e6zN>)lBBHR1^SlEtP3vs)*SZYs>(~_Wrt(39MUDP8ue81Dr#>D*L7~(^?oaT zUN56|BI}}O8~O}VAktHmYZUz_c%`7QVz1UNmbws^Pm$!rkeEckM(Y-rPw3!@N<#PIuYUSGA>6=>n}bl z_M*py5~Svya5Dl8GvcA9xK1^xS?U&DI<{>$4Q4dvOSBfSbgc)rjSkeHp{+@T&vkoK&^X)aFo%iMc8CIO{?>^|(0u-XEYk>riKcS>M z4o3;Y&?-JL238b34K7919;S&=5+S5p#3M!d6JP!>qG{zQ_8b?6=z74~7ms7~#ZjQp zFM;s!fgo_)UT)-R9lE_O-I9rt%vTp}2V+D8??GZk&Y~doEYPV`AOF)DQz#L;BWso^ zJ{1~2?PrA!l=1zFp{?>4|5?kD@@1{DE03{)7D%EZ8re!i1`-xE%F$Dza2Z}5n*I&z zeB}T_r*$%2`(nuRkspN-ueL;8Pyi2u)UG~C>heI^W9m@Ya>hY^i8MGpFuAJ@s?))!6xoOTjow@t0ed@p!GiXnRO9i<|{Hg80- zohXh6L;~^g5({csZv;aeXM62;OOVlU6BgJ9A_#8ch*EH9~ez4#}NetgQW&FF_;++kchEeDJ&Ly zZlPuroGqWo8ZBga8xf!5gyu4Z?$raztlA;5=IQLc!KjHED$uH05 z2d>~ILeeuFa9+Z&+9uj?`ergeq)0wB=lb@O1L ztQ;~zGP?k54+ z51II4yp8VYw(~J6bS(9^KNI720%PUnHa%A=S)5oxpHZh$5lfS|d5#`et8ljr>pe^K zW(|sk$I6o9ck&FJ)TB13b6V0G&WrbFQ-<4_b)BG7DsvMF zTyRL4)i!9*s@H5N6zZD)&K-DO(;c;>u>TBpqB$|a+-MtGFUNznj+g=iA^SRuki;}d zBCFEQWDMIlL-yK9;~S3`Itq7tNhdRs=xN~Ywxk!XSYD${1f4d&GYk7tU(+dHZ{GC& zv_Eeqg~!)G_2WHRRKJ6E((&qC>=ykSzPpcNo#ByuN;?lqq-PNgNhkx6lwg?Yl4W#d+MKtcTGDs$<%;Ho>d zNTPT7<;YdqATurhPLn3asXtESv+u_{QJ-F<;SiX*YiN=~IF_I((9gJ~u65vAecb7j zPVgvblRV5n`9}UtpUVnLrDN5V1#M>KVf4h%={=^F$oZJqX ztGy4=S>Qy454EQ&R)aM3$MVlK>De047j6f)Z838=g9C)Rbm{~a9C*-RLWBbg7Nn-G z8bpZ?BR<455o1M(8V5dn*l?l3f)OJk1UXV2#giyKO1x+>rMinGWr|!GvSCD!($ewt z*%Rnbp+MsWHCl9N(xA_5GIT2P=}m%Bsh+G_)oRnJTctkr$`$KXc>|e#Eo*QrRDo&B zrd{YZ;aRf(BHPXl8L}=~yKC*T<*P35UA1oo%l(@d@nEec``Y~}wJTW2Jzd6psZQuK zo=A=Ilzdfk)`pi$Moy?$Wopct(;=E_0 zOIyPY!MmbFQ?>0v9B-;M>%6ne+aOf4z*v{m6hBV=v{fUi%8L)pflBMtvzmPMkw!Aj zRBhNXVQpzU@P>TuNu;D3O4_8Rm2^AiBGvXuT(&3JrkGdz00qV-i8=^tNg8fu629eT3v+)7Wr`q-Ot%skh82ZJI%liWtGf^EWAl6g5s5ww}b3;2&INTTAHzD z_4cYew3P&qXIlM>kJjNh2U^LjuO=Bu#folpR$0Yvsv1jWAD6idb7mbb)rS^rRTH6m zx7e0!WVcW@TZCBHqi?1eQIn9DQ$~j7e(Wja9~auIw7w1=&p$g3UBl1q4oM^bNXcFK z<$2Xlig0GrabGEwCSIR@cZ(NT_BJ>5liw1HO*BvbdsW~D_`DN6177Q@AC%0cl$Fiy zF@qZuP4Xl`+|5NT1%hB_$dx&=V98D)1Rkn3*SfjbDq_?Eio!r*GL~)dCvNfGa)8pm z{q@L#e52gn!t#?bfiO!@@!)*0v@4ykCOgri-u!0QJQY!>c)OZg!2Z>{K3R}Ye6k=8 zuh_*b3a(EE43W29SVMr_s$FeUl(bS5J=V$UXC`!(6(BMgv$l9o21MQtiD zRG+gl7c@d{Xe&&)&2EYpztwG@;J0~ zk_SIGn8!&HpF7Ma5Ba%Ie}?63M`D^XeQ6VCePnxh#3KKS+pj!XR*bHbSvJHD43FhnIupqIZw|z#x_B|hm&FBWq}GBMLpW|i+J=}_h6;2 z0s;!CIt8Pv05?E1;S_ktTj&hsH?R~ouz_|g*HgQMzjGNAsug7asI7j=uLMG`s9qXe zLZMklCZ5!wW)a<9$ja5ARwgfjVp{OWWj*PK~{E0=Ul#tTgZcQJhq2kt(9XAQEGUN#yOn^8PZEj1WsRidtRohzD zzLuO<8YgFRWWPS{5O1CN(n5QB#T|dRHh#bn%-oNvYEnc zFEFQeH_$z{v7)+`SM^xTF72}_Ey7;UveF|0TdhR!GZ-kTxYMx) zv9}>k9$#x#mn9DAB)@v26fG-Wr`^+Oe)LR$x7*A4rcR~_;~6ev_Sh1ywZ|9M6pwdF z-@WWOzV%&22uefNc|0d1htML;iGv8#|ohs~G86Rk?>h#^m{U* zQ@iacrB8*wo^6yJNN(D#FWZx)WO#Hu?Ej{_*xZg%jB$H8eoF01{Q`C+Q9LlQkPJ45 zS?1fwr@!L?RlRU52)M}jKuyN>n(K8pv0X{$AIrDSK)yA6aU9=wr|q8%4%K)`-pAl# zeB-9BvLUI~a_aUMjZiG>qJ-RCb)wb2s0`Ai4?fbq{zWY%Enh|__MK9G+&Iw*GR^$h zbcW#Zm;+PnGuc)3fn9dEwdxzJFt>231Pkk^68XzHH1?dsXUtlo_}bwU7$vWL;&F#N zL{Et}s|4$dllHK*ZQgMkx7&opQI44e)?1tZK9RWxH`Bz36}qvFZOk@pRh&g@tLc`7 ziI8a~p?_B?=hXGTb9ee+in8~jV|w4GZyR^uooH|qU47ox_PJ#*TZSXL_53b(qFw(a zIZ-izlQy=z*U0U3(mS9J1^2lxf5_$M`@9Gft$_h&Rjvq|cU~kMLIx-E3Ec@+gUk8E z+m7Meqk4stYaI2CUp9BjtL)X#EmDmb)hOi|BLaEZ`HYn6T%+G@{x8$c58H0f|8!5I z#;xJ{Z}m1Q^O!KeK0BAbp(bPR*W{LaUU z@CXye#Uw76jwJbL$+2whE6gr zlBi8xT9D7)jMBW%<2*@S`l2+jZdiyT@^-^8E(Yn~qiU$|2f4H?<81CM7xzcOGA7KxV$;Miw1Q)9gzp)8#D9dY zSj4Lh%j+4TZ~aznS8PPIGHC<@Z!b3}-LjF-YVHzeI(~iV`#XF*7eyGdq(rhl!mMuOod+e-Lpi z&GK28ueE54BeRSjjWR$c#K%21Cb9niaK z(Acms0jaS$w^5C{Q5&7}^?DDy4s+k6lZKiR7!U2GytCUFaQHaG;Q){|gwQPy(pYZj ztx^ocl=4Qv;+u4>O#UR}m{K@+?RTCk5Ql=I=tV?E(b4p+l*ZB|cTmO_=q1xa!Q?^3 zRAQ2h!XW926rV1LCJ$DKXDtk`(U9`Wg0G4~G}7`2&^|OoOY}rj6h(jY4lh(5k%~hV z>R%)ca%2RMBnrJ z2JQ|}A=9Mf^$Cjf~LGsE#6e@}lxQ8W#Uq44h&H*7J;(&%n3IzSTZ)J5qG zNB%}+{4~T;LS{%vYe<|DQA72-%u}4c)Ap2;hRjMeH|PKb36qo+0Rgc1`1Apzkz0|m zRId}G{t*-ZA&xt@^P1Wev(E6T%25F8a%dPP4PDSiVl^shCESkD;r69Iw{U-!^+z=) zO*zgzj+5VVne?-MoFT{4zH zsZQ!fmSB)cWK{%XIo4!Xmh+tIkv^rJ2(>I5g(+@!EeL`s)FCb8A!mKoXDzV>H-=}4 zgJ+MnLVR}e2FPdM3u&DeX@{np-m7V$)@mE#!mgHTj}}p;MFnFv9(49;eKt0Z7F7l@ zDv%avwU(r4#3{xCXpOdLx%Ox^!)~+XJm+F<*LG(Kf^UPPZ*jIQuGVPf7TKbzTGZBW z7x!uZ!RELaw`n7nYK;SKDc5q>c55ZKX;%qJa%XQLcX7$|XBC%ps}L>d_H!{8XW_JL zuNHF)YIP;|X;C)|Z)bKhH)<_zbaD4}JNI%a7j&zZZrej}O9pgHS2&6nXRQ`hf_CIbGofcPkx7KgP4nfM-e7UwFs<#@dmm1(# zdf}FQ`>rA47J1*xVs@4x=yz|87JpmnYcWB7owjxRmLMjidI`9Fxwe0cR(Kz=c?aTu zgZ5;C<9)v;avzv&Bk3W?R%@-6do9;yDz|xewtRyYaQUKgC%1XOH+P};av9fv{nlvz zmv@C@*M^<8f?0S;#1?0p7KL3GXjb>VyqA8H7HVbKgkvLapV)bWG$6!quoysArBHh&A0xz|YWPsil8dZZ zxU!E8;YbuqaRXVA4Zq1wL+6V+$x`}QB!EvM6#0;21y*YUlEVv*64{U~8I&x+Bip$vkS7Fkw@RJd3QmCH6PqG^j&ij858i@Q|AYS}#^*_GcoH~VH--dN)D z#5Nh(JwX&>RBAC$8I6NvfLbDB0P`gxPK%KlWnLqUO)2LRMg6u|xDe8dv6&_Rh$Z0C zVON|28bIQl7Xlj6VV&DK9l${y;2EB6IUNq7o;8)7^VyyE`5@MLE9SW%=2@QOnV<)H zp!->!5n7)Y+MNj+eBK!<{28DV+91~Xo()1bU=*8lctTq&s>bYPzOT+MEkQqIr6!5jv$a zI;fEvqltPd(%Gk*TBt=@sjvE|tNNfb8l8=rs-4=QnR=;1TC1&^tBZOdvKp)>`lpq; zthbt-yE?7Ex~#8StV0?hCVJRPTBGqgtn0&_NxH9HI;0olqz9v|m%6I|{Th4{`>F5x zuCp4V`P#0rI;bJKu4U!0=Wesry0U@#u1R{NJ5Vx6TBnD4s<)!885*o}`lh2Aq1Sn_ z|C+Yv8M9kEwY^%k=^D4=IuL0Vl-}$%+TdL1`s(Bl=aXOVd z6{M~EQ_T{)J^8w`d%G!4mblxy$D1z)gS^lCyU|;{zni_++r8U+yC?3w=Ud@66~5_v zlQkrZ3uM3XyT0!mzRmlU*6uI;`z}7>8PXxa7hJ&`oWXC_!4(`ZBK*N8{J|aE!YN$C zFPy`TBf&Gc!xucmLwv#sc*9RT#2Y+*HGC{myu(`@#bcbqM_j@G!KTI|9L6s^#~Xad zU!28pyum?S#xdN*d3?x=9KspA#Vwr1k^IP?e8Fuz$(cOCd;G+UqRM03$~#=hogB&K z&&yAI$GaTLQC!MVyva|T%)>my&z#Lqe8u0~#ofHjIXoidyw39+&-Wb7D}2V4T+e0v z&M7?2`P|T{LBr!5$eo-T!hFjEUB``_&FLJ^P2A5t9MUDc$aDP6cl^wM5%T+|;N6U1E0;XKmcoYOHK&|y8$w;Up#LH_RH9{fSqd41P=9oX+-8+4u6h27YT zJ=l@G*nNH2hh5o~-Pe5`+KJuSjlJ5RJ=mi?**QVihn?I1u|3(fJ=t}g+poRcrybj+ z{oBDE*?C>u#a-FY{n@`=+TWen<(=KRUD|)0-}k-Q**)BU9o~!m->;q62Y%V9z1tW5 z-;dqh4?f@l9^CDn-wB@K7yj5CKH|y!*w_8p>wV+zUE(Vq-uHdvAwJttUgQn_<53>u zEuQ7kz28;-;sO5UUtZy3zTI7(=B-`gZ@%Xnp51|d+%w+hdA{gR9^q&HIHw|Q$FqgwLab7zVMY@@rfSb760jbe(dAk z=t+Lud0p;D{^2`6?ET>%vf&>_fAmG4^iLo4Q(yHD`m_K0oxl36|M<6m{Jo$1!GHG4|NO6?{kz}&!yo?FU;ddN{mp;=dH?4;FODFycap2N7D7h!LVfffX@sBskHd$d4p1j{L|Gp~jOT zL0;@Q5~fL+Cn>&USu$lsnj%-OWXaPc&XzO(hbDY@6l73|KW`3g+7ze8s6CUq)M?bE zMXDZsYBjpFYsj5h%bGolHm%yVY}>m1`ZliIxpeE=y^A-m-o1SL`uz(yu-dSK3mdk2 zII-fzhGjY~86{&{U{*;Q?t9=sg+S*$9t=O(=xyw~mB zQ%^=sUAJrS-LI=|pS~*dw9j)7_l}Y@*4T{u`~MGMfC3IkV1Wi6h+u*WF34bm4n7EB zgc43jVTBf6h+&2rZpdMW9)1X7h$4>vNMea5o`_mX0|9#*?1G|KR#5jMi)@XAj1;^%<)h*iW2l`r8HrS zs5K&}!$k}t$ne5FFYvH!tlYx?^%#HW9W^UipdE{EzWVN~1*mBG@Wc}pOgymw&lH{PTr0>y3Ku`rWT+9K@9O^JYMm92(ymqXg){IU% z^>NX%vm^|pjtq)s>ZuAqmDX@f;0A}(ionN4^aH`jXL9~ zlT8&3oRdvD(quEo4vlXA%FftM{|^8S4^_wj zfef5%AwvTRL@xpW=ZLU_4m_as%o9XFAwmNpfB-=W=Q|66K&)OsKmsOkynOfB5P&lR zs6ZVM;6Vcj;IpCZAaOpp0OLTwhksn)c@h)>I(#64n`Pqy?t6g%|L22mG(ZL!G#~<1 z;J%Wbs{uz4Aq0$us|Hko04CtU3qTMKlx1)ZI%t3g8sNa;32}%-EaHGjP(TAxOg8DJ+ zA2J|-H@v94Y{ZfO4o)P46VJj45Lha84$Ou+K(IEb38ZE_V1U&c0F4IdfLT5eWE{cK z0BIxu0@+A_2rxN^CPuOl@t9&9H^nR-_y8TBJliQRkk%KOL0cz})xYD)&o0|CldcQy#5u~BCEh6xBD&3~L@jM=*e{bF~(JkGI> zw!$A93DAXfq)Z*-It${erZ|B>Ly!`T+CRSdf;29yX5-kD3eE`vaHMP;Y21bgV(`L^ z5!9O^;6pK;w>&nEYhgQ(0SVCI1`xzxb>q{eM?VVEkdCBy1Cd-IGD#1rO;831Gsw;I zc~7b-PZ|yXSlijw^((Y2?*cFQ$2CZ>1te5sAS9sL$S%OEY&4)Beska!2yg)p{38M& zh({Xz=uNZu!;2T#Mgu#RF-4Kom`A{5)fzB0E(nCG)1az6(zpYvZIufR;De%`Ap&n) zL!|->rmc?fkD4ybA3PwyH@4t|7@Us_wG^pi9}C$ck^le$xknlSz=P$zAR7>Xz(3Yy zz!%WehM6_sH8uuI2RH;CNRzV>K{*i!w6Z(2P7;nr`0?{ z1PBmR782kHL5u+s0Fc3W&~am1_*w%1KmvI*z<@`nMjEw{M#~zL2X(Znq8j=FBy1s{ zxH{ke#@4lsUoOuUBoNp(<`>!g>UY2Ty2iHP`GyzpK_1VLAZ4!+-va;R88P@rJYH~( zYQW>OFTlpOBDj`#2m%vXT}XllGL3=6V73Ff#)L8P0!&mk2KR7F40_6tYHULg3VFdL z5^{+cNY|G`g{F_U^@11Bcv}>v_+7?z5l&4Cp`$ zdeDR}w4o1;=tL`e(Tr}iqaO|FNK1Osl&-X;FOBI;YkJdZZq|B$#pzIsdej}d#v3~S z+!j+`1J$Zt2&zd9>sT*3fk0`DSlU2{e7u1rrf$QjG17*75P}f99tf_1pz9L9p$*<_ z^}J*a?P$vxcQjcRQv~N0W(~zsBQXcEeZA^%hx^p4UUnPIVGeMYyTjKwb{k5J>tVmU z*UbLM8`4ega_3vs;#T&rr48_aV_AX=8PtcLM%2o4=$+Z5i&rp}gT4OE5%2j=cbfN7|M4x4kAsXM>{3?O^xKsY(eAJB4^ zn}Y)ZU^x(S{eY6!`vo!hK?uy@Q*(HL1A!;@8%|J!n*W34`2a%Uy&d_GBmVTJH^&?D zf%s%gefG4!W$Nn3k=lwN?-}C(1k|C!`aKQ=bEqEywo3*bo^J~o%qwH5=M5udkc}L~ z9vKF50X8N;VCQ;YHqLM^2wdPD&Hvi_j`jv1VBi5%Ks*K_z=OsYpaUHT0tGy<1sB{u z0P6=r0Wh$BtCo3ffB~!EQ=o@#ML+?j<^mn?b3gC^4}bwhKmz$t0{{sB1QN)96p#-Q zKz|fq4)hm*wMT*_I6SZgPfhe$A}|LsbVuq?SW2}xUjqW_t6!voo8 zLFbhKd@wr)bv8gFLC;e>B9wIua5j0zQa%tuUW7q5^#%{n0anlf1z-h&_XZvidG?0^ zUHAaLb^;O52An5>_E!YIc5W1a0p#`r0Wfp{@B^zB109eLzE%Vezmk$AO0wR`$dpL75*9VJ-0sQv^ zE`WwWPy}u80~mM$*BAhA5Q&tikNcP>U&JW?;7R`=JDdOk%#ukwus9v2Ja0vW^B@pj za!ep_S|&9AGPV^{w}N38um#m{KFec6 zMRhA9gD^)x0P6x>@xVbKP+dN-T@V0cH)ucrzz4O|P-5^x5b$CR00J^_4aSuag`k!L zu?5c<0b39O|8NAJ=>hjpJX;_K9`jkBxdomX16fr)R7DLW@R_06g9))NTfhTn0G6t? z1th=+JP-j#AO|mS1PHbTl^Frp^#(E!0TBRUX9=CrSt#*EW1n?n1MvdekX82(1KW@g zZ=fht7GDv>oj3Lk_3#f7^bJ8q4FeHh-uYSEP-FwKp4EwA+j0-`IS|zlWCI~)+CmWM z@P#8r5SK6q-XIS~R$#sM`VRpC03rDV1rz}P04(SL z9RQ#M@&bp_hM*QAG~f;S zsAYaeOJ2g_@;X9P!a_{=tiGC7T*7K>vMMx|!q4RV{Ia&ZmYSr7K4grRsZv&KK1N*5 z?)2VXZ2HdJ+OE8^;`FAfw58AKnxd@sf{f;}yt2Z|UWVlQa(u4N%*M{tes+A;LQKYj zgu?Fh(*FF8#^lDPv}|;OuCBCdn!IvCL|R^KVxGjJ!pySLt>4wzPu6@ZR3!qN>C$I!ylU^#1hxeoj>O{QQzWMDqOn?(XdR#>D#W z^!CEcmbT1JW^DTU{JOrpE;>}|_Wb(x{4zdN{`&kfI#lld{O;=fW>O%?*udoQ=$>q# ze1@FnKG@vcK$Jdw_K@WEuJG2b&@ePusO+F#R&+ejG`=ucn7V}AT1d$Bph`YuXvDmt zKG?A2(9+WQ+?p^zVr*!*@G?4Vpyc4}?4b5kVEpvx;_~=1G)P8>ggiLZHb@xY^3dYQ z@Y1O4Mq-%KisUFXP?SQP^x(|&{OH{5@Mefy9EhY^xPUx}Bwk>cn2hN3^yprGh?wj^ zq7+!9rtC&yK;~kcnvBHk{OG9o;Fzj3Bq(TbV1)Ga(1fJm$iVRA-tgvD9Ej||&h*e) z=(I{Yl)hM49?-PhsGRnmtnA2`>iYPagrLl-c)l`}N=7`$(3q5LOezpmW*SuH@c1ZV zY?Ll+?DXg?Iygdz;7Yj6th(?XLR6^4z-C}j<``H?ELbQ$P~3E2I4D$xjKKW-=q@ry z;NH;c?)WS^kcd(o>gxRZ{`~gx^zQcj^78!Z{`~6x^y2RP^78cZ_WbVp{Oh$*Z{POnn>hkpV_Vn)Z^y>2b_VWDh^8Eh({4O>`00008{{a69 z97wRB!Gj1B4opChVZt;DBTAe|v7*I`7&B_z$g!ixk03*e97(dI$&)Bks$9vkrOTHv zV}hLb@52d!C1mQ{$+M@=pFo2O9ZIyQ(W6L{X4LVofB+9p1a5!;p`eF<9s=oqu-&!9t#9!bo_t}L_ySDAyxO10m z00gpIg}-$RA5Ofu@yq)fJP_?R`0?k^qf4JIG(iFvR3gu&&b_<$@8D0S|6xZny!rF! z)2m<4zPNG7S|l1w(~A8=sl<(C1*v&I_nfNAEL z_>sxQ8fdoZ=6hWD!vz;_)@kRRc;>0+o_zM%Q<;7SDrjkI4%Ep%5_p3fp#LcP4;*B& zIq0NKa$!Ix5+M4|rMS^KW;~E~vIZB7Qflgv{t)ob69Of$L8irZ|4HVlv>JJbKu##& zLaVvC@lPioTm@;F-bh;Ou|9$$(4qePqkwP*c%Z_w21tPEKcyn8t&Ik}h3&VS-R8k{ zhZ10bAK7mEoCgGWz|?*m93bxjQpjs!xBuL6E~x*w!K!iisyiJ41gpB=ya#X)?*S>& zg6OPpID{JkCu|UvsBC)ErNGY_;BmnD%{#>dQ^cF1VrvxJ=&tYD7Jvl$2=LFClBUbC za5m&z)xq*E@Bjc0Fnpg2nA&{v(IU}O!WxGneUTG#8r@Q&|M&sxaBk4)^m;awAVITR zTLi(iVVCVu*KUApwn9jFy!P7}*`R^7a4+WU24fijM?6~B|Lu269T31bi+8XJkyv_* z`S;>05rBr`D^|x95%N)vaCpkqbW#EW_#u#oj3iNXFb~G`gB)sB8;0_(g+&QHbm)2SqK@&T;<3jsj@_bgKA|iE<=~<^0S! zW_pqm+QW?$fno^-BGQ(g0}N$M=SW4f0P4gvB>uciJ@&Cfk~n7~`S7AT!663gR8>_? z9qMC<@Q(!m;0j&k>T}pQ5{TF%prEOu1{`J*ew1KW6|tfi5kd1R2p z#)yvgw5Uz3YFEqJ*18rW^-!Z|UmL-D|A6BjiluD?HK2fL=@x@PZG$R*OWfib_qfPS zu5y>l+~zv>xzLTSbf-(*>RR`@*v+nXx69q`diT5F4X=2|OWyLD_q^y$uX@+Z-uAk8 zZ9Sdsd*@5v1w~YfE&T_6>l;2Hg0r;$I4Xa;_eCojSRh`khdt`S4tzjy!3};egB`5k z2}?M_6~-`xDZJqebJ)Te{;-BY+~E-G)Qcicv4>Y|;ue?q#UzgLh-a)~7t6TDFs3n! zbByB~@7TvC{zr#}Y+)g{)WJrMaFUT+PbD{*$w#*0lcgNxB~#hSRi3hyiLB!g2YA8% z_;8pdJZ3LfxXfS{bBe#LMm3Xp|IKF>bDO)UNqZ#r&K-59p7+dWK994{eKshd`7G!` z>-o=yc4wj8xzu+OI?;!Qv^x>>tZ+i+1GaVv7^oHY;!x<-gb7lzYT3|mz&(%Hut!tjc#nW8{F@tp&1 z?{k%&kcO4~Up^x0=&i50)~@+s_0wX7UOj%kkBf!h&04)w?Yqc)T%`7H-on0apt<2s z?rW=syO))fTVo#Ad29T#&wBewv|L{%$2i=hl}}vV5kG`dx0jr@2BteqLi&%p<6rYvtwfchi@T_k8wd z{)yv%vT$|e7{|poXFoQq72n+XJ69_z@ z94KiPDCH3-ogCPPV1L^KV^MjU^8>qIpJe&NFXs?XunQfBKCof*gdHUHj6n3E2P$TQ z7|M|o@F1frdWZb4o;4HH2aDO+XTd!TBS3mLhQMQE)B(%e;ImbN~*E{5Xa@2ngaB=;}N!SsbQZ4=its+Jl7& z(j#%o@Y{;ON&m>=8W4{zyOU(E*80hm1w3a+jCjlN{pnoi(g^+>_Dp!vKb_M_iDbXoraeTF03p5gcZJfwJ zoM%&{*j=^TnE2|~5eN0-D}N&0vad)F#2?#?mlsSpp^~6zo}lEJpv+21P_0c+A4t&L zOu!2!YO5sbnkSy~Ow>zBG^kB9tW7|@hK^5_(qkN_(}S@#@}Snf6Ib_(W1ij2L4y7W z&ugGC1Mx@?i^PY>VvbpxAM)Bj??P{_m>?_+wMzoH@PIfy@H%*%l$c020{`Rzrzx<# z{z61+_ zNblNArwL~CsATk-XMFd}*gFne4#}u9j$$!_a2QexYYf)RK>umWXl;b|>S3_NSYQDB zAOWin$)qWu^{H?{c{sTr6flZnnTe80iw)a4lt=3Uy#GZM`FgB(P2YYJ&Z0@0p~NK}RAN(W)-fE2k%5L?tX zD}E3U+s_cT!kJBVBeZGJ`qU!neo)Z}QC@<5*;Xh+1J3uN_XEJRact(C^`J5wv8ln+ z9-!q9!x|xuR-~8zdRFN=qozF@g;{g=PoX6YYVp{RKjw^-D=^5dsEocD0|;h zIDSw#w<&YloURHLm{KgbPYl-{VNIz>tf^p;D)xR7z*=OFVv(o;z!+DI97FD>0SEkF z8Do%g<`sMCArsK;4e0-D6b9#(hvh|Ltf&Cq2*#ql;-JM}6(npvPMQutEIb5VvtLtS zoaU#nyj1W26|iBT`G^=hdD!tAutq%W3Xvr0$$c4Azt2^`?Df<6u)={EfQ^nEJwF~0XO z1{lzah7OJbCGZgj^f>1#w2OK2qggQ{B6m!-z=$#Id>W|xwiXEk_%KLo3|yKDSZ$*f z=y87-Nh{deKU%1pj8`lYcxPY27w!7l#xlBA<(@I*83wv{8-1Gej`zpAk?nVMmT<$k zTEoPRhAHocnY4zvw++JyFL418BF$L>G%(I1D@vefM5X)y7TSweS{V;pkw?4u$Jo-z z&%04!+m{853gPm=IRf?G&ln+_v-<~LgzH7OZ=o+ya~?Ev^2uQrNpTAV&VxIRAw`)i z2;A^Q;?>JtURbwY5`)lmSOmeMfk%9FM4Xgt3FX{2=kXjZhi5Vbv%!1^u*!jRiqiaw zyXlx5s%tLi;7kLLi;e~@x-Ip^K{?bd-KGw`Kod*sF%p7>Ckaxs4R=)cM;NcsI4^O; z9?;{&VOn_`;kP>*JkmdUsRynQUU+wk9VS6xKREdVVbA!uXk5k5vH@p0?lohPpfNmL z9gD-m;f<(dr)Sm1C}S$Zih$fNAAeG<9ccsh%j({>6qfnXo<7uGZrH)D0KAH9O_md} z!XR%`+YM>$vim;4yJ1z~S2xM+RgH*NpH7R3Psnb>Lw|z7+l;bC#Fqcr3&CgG%R0Zh zXzz*m3@V^lw9e7_&$SX=xA5p&^v~0syyKmp(e@*EvQ__O zXXp!a=L;Z8>qcCMkHL&&Xti;G;|4l@78V~ts~N*wJlkpbisqo#{dE?x?*x>mjCp7aVxz(Rw1YnEUaKKE9`V8*a_Mhik1%-$*Cd$crE=%!)jR;?580F}_x5T=VWrSC^rd zU7`Jin9uKooV$Tw%lymuzGDGp=mAk{YW+Kw2-eykFa=(j4mR792NhtTI&QGr7=BQ` zgS#8~dw2M{2`!|tcU(?y24 zG{<SHv>Mp2x*02<0B&7?(PB;I_%RipkEyBjfzzpBvq}rgzZ4 z=Es$MC0;-IsoL;UedMPm^Cw;m8U#F{Ydvw!cS0|7!k}ToaAe{VbHYe$@`~o9iSxzq zccc2nqhGPb6EDOb%3^FO!wSax1&sCwn?=nZ6*(Xeu4ag-%fkgR5pfG#`{p|&BU;>{>YgvY(_aZZ70u7q2xt&ku8A!;fRG9 za=A@3-kj&ETFkpoL&jMPYl>h6zRd|~#homRYuDiVFp_6Q11`wV&?LsMjVz2X7wFbJ zIHUcK#1^%%uwVBV^#VArr%%m|a7RN+b(+6dt$(li{@%#^z18r0XXH1N`5O>tfM*zp zn+%j6gFTC}SNvjFE#pw*OCf)L&YN70)EaJau4SL4*9Yf>Mwc|QL8T0a&*IWvo9EAU zm&M`${GspCFEQ75(UBv|SXMTCoq>|gz}#;6>KK_6f!_ML-sYv`R>{>f*-{!2ShM?f0@=T7ZZb?R3e)lk6-kW&VUX zm(l1NHegm3m?J*GZZQx}#LUN+v0pPIDq?WIchR(qFKsDJ`%Zu#8xU&-{5QpM2WZH# z7cXaM8vs{_wfwAi)@JJa&GcBF(jBbK4b=Th9lxcgvSIC_aj*8vZ+Sa`N1~@#H28y? z0s&t-Zu_qR%+Y=k#F5ToFs>-sCYmGWQ_Vc~#1Hz^C)8;ZY)jGmB-m^z3!M|Y1!TZf z<*?s2(9OhAE>?u#eF_5G)zSO=iy)dopB=kB=z1&7Z=QMA zCB9YWLJ`}o)F{D~x$6|04>5;se4jV3ynDStV*TY&?NUdz_zNqo3s$wBe-g` z4M3u$Ec+QYEfKuO?+o=X-D-!kB+kZ05}g35AIWB z;1S#=#KyJ}uM5&(34y4+cMqbu2+dwY(dJZJW$0jZcwBV3E+{VP@?14>Im>s$b?L0Y zNo}?ufwT@FL+DZT{$O5LIayGGX{-h3wE(n`;t7UF+1V0#I-u^<(^f#q65pdQ_qx(q z=jhwjlK3|RjXGFPkwW~dP(ii>Qy5h-p3|aE}Z&fVsiD` z<7=ij9{;&!cI(+=Q;U05e@rbOeR~WYef;OobwUuYo0)CYsWmgZgllf*chVlOnLFe@ zbF*+Pty;5idi~AqhV$Ey@1@Tz>E~r1e0qF2D`?T-K*3?6!n)P!UOl=vxHtS^5J>;_ zTphW5lsf!qBgOq@;M=csvJWezkPzB<0XM2vhSBK=J<_$viDWkRg>V%~H`>zy7peZ{^arZ8}7T0`)@se;x>5i&92+fqrNBZBi`$~?(`sjUyt#q z(@c+vgzLVZQ)%u@&zaokzFu>sZk}pU?8A(=4CWuh;#atUZ2E znzdFK!nz%?sNcricziTld`rAe>!*)KHe9$Zt9;@bR#GI`Ux?hK)kC6yVn=T2Y@F9n zHS&+uk`Mg21+A?i=z%C_qB%lcuT^)Z*dO>$lu%XEB(tom*YkbiLs$Qnn@>S&XWy&( z5?h~8Z94~-pz4NBNDP}ygPTsc#uU_B-*I*GiXO3bdNr~d8u<*;LkJU@bVhl{1f+G# zt-T}Ti_Y4*QpeOGxoZtfk4bs~Ax=nTS8CT#Q%NOce36S2gE?hWi>r_)LRMJP(DG$v zJ+f&&wPRwdx>MWQsiC31yrdFxz=HCYhVMgxfzcz2^QqOH%L{)N7UoA{^5S1rdWGi2 zbadwAltFqZuV(VSsdI$6!)$Hw3QUwbVK}w5vphaA$=q>%=mQ~?tftA@R4+t<5G|d6 z+;DE~uLjPoLI}u&>}hLfZEKqedDHUz9Y>!v=3gjWhJFNb)$)#0u;teLjM6!QIn92^ zxth9G>6Ut_CGzDx8{q}>sdYZ7^LSV5V93JGS>x9-(|c&N)Phd2;Jm=-8i)nul?*SL znMqqaK{j_l&jgY?2nPwKuFS<5Ic1WVyvZ7E17f~$>O@&|qGqrpu_zDHKv!RnIUzI1 zAA?WW87L<@SR2jRrO%m(oRA`%P%>+3V5H8I3%ZIR-(1U^pD*!%NRi{5$>RKmgAqvvEtcRcwGC@dtAv*kvfOCJL5RyUW+{7^@6UYJo!G#bVt}++@f*{0;5G+ET2q7cH zg~yaEnb0eFZRdYc!m__g=>1<*!rrCJR73##f2M@4t6y5!;YSfU$`16bfD(9>^l(Sj zzf;1*waOOZepsoN6oxHuvk-Tn<+1(B-0=UUglp{6;vC2JF6=i^uc`R?FC{$jLCJal ziR#YUe<|T`#j)A1MgLI3Q)Yf?R-ZEep@avosvj$pKejkZ4caGe?Ww1K3ukVd^Wk`xB-*s?fiC=E`@1lfvrjhDl)T@(8mRs)O_^YlmQ^|jB zd$RM{6%uq2H3?J>iVbl$up zYG1SQos})4E;@wagM3w59>fXI8Y|)q$38p{MIAZ%G>(0u@yt!f6!ZI_;q_wn;~uI< zT~Q3;HD6(jWB@|40f*r_ZYL-OoW)#UsG+%#9~AH0CoM*V!$^~7uF@^i*trd%^Vmwn zz{3rn-r;87ZM`5j?qwyly-Z?x*p#xr>GlrMdao@q#J;sw7=Ti6AXWJAb^7PGD;BoE zK--!dD`$B-aj1`~dv?%#WTu;5x7zd5fmrxqD`O#6C%NRSiMt{Q-Z#By%oFdp@c7(y zxcp2X_K-ZU**DuI`2nXllJFY61P|mz=V0DFN%%J}P8%E$U@`7Qeii~wnAuVV7e>Nk zS3$ln1#O;dXI6c{WuD~u10ax&XBQy_G)lF%U-DW`AamqPpV#lWlDeE& zWSB2|y}iSro>Sb&J5MITUd*t}-_;%N*2R*u!OaqWi$2 z<$2thNW;^eAH)qziekUV8OEA$D>_rd9vYmu8i7k7BO-#%Ky~R}%2<$Wl_56@M?$L? z<_ia~b7`&Uprfgh9$Nk{w3}dlm3UMndx>4REQyyR57xOy$3W`}!u`PG69mdLj!W?X zR{dIOj*UHjtVG*qLZ?u$d%qRyW7oTKTDi!dQUcqB{wAR{O8CWj-9cmH4`T8hZtOw4 zqW7o>R~+nPjuUJeM4=tmUX%B~a5#TquqXB3DdAcb<>?D2`wKq?S%YiUHJ@KNyMOvI zvbFYxy1$$l_HRnqr&$6ip}RIsPRhD#%W}dzA*`R*{&Bv2pWhDS?bwdZ{y1~P-#xUq za(^83;#7~yu=Vw3N zT#@x?SU5E0|MM*!{(HRvHF8`-Gd6{V<0zudkR;pg<5pL?C27e~$W40HAig^FglhHVeu4Uc^V zug++i@*Y_baG$F%U#@O9PQU^ zC-Td!&^=>PjkQUOZCEY5ygPS;wblQnVeQSRfmqQWal<7!sEe`(r6TNivX*dYL=#N? zn9N=kEtb909}`l<8GUJ**3RTjLC&F;qX%;(b?GoEhR)YUzeF3L?;!|ZGSyqwW|6q{ z>ChYlbYGx9NdZ(}<%yxfk5cq?xp?{fK@J;mJ1n5K8NnVBDAyl&Y$H%!An1g0kfK?T zl1Gqoa*%3GkU}jnd@VFXgYp`ykl&M6D-W1#O)xX?y$ zMS3u39KszC+~Xc%YO>&gnSY%y)RKrG;%ZU)Go@$wL zbk42dSwi&3A_1@b=&p@un!wW@<)^)7PrrLS?N5F>So3tK|LMraQ@TLRxN^*dS+!>DX&M;6Z#WHXJ+)1ajGclYM;5 znnT;}=shH0{WO*r01gnr6C~|@f`?rEfire6c7G6~f`t_c9Hf}?0lb1ThZacO38zF0 z6tG+CXmulBKKb)9o09A-d@OXPAG<#=eqCVlIwt5KW)BrU^~$t30K|GG40N0G_JT%1 zaqw)PhZ2|t8mJ?ysUe~P*)Xo`4;yHEOwND{0t|pj$zuFed^?EGg>53? z@v!~!sQPZS7GCT$5h2Pz3m74Pegw_{2(eTNq!A!vBtRsA3PxfNh#|K9mWs5HE}bZ3 zTyonaV8B41p>gr-Rc+Wrx7Mb0e1k218Ek%OZh>P-RJI4% zBel@0uFz+&(08lQU#KWhwJ6vkGuEtVk1cwg5fIKwEn24vKefOaGSG%(-Z-z~@jLD_eA=LJ8RmYu_Q-WEN~sMmgex?dc^M9Re=h`7{MKh=^oJ;B9}5J)smX z&sM97x(Sr*AwX_~zDq>@x-Pa~o{ha5jgZIG2(fo4!~TG$Sazgg1=y2E+|x#Sud?BV zLRfwhh@l>1w?X2lU|lzG+7bEQgZr#J$W1R0Hz&1_v2xEv;awZ z5DS1|@Zek3=%v&A=-R}gMuCGwrGt3NpbI)}3=B)gNXoN?xWM*BQQixn*cq4A_r!Dk z5MA>}oURb$3>W3Wzd({PS6B>mSt_O5G5Uy#xcxzKISR~_ER3XHqitv(Zc~y}!{{27 zT-vTQ60W+UR%LRd%GA5cEUn7oZI$J=iu_(K&Ake07yxFJB7g}y<&bSDT`0Mf*AtJ_ zC&O2PkZBr9#z1hLaD~VV5QV^f7|^}+lqfOEBogaDlNN^w?+fjh*A5K(o@{=AJ}Dno5|9EZFzt<~{6rOqjKFRKi)+ zr|k8g?>&ixAw*c%u-k)Gf0Z!JQlF7LohJ5EP5ji6s?(U*UpK^l8emTm3p=)jKNN+p zL}AdBe9(r?j`Y4NyYvwaZ9(G1PL}Onz~EIe@Mz&(8k#MglgEj={|6P01xvTlaFND0 z3aDLq)FUI5q$Rq&s1Ys!cQ+8ZV1&{|Gz*qBv4GUfG?a;quxT)Q;NIEJ{3epBh~Fyu zJ`w#`zuAI{FxWv`;5gUxgcAl^3<+!o>K_c8iWMy}Ig!HajPsbKqCaULY}H%sEL-pR zv^u1>I@Y(AC|-lx0Q#R=U%9~fH_)fY*q4udd^Lut+>(4eRQYfKR5QTvsvTO#!7;1x z_gDkqK*3~^*~g;vj*|Kg z9xQKRXGf6>sJPTY@ewk{eypk&8m~KAo6c{OZecU4-e`z@KlI5@A&<8r*ydj66PJ+a zrMxzEtgT-Ct8YzT;Ne;9DZJs>ee~RC?VSPop9vD7mjuGR7-WH^&lQIXa(ZCrfm9v& zHg(l6_Qvm61ek_D43Un}V04KRj?7RBC_|`X&o7rKaO6=wK72B93+=9qJxfE~mG2Ha zQs0@5SfN+b>$xx$p)3j7(-Hx(cip9N;Le4fJYxYFM#Hox=UH5rLR|2CwCF9C5wO)Q=5yS55^cZ++8=?EF~q87lr9Of-N66@W!&auggL&KriHYSM`5o) zkO_N4`S$znSpmXZ)qv`uY!nX%WQCv#3FL_GA&T}r)aY}y>T8{kr0AhLx6uzApqncJ zD@?zYKWC7}SHCCye6~8$-!5fF^~W5QNH8e2G5U}wIuQG0fa^qO^1Ffb;epKE0kY^| zj>ce~)nLJs!J>@8l6Qk;!-M6!gA~ypuQYyCTKy<=4f(M1)Qa$}*#&ir_H{0f4BDVV zcewY+u)e zRyK$}HficRX_h%@(J%?soz~1rg4mR;=9HcF)E(a`hs-I*hAF3!DQ8VRCCaM@4cN#* z*r4jPr~=|)%(M^;RoOZ1l{?_CIiubUd)7JS$GSArGV~#^e1@wLV>Le`VgsBeqTP?p zvX8MVDzvj>`D2~9o{l`#n&-b*H!&aq8cCnq>%zv3gVPk*GxkAoU)6X%7 zaiA1q?oa^YbV3YunEw>;byxy?6!(i=0`ze}e6IcJtMD#aa{=!%D&{}hdkO1CqHm`! z>}~w<&TLXa$oTMX;T_sAdpzv6e2;&LK&u!|j)rz5xA_b8vYh~KlTl*}3;$F?9hW9? zt)|=f93iJ)&ukciT1^ManqFmklVuFHrlY2&}djt{DxafQG^k1KVYyp^i@lYI$ zP0b2*D7tsAA6Ci|rEgVbgMYh+6;RzHCTLFv9#Qy2&on#uL=9iUsFL!{$Pl~P%3NK6+o1ZEQL;cokb~d_GQhKgMzWS2oY+=?KX3)P>*43k39Io6&>m?t zPiG`|XQz4a`SXT?I*CTQl?aclly_zQuG#F5&I;b8S-D@o{4irC06R;-mjLfKqDMDk zSQ{kq&G<8$i8nWs{5F&Ssf44OnXFARS`(+Zt{?$krcTyA=fG~&5l5CKyNw#ixqwi+nkK-h8^0?DLD;{)c#H0 z<2i7J-R{`gW>&-Q2y2%v&Ky6(oVdxH@?*|qG3VYh=SP`~EG9#owS0!PdXu&0$LdQM z*>yf!9p4;<#@Gz*2zQT>Q8XSCM|1Nr+vOF$PX9+G>~#pt zl!|QG=5H;yT4rA5JURI7*FTi-_S_iZi(e!v{`QaW3pZ+9CcE-)T)&5uWRYf+`)wnv z{-+Yk#z}63fe}dP1Bc|lDWP+Fux!KLNai)8wt;^s;qGJox8MIyDWMih*p;EhelQVI z!XuRoZH|+@M4kUy<@V$8x|PSzXYSTs zzpB=)eKM-1v%Hu8^c54 zbw(!@{VQW@Xn|5UD2;=F3bHQ9sLmPMK%Ew*(g+?Z(=afNN-QR94rzOqw zVbAyR^iR`JM+fC_sU4kAQx{oKncC19qGj`{lLq-&$sM12j{+f4gPOPUo(ZUPg9ib!#0yH`pvVm>-ylW%+qR8NK&>5=z(MgF6uHT(nm``LEGWwv zpNQ}1Ea{kl!nV1&4Jdwtxa*(&o0pf5?G;xDy&xcg5;%yyAoTiMRs93E{?ZZTUjMKz zh`t~LW6rJqZR#NOV(vf=Y<2F0+4b+S~e$OCNFPvLiulZSE5IN;Hs&s z9&hUau^fSrJZGDJLQ2=N#(z%TwTMQ8I=W=N^uMe{G=$1J2&K;1>Lr_qaOjy7Y4$sY zTS8pdL!)Za6rnCo+E#teR75je5@NZ?dAgjkA{6XFHDGu&b-bsoh|g?`PKO{(Ravnt zIvs*5D61={QBv#6pz042vJ)qq=oWgGGnL^j6ggEMsF#DPJSfkD+z#SENXsB0OO2k% z>tw`O=s{^8R24b}lER7MZ4JGtb-|Ew{l%@dRS2~HR&$Vh&CNrm1)&$z#r-Ylpd4-s zO5hCtna@Ft3IW*o;@`dw;xUNC{ulJ>h0se|{=bM`iR%xvkO7DPGxSPdiVY0mJ&WKo zt02=O{f?A#cINztG_LyKt2;RG1uD04`}Deax*ykpxox)NFa9l!8*3I@=fh}M92@`l zX`GL~a-YJ~ztXtMyHAZS{VR>5d~A*SR~nZ!f$@B)Jz58$*QNT6Ne>YN63Y26deL&x z-^dcw_%`=^2)&>*E`c~W#>(A%_umD*pft{Ht==Y<=epbMA^#lq;O6IcNuF6e%7n)J z6sPKe5#i1HrO>p0q1QjsxXL?O{wRyVbok`T7vR=N(#)BA1_4rhf#(J%gq7*?)NB@= zfOgK6k1Qy>QomG~mnW2hzGbMN=zm;W>hXi@E@c^ijD9pLf@{erpE7ZOT z0bAy+BCv6c0)lN*^8V7KP?`BksgaMHx%WZisc0SLWTvtfH_`=uY! z!qj?jO?;+#Fg?7f(UpYIYTZ!LRYKUlC#Qoq=|AtP4T);9mXmAuY&dQrAphq35q%`=dr^Q?cZt-itP~2s~5Dl+zM?KT@RPk2u;4&r;w1mME$79 zefJ^_bJC5TD5X$peyKCjMhqzfSGo4FMZEg8;-CH|RRk9c_0^b^f#@F7a1D=j9!WUy zb%#x1Oa?QecsS)EM}*peEy25>t0bzb4P7XW@!iqMWa+{8J=5f`KYejKi zQeGaH4E*GD1bVJW7G(Js>mOVRI?SHO)_u3&Jk=kaXghURv0p$x)C05((Ph6ctqsG~ zer?+@!3heq?MqJxQ6eGu?n%!$Q2oombY0j<^0jLhuF^b#hcAxWN$Dtz8`Ur$khQCR zA%|@)zuWrr(tm&1KhW!rT~4SKfv;6Qv|h^8e=i}4L;MCcsUY-K^5?QUr#}sJdHrvo z*L%0KA332k?gN4kw}vKV62kkRempbQ*s|dEJZ0#^)Ylp8CExG_ zF*xjj`-fthpGV^H(c5h9OEx1Oq;>Iu#0Ng@7jA~+T*u9Qkn*dH2ucgTOsL zBSf$3^~!QD{eJ%7#|=Jj+~Pg%GyDVErL>W?H&Z_Nh#YC(BjskEAx5A^wC$bH{sx|| zyAi+djqsJ$G^0__Vv4%0;5L8r*hK|*6(1Wuwt)|7T{f{t4S(i{ZML56Qi$LF^mAdW zzvYzgGU>Y9B>PqKcGEj2NbmREX1Z;DAfSFfn{NMbe`=kT3uG3gPpjL9d{61}$b7B3 z5lB1um!4bn0J1Rm<8X-dDvGULUtpbbg#WowDG~+NPu5vo=0nW-h0(c4jWW-|G9_AxQ5i zGt8FVj&O$^7QDsG+a%W+0^s3IiNhO*g=(~qEUio4NBF}$1ht9WL@n6G9JnGLrjrLt zc;=r_0rp?v1+z^J4(O;DBNWMSmj6*cX+b`D-OCE_lK>K=NIqnsVKjh43&w61SeMIj zS6*1DCP=+MNOL0yFA%J)9IR^=e9j|SFFDwNo!beJFr~vTY=~WSz}%sVa$rE2IDmx# z)k!yEx}OMN2}#ld(Nr!DI_wkx1D60vJl0;?_XZyG#Ex&-9|jKyX|+MI0fNgIHaP{5 zMFJ!k00slv=3xTOLMN`nF9D&qSQT(+e(ay;!b$`T7bT1|hGx;P$w{CkfN)33<-O4I zIyrRwBLKB6vgC04IOv=o;C`|&iU7E2VRjLM49Q68>v8W=}DdJLs&t1)*yLmqMNO|s6``l;Xx$oxlk;bF`<{||%;b;tJO9klC z77?T(g203$10q=zn31vw91q`1O@1mUvg{A|Z=j8oK{0Y@xt_>LI&g$;ndT|{2g3%# z1MC0--W}pnlX`7DiggNoA2@rn8)f|=t$L3HlWJrqkEtr=`GdoJ6i7#dm)kedUp$F= z0q9@2jLrcZ$EkE%V|06QRNo$nFPj`k~z1?7@=$~)odP%Y(B4S zfz)iFx@?ibY%wn}dwNt;jqe_RSp2L6Hv@DciH-z>wcZW5LvgDq7tCx%BCbLOC5`LYSyk&Qm1=3FMQOEHX-#UWX%A0p(;=rfLz=tXldw z8wJABhvN3WZi5w5bCEWv4k`>q<|t-x7L%S*h zQ<%&x?iw*(|6`{E&<1CP&E*UEU;v5%GbCZGh;S5D*o|_nGCBIR7J^eB0mH=U0tu5a z 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