├── agent-providers └── span │ ├── src │ ├── test │ │ ├── resources │ │ │ ├── logback-test.xml │ │ │ └── dispatcherProvider.txt │ │ └── scala │ │ │ └── com │ │ │ └── expedia │ │ │ └── www │ │ │ └── haystack │ │ │ └── agent │ │ │ ├── pitchfork │ │ │ ├── HttpConfigSpec.scala │ │ │ ├── processors │ │ │ │ └── HaystackDomainConverterSpec.scala │ │ │ └── PitchforkServiceSpec.scala │ │ │ └── span │ │ │ ├── helpers │ │ │ ├── TestDispatcherEmptyConfig.scala │ │ │ ├── TestDispatcher.scala │ │ │ └── TestDispatcher2.scala │ │ │ ├── spi │ │ │ └── SpanAgentSpec.scala │ │ │ └── service │ │ │ └── SpanAgentGrpcServiceSpec.scala │ └── main │ │ └── java │ │ └── com │ │ └── expedia │ │ └── www │ │ └── haystack │ │ └── agent │ │ ├── span │ │ ├── service │ │ │ ├── SpanGrpcHealthService.java │ │ │ └── SpanAgentGrpcService.java │ │ ├── enricher │ │ │ └── Enricher.java │ │ └── spi │ │ │ └── SpanAgent.java │ │ ├── pitchfork │ │ ├── spi │ │ │ └── PitchforkAgent.java │ │ ├── processors │ │ │ ├── ZipkinSpanProcessorFactory.java │ │ │ ├── ZipkinSpanProcessor.java │ │ │ ├── SpanValidator.java │ │ │ └── HaystackDomainConverter.java │ │ └── service │ │ │ ├── PitchforkService.java │ │ │ ├── config │ │ │ └── HttpConfig.java │ │ │ └── PitchforkServlet.java │ │ └── core │ │ └── BaseAgent.java │ └── pom.xml ├── api ├── src │ ├── test │ │ ├── resources │ │ │ ├── singleAgentProvider.txt │ │ │ └── configProvider.txt │ │ └── scala │ │ │ └── com │ │ │ └── expedia │ │ │ └── www │ │ │ └── haystack │ │ │ └── agent │ │ │ └── core │ │ │ ├── helpers │ │ │ ├── ReplacingClassLoader.scala │ │ │ ├── TestFileConfigReader.scala │ │ │ └── TestAgent.scala │ │ │ ├── SharedMetricRegistrySpec.scala │ │ │ ├── ConfigurationHelpersSpec.scala │ │ │ └── AgentLoaderSpec.scala │ └── main │ │ └── java │ │ └── com │ │ └── expedia │ │ └── www │ │ └── haystack │ │ └── agent │ │ └── core │ │ ├── RateLimitException.java │ │ ├── Agent.java │ │ ├── config │ │ ├── ConfigReader.java │ │ └── ConfigurationHelpers.java │ │ ├── Dispatcher.java │ │ ├── metrics │ │ └── SharedMetricRegistry.java │ │ └── AgentLoader.java └── pom.xml ├── .mvn └── wrapper │ ├── maven-wrapper.jar │ └── maven-wrapper.properties ├── deployment └── terraform │ ├── outputs.tf │ ├── variables.tf │ ├── templates │ ├── haystack-agent.conf │ └── deployment.yaml │ └── main.tf ├── .gitmodules ├── bundlers └── haystack-agent │ ├── src │ └── main │ │ ├── resources │ │ ├── META-INF │ │ │ └── services │ │ │ │ ├── com.expedia.www.haystack.agent.core.config.ConfigReader │ │ │ │ ├── com.expedia.www.haystack.agent.blobs.dispatcher.core.BlobDispatcher │ │ │ │ ├── com.expedia.www.haystack.agent.core.Agent │ │ │ │ └── com.expedia.www.haystack.agent.core.Dispatcher │ │ └── logback.xml │ │ └── java │ │ └── com │ │ └── expedia │ │ └── www │ │ └── haystack │ │ └── agent │ │ └── HelloApp.java │ └── pom.xml ├── docker ├── default.conf ├── start-app.sh └── Dockerfile ├── .gitignore ├── config-providers └── file │ ├── src │ ├── test │ │ ├── resources │ │ │ └── test.conf │ │ └── scala │ │ │ └── com │ │ │ └── expedia │ │ │ └── www │ │ │ └── haystack │ │ │ └── agent │ │ │ └── config │ │ │ └── unit │ │ │ └── FileConfigReaderSpec.scala │ └── main │ │ └── java │ │ └── com │ │ └── expedia │ │ └── www │ │ └── haystack │ │ └── agent │ │ └── config │ │ └── spi │ │ └── FileConfigReader.java │ └── pom.xml ├── .travis.yml ├── .travis ├── settings.xml ├── deploy.sh └── publish-to-docker-hub.sh ├── agent-dispatchers ├── logger │ ├── pom.xml │ └── src │ │ ├── main │ │ └── java │ │ │ └── com │ │ │ └── expedia │ │ │ └── www │ │ │ └── haystack │ │ │ └── agent │ │ │ └── dispatcher │ │ │ └── LoggerDispatcher.java │ │ └── test │ │ └── scala │ │ └── com │ │ └── expedia │ │ └── www │ │ └── haystack │ │ └── agent │ │ └── dispatcher │ │ └── LoggerDispatcherSpec.scala ├── kinesis │ ├── pom.xml │ └── src │ │ └── test │ │ └── scala │ │ └── com │ │ └── expedia │ │ └── www │ │ └── haystack │ │ └── agent │ │ └── dispatcher │ │ └── KinesisSpanDispatcherSpec.scala ├── http │ ├── pom.xml │ └── src │ │ ├── main │ │ └── java │ │ │ └── com │ │ │ └── expedia │ │ │ └── www │ │ │ └── haystack │ │ │ └── agent │ │ │ └── dispatcher │ │ │ └── HttpDispatcher.java │ │ └── test │ │ └── scala │ │ └── com │ │ └── expedia │ │ └── www │ │ └── haystack │ │ └── agent │ │ └── dispatcher │ │ └── HttpDispatcherSpec.scala └── kafka │ ├── src │ ├── test │ │ └── scala │ │ │ └── com │ │ │ └── expedia │ │ │ └── www │ │ │ └── haystack │ │ │ └── agent │ │ │ └── dispatcher │ │ │ └── KafkaDispatcherSpec.scala │ └── main │ │ └── java │ │ └── com │ │ └── expedia │ │ └── www │ │ └── haystack │ │ └── agent │ │ └── dispatcher │ │ └── KafkaDispatcher.java │ └── pom.xml ├── mvnw.cmd ├── mvnw └── LICENSE /agent-providers/span/src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /api/src/test/resources/singleAgentProvider.txt: -------------------------------------------------------------------------------- 1 | com.expedia.www.haystack.agent.core.helpers.TestAgent -------------------------------------------------------------------------------- /api/src/test/resources/configProvider.txt: -------------------------------------------------------------------------------- 1 | com.expedia.www.haystack.agent.core.helpers.TestFileConfigReader -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ExpediaDotCom/haystack-agent/HEAD/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /deployment/terraform/outputs.tf: -------------------------------------------------------------------------------- 1 | output "proxy_grpc_server_endpoint" { 2 | value = "haystack-agent:${var.blobs_service_port}" 3 | } -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.5.0/apache-maven-3.5.0-bin.zip -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "haystack-idl"] 2 | path = haystack-idl 3 | url = https://github.com/ExpediaDotCom/haystack-idl.git 4 | branch = master 5 | -------------------------------------------------------------------------------- /bundlers/haystack-agent/src/main/resources/META-INF/services/com.expedia.www.haystack.agent.core.config.ConfigReader: -------------------------------------------------------------------------------- 1 | com.expedia.www.haystack.agent.config.spi.FileConfigReader 2 | -------------------------------------------------------------------------------- /bundlers/haystack-agent/src/main/resources/META-INF/services/com.expedia.www.haystack.agent.blobs.dispatcher.core.BlobDispatcher: -------------------------------------------------------------------------------- 1 | com.expedia.www.haystack.agent.blobs.dispatcher.s3.S3Dispatcher -------------------------------------------------------------------------------- /docker/default.conf: -------------------------------------------------------------------------------- 1 | agents { 2 | spans { 3 | enabled = true 4 | port = 35000 5 | dispatchers { 6 | logger { 7 | 8 | } 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | *.iml 3 | *.ipr 4 | *.iws 5 | *.log 6 | .classpath 7 | .project 8 | logs/ 9 | target/ 10 | .idea/ 11 | package-lock.json 12 | */.DS_Store 13 | .DS_Store 14 | .settings -------------------------------------------------------------------------------- /agent-providers/span/src/test/resources/dispatcherProvider.txt: -------------------------------------------------------------------------------- 1 | com.expedia.www.haystack.agent.span.helpers.TestDispatcher 2 | com.expedia.www.haystack.agent.span.helpers.TestDispatcher2 3 | com.expedia.www.haystack.agent.span.helpers.TestDispatcherEmptyConfig -------------------------------------------------------------------------------- /bundlers/haystack-agent/src/main/resources/META-INF/services/com.expedia.www.haystack.agent.core.Agent: -------------------------------------------------------------------------------- 1 | com.expedia.www.haystack.agent.span.spi.SpanAgent 2 | com.expedia.www.haystack.agent.pitchfork.spi.PitchforkAgent 3 | com.expedia.www.haystack.agent.blobs.server.spi.BlobAgent -------------------------------------------------------------------------------- /bundlers/haystack-agent/src/main/resources/META-INF/services/com.expedia.www.haystack.agent.core.Dispatcher: -------------------------------------------------------------------------------- 1 | com.expedia.www.haystack.agent.dispatcher.KafkaDispatcher 2 | com.expedia.www.haystack.agent.dispatcher.KinesisDispatcher 3 | com.expedia.www.haystack.agent.dispatcher.HttpDispatcher 4 | com.expedia.www.haystack.agent.dispatcher.LoggerDispatcher 5 | -------------------------------------------------------------------------------- /api/src/main/java/com/expedia/www/haystack/agent/core/RateLimitException.java: -------------------------------------------------------------------------------- 1 | package com.expedia.www.haystack.agent.core; 2 | 3 | /** 4 | * exception that captures the rate limit errors 5 | */ 6 | public class RateLimitException extends RuntimeException { 7 | public RateLimitException(String message) { 8 | super(message); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /config-providers/file/src/test/resources/test.conf: -------------------------------------------------------------------------------- 1 | agents { 2 | spans { 3 | enabled = true 4 | key1 = "value1" 5 | port = 8080 6 | 7 | dispatchers { 8 | kinesis { 9 | arn = "arn-1" 10 | queueName = "myqueue" 11 | } 12 | } 13 | } 14 | 15 | blobs { 16 | enabled = true 17 | key2 = "value2" 18 | port = 80 19 | 20 | dispatchers { 21 | s3 { 22 | iam = "iam-role" 23 | } 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /deployment/terraform/variables.tf: -------------------------------------------------------------------------------- 1 | variable "kubectl_context_name" {} 2 | variable "kubectl_executable_name" {} 3 | variable "namespace" {} 4 | variable "node_selector_label"{} 5 | variable "kafka_hostname" {} 6 | variable "kafka_port" {} 7 | variable "graphite_hostname" {} 8 | variable "graphite_port" {} 9 | variable "graphite_enabled" {} 10 | 11 | variable "haystack-agent" { 12 | type = "map" 13 | } 14 | 15 | variable "aws_region" { 16 | default = "us-west-2" 17 | } 18 | 19 | variable "spans_service_port" { 20 | default = 35000 21 | } 22 | 23 | variable "blobs_service_port" { 24 | default = 35001 25 | } -------------------------------------------------------------------------------- /docker/start-app.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | [ -z "$JAVA_XMS" ] && JAVA_XMS=1024m 4 | [ -z "$JAVA_XMX" ] && JAVA_XMX=1024m 5 | 6 | set -e 7 | JAVA_OPTS="${JAVA_OPTS} \ 8 | -XX:+UseConcMarkSweepGC \ 9 | -XX:+UseParNewGC \ 10 | -Xmx${JAVA_XMX} \ 11 | -Xms${JAVA_XMS} \ 12 | -Dapplication.name=${APP_NAME} \ 13 | -Dapplication.home=${APP_HOME}" 14 | 15 | if [ -z "${HAYSTACK_AGENT_CONFIG_FILE_PATH}" ]; then 16 | exec java ${JAVA_OPTS} -jar "${APP_HOME}/${APP_NAME}.jar" --config-provider file --file-path /app/bin/default.conf 17 | else 18 | exec java ${JAVA_OPTS} -jar "${APP_HOME}/${APP_NAME}.jar" --config-provider file --file-path ${HAYSTACK_AGENT_CONFIG_FILE_PATH} 19 | fi -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM openjdk:8-jre 2 | MAINTAINER Haystack 3 | 4 | ENV APP_NAME haystack-agent 5 | ENV APP_HOME /app/bin 6 | 7 | RUN mkdir -p ${APP_HOME} 8 | 9 | RUN GRPC_HEALTH_PROBE_VERSION=v0.2.0 && \ 10 | 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 && \ 11 | chmod +x /bin/grpc_health_probe 12 | 13 | COPY bundlers/haystack-agent/target/${APP_NAME}.jar ${APP_HOME}/ 14 | 15 | COPY docker/default.conf ${APP_HOME}/ 16 | COPY docker/start-app.sh ${APP_HOME}/ 17 | 18 | RUN chmod +x ${APP_HOME}/start-app.sh 19 | 20 | WORKDIR ${APP_HOME} 21 | 22 | ENTRYPOINT ["./start-app.sh"] 23 | -------------------------------------------------------------------------------- /agent-providers/span/src/main/java/com/expedia/www/haystack/agent/span/service/SpanGrpcHealthService.java: -------------------------------------------------------------------------------- 1 | package com.expedia.www.haystack.agent.span.service; 2 | 3 | import io.grpc.health.v1.HealthCheckRequest; 4 | import io.grpc.health.v1.HealthCheckResponse; 5 | import io.grpc.health.v1.HealthGrpc; 6 | import io.grpc.stub.StreamObserver; 7 | 8 | public class SpanGrpcHealthService extends HealthGrpc.HealthImplBase { 9 | 10 | @Override 11 | public void check(HealthCheckRequest request, StreamObserver responseObserver) { 12 | responseObserver.onNext(HealthCheckResponse.newBuilder().setStatus(HealthCheckResponse.ServingStatus.SERVING).build()); 13 | responseObserver.onCompleted(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | 3 | services: 4 | - docker 5 | 6 | jdk: 7 | - openjdk8 8 | 9 | install: 10 | - mvn --settings .travis/settings.xml install -Dgpg.skip -Dmaven.javadoc.skip=true -B -V 11 | 12 | before_install: 13 | - if [ ! -z "$GPG_SECRET_KEYS" ]; then echo $GPG_SECRET_KEYS | base64 --decode | $GPG_EXECUTABLE --import; fi 14 | - if [ ! -z "$GPG_OWNERTRUST" ]; then echo $GPG_OWNERTRUST | base64 --decode | $GPG_EXECUTABLE --import-ownertrust; fi 15 | 16 | 17 | script: 18 | # build, create docker image 19 | # upload to dockerhub only for master(non PR) and tag scenario 20 | - if ([ "$TRAVIS_BRANCH" == "master" ] && [ "$TRAVIS_PULL_REQUEST" == "false" ]) || [ -n "$TRAVIS_TAG" ]; then .travis/deploy.sh; fi 21 | 22 | notifications: 23 | email: 24 | - haystack-notifications@expedia.com 25 | -------------------------------------------------------------------------------- /api/src/test/scala/com/expedia/www/haystack/agent/core/helpers/ReplacingClassLoader.scala: -------------------------------------------------------------------------------- 1 | package com.expedia.www.haystack.agent.core.helpers 2 | 3 | import java.io.IOException 4 | import java.net.URL 5 | import java.util 6 | 7 | 8 | /** 9 | * A ClassLoader to help test service providers. 10 | */ 11 | class ReplacingClassLoader(val parent: ClassLoader, val resource: String, val replacement: String) extends ClassLoader(parent) { 12 | override def getResource(name: String): URL = { 13 | if (resource == name) { 14 | return getParent.getResource(replacement) 15 | } 16 | super.getResource(name) 17 | } 18 | 19 | @throws[IOException] 20 | override def getResources(name: String): util.Enumeration[URL] = { 21 | if (resource == name) { 22 | return getParent.getResources(replacement) 23 | } 24 | super.getResources(name) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /api/src/test/scala/com/expedia/www/haystack/agent/core/helpers/TestFileConfigReader.scala: -------------------------------------------------------------------------------- 1 | package com.expedia.www.haystack.agent.core.helpers 2 | 3 | import java.util 4 | 5 | import com.expedia.www.haystack.agent.core.config.ConfigReader 6 | import com.typesafe.config.{Config, ConfigFactory} 7 | 8 | class TestFileConfigReader extends ConfigReader { 9 | override def getName: String = "file" 10 | 11 | override def read(args: util.Map[String, String]): Config = { 12 | ConfigFactory.parseString( 13 | """ 14 | |agents { 15 | | spans { 16 | | k1 = "v1" 17 | | port = 8080 18 | | 19 | | dispatchers { 20 | | kinesis { 21 | | arn = "arn-1" 22 | | queueName = "myqueue" 23 | | } 24 | | } 25 | | } 26 | |} 27 | """.stripMargin) 28 | } 29 | } -------------------------------------------------------------------------------- /bundlers/haystack-agent/src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | true 10 | 11 | 12 | 13 | 14 | 15 | %d{yyyy-MM-dd HH:mm:ss:SSS} %thread, %level, %logger{70}, "%msg"%n 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /.travis/settings.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | ossrh 9 | ${env.SONATYPE_USERNAME} 10 | ${env.SONATYPE_PASSWORD} 11 | 12 | 13 | 14 | 15 | ossrh 16 | 17 | true 18 | 19 | 20 | ${env.GPG_EXECUTABLE} 21 | ${env.GPG_PASSPHRASE} 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /agent-dispatchers/logger/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | haystack-agent-logger-dispatcher 7 | 8 | 9 | haystack-agent-core 10 | com.expedia.www 11 | 0.1.15-SNAPSHOT 12 | ../../pom.xml 13 | 14 | 15 | 16 | 1.6.1 17 | 18 | 19 | 20 | 21 | com.expedia.www 22 | haystack-agent-api 23 | ${project.version} 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /bundlers/haystack-agent/src/main/java/com/expedia/www/haystack/agent/HelloApp.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Expedia, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package com.expedia.www.haystack.agent; 19 | 20 | 21 | /** 22 | * creating hello app to create some java docs for nexus publish 23 | This jar is mostly a bundle jar that comprises of agent implementations 24 | */ 25 | public class HelloApp { 26 | 27 | } 28 | -------------------------------------------------------------------------------- /api/src/test/scala/com/expedia/www/haystack/agent/core/helpers/TestAgent.scala: -------------------------------------------------------------------------------- 1 | package com.expedia.www.haystack.agent.core.helpers 2 | 3 | import com.expedia.www.haystack.agent.core.Agent 4 | import com.typesafe.config.Config 5 | 6 | class TestAgent extends Agent { 7 | 8 | var isInitialized = false 9 | 10 | /** 11 | * unique name of the agent, this is used to selectively load the agent by the name 12 | * 13 | * @return unique name of the agent 14 | */ 15 | override def getName: String = "spans" 16 | 17 | /** 18 | * initialize the agent 19 | * 20 | * @param cfg config object 21 | * @throws Exception throws an exception if fail to initialize 22 | */ 23 | override def initialize(cfg: Config): Unit = { 24 | isInitialized = true 25 | 26 | assert(cfg.getInt("port") == 8080) 27 | assert(cfg.getString("k1") == "v1") 28 | 29 | assert(cfg.getConfig("dispatchers") != null) 30 | assert(cfg.getConfig("dispatchers").hasPath("kinesis")) 31 | } 32 | 33 | /** 34 | * close the agent 35 | */ 36 | override def close(): Unit = { 37 | assert(isInitialized, "Fail to close the uninitialized agent") 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /.travis/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | cd `dirname $0`/.. 3 | 4 | if [ -z "$SONATYPE_USERNAME" ] 5 | then 6 | echo "ERROR! Please set SONATYPE_USERNAME and SONATYPE_PASSWORD environment variable" 7 | exit 1 8 | fi 9 | 10 | if [ -z "$SONATYPE_PASSWORD" ] 11 | then 12 | echo "ERROR! Please set SONATYPE_PASSWORD environment variable" 13 | exit 1 14 | fi 15 | 16 | 17 | if [ ! -z "$TRAVIS_TAG" ] 18 | then 19 | export AGENT_JAR_VERSION=$TRAVIS_TAG 20 | SKIP_GPG_SIGN=false 21 | echo "travis tag is set -> updating pom.xml attribute to $TRAVIS_TAG" 22 | mvn --settings .travis/settings.xml org.codehaus.mojo:versions-maven-plugin:2.1:set -DnewVersion=$TRAVIS_TAG 1>/dev/null 2>/dev/null 23 | else 24 | SKIP_GPG_SIGN=true 25 | # extract the agent jar version from pom.xml 26 | export AGENT_JAR_VERSION=`cat pom.xml | sed -n -e 's/.*\(.*\)<\/version>.*/\1/p' | head -1` 27 | echo "no travis tag is set, hence keeping the snapshot version in pom.xml" 28 | fi 29 | 30 | mvn clean deploy --settings .travis/settings.xml -Dgpg.skip=$SKIP_GPG_SIGN -DskipTests=true -B -U 31 | 32 | echo "successfully deployed the jars to nexus" 33 | 34 | ./.travis/publish-to-docker-hub.sh 35 | 36 | 37 | -------------------------------------------------------------------------------- /api/src/main/java/com/expedia/www/haystack/agent/core/Agent.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Expedia, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package com.expedia.www.haystack.agent.core; 19 | 20 | 21 | import com.typesafe.config.Config; 22 | 23 | public interface Agent extends AutoCloseable { 24 | 25 | /** 26 | * unique name of the agent, this is used to selectively load the agent by the name 27 | * @return unique name of the agent 28 | */ 29 | String getName(); 30 | 31 | /** 32 | * initialize the agent 33 | * @param config config object 34 | * @throws Exception throws an exception if fail to initialize 35 | */ 36 | void initialize(final Config config) throws Exception; 37 | } 38 | -------------------------------------------------------------------------------- /api/src/test/scala/com/expedia/www/haystack/agent/core/SharedMetricRegistrySpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Expedia, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package com.expedia.www.haystack.agent.core 19 | 20 | import com.expedia.www.haystack.agent.core.metrics.SharedMetricRegistry 21 | import org.scalatest.{FunSpec, Matchers} 22 | 23 | class SharedMetricRegistrySpec extends FunSpec with Matchers { 24 | 25 | describe("SharedMetricRegistry") { 26 | it("should build the right metric name if agentName is not empty") { 27 | SharedMetricRegistry.buildMetricName("spans", "my.timer") shouldEqual "spans.my.timer" 28 | } 29 | 30 | it("should build the right metric name if agentName is empty") { 31 | SharedMetricRegistry.buildMetricName("", "my.timer") shouldEqual "my.timer" 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /deployment/terraform/templates/haystack-agent.conf: -------------------------------------------------------------------------------- 1 | agents { 2 | spans { 3 | enabled = "${enable_spans}" 4 | port = "${spans_service_port}" 5 | dispatchers { 6 | kafka { 7 | bootstrap.servers = "${kafka_endpoint}" 8 | producer.topic = "proto-spans" 9 | buffer.memory = 1048576 10 | retries = 2 11 | } 12 | } 13 | } 14 | ossblobs { 15 | enabled = "${enable_ossblobs}" 16 | port = "${blobs_service_port}" 17 | max.blob.size.in.kb = 512 18 | dispatchers { 19 | s3 { 20 | keep.alive = true 21 | max.outstanding.requests = 150 22 | should.wait.for.upload = false 23 | max.connections = 50 24 | retry.count = 1 25 | bucket.name = "${aws_bucket_name}" 26 | region = "${aws_region}" 27 | use.sts.arn = "${use_sts_arn}" 28 | sts.arn.role = "${sts_arn_role}" 29 | } 30 | } 31 | } 32 | 33 | pitchfork { 34 | enabled = "${enable_pitchfork}" 35 | port = 9411 36 | http.threads { 37 | max = 16 38 | min = 2 39 | } 40 | idle.timeout.ms = 60000 41 | stop.timeout.ms = 30000 42 | accept.null.timestamps = false 43 | max.timestamp.drift.sec = -1 44 | 45 | dispatchers { 46 | kafka { 47 | bootstrap.servers = "kafkasvc:9092" 48 | producer.topic = "proto-spans" 49 | buffer.memory = 1048576 50 | retries = 2 51 | } 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /agent-dispatchers/kinesis/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | haystack-agent-core 7 | com.expedia.www 8 | 0.1.15-SNAPSHOT 9 | ../../pom.xml 10 | 11 | 4.0.0 12 | 13 | haystack-agent-kinesis-dispatcher 14 | 15 | 16 | 0.14.0 17 | 18 | 19 | 20 | 21 | com.amazonaws 22 | amazon-kinesis-producer 23 | ${aws.kinesis.producer.lib.version} 24 | 25 | 26 | com.amazonaws 27 | aws-java-sdk-sts 28 | 29 | 30 | com.expedia.www 31 | haystack-agent-api 32 | ${project.version} 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /agent-providers/span/src/main/java/com/expedia/www/haystack/agent/span/enricher/Enricher.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Expedia, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package com.expedia.www.haystack.agent.span.enricher; 19 | 20 | import com.expedia.open.tracing.Span; 21 | 22 | import java.util.List; 23 | 24 | public interface Enricher { 25 | void apply(final Span.Builder span); 26 | 27 | static Span enrichSpan(final Span span, List enrichers) { 28 | if(enrichers.isEmpty()) { 29 | return span; 30 | } else { 31 | final Span.Builder transformedSpanBuilder = span.toBuilder(); 32 | for (final Enricher enricher : enrichers) { 33 | enricher.apply(transformedSpanBuilder); 34 | } 35 | return transformedSpanBuilder.build(); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /agent-providers/span/src/test/scala/com/expedia/www/haystack/agent/pitchfork/HttpConfigSpec.scala: -------------------------------------------------------------------------------- 1 | package com.expedia.www.haystack.agent.pitchfork 2 | 3 | import java.io.IOException 4 | 5 | import com.expedia.www.haystack.agent.pitchfork.service.config.HttpConfig 6 | import com.typesafe.config.ConfigFactory 7 | import org.scalatest.{FunSpec, Matchers} 8 | import org.scalatest.easymock.EasyMockSugar 9 | 10 | import scala.collection.JavaConverters._ 11 | 12 | class HttpConfigSpec extends FunSpec with Matchers with EasyMockSugar { 13 | describe("Http configuration provider") { 14 | it("should return gzip enabled as false if provided and its value is 'false'") { 15 | val config = ConfigFactory.parseMap(Map("port" -> 9115, "http.threads.min" -> 2, "http.threads.max" -> 4, "gzip.enabled" -> false).asJava) 16 | val httpConfig = HttpConfig.from(config) 17 | httpConfig.isGzipEnabled should equal (false) 18 | } 19 | it("should return gzip enabled as true if not provided") { 20 | val config = ConfigFactory.parseMap(Map("port" -> 9115, "http.threads.min" -> 2, "http.threads.max" -> 4).asJava) 21 | val httpConfig = HttpConfig.from(config) 22 | httpConfig.isGzipEnabled should equal (true) 23 | } 24 | it("should return gzip buffer as 16Kb if not provided") { 25 | val config = ConfigFactory.parseMap(Map("port" -> 9115, "http.threads.min" -> 2, "http.threads.max" -> 4).asJava) 26 | val httpConfig = HttpConfig.from(config) 27 | httpConfig.getGzipBufferSize should equal (16 * 1024) 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /api/src/main/java/com/expedia/www/haystack/agent/core/config/ConfigReader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Expedia, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package com.expedia.www.haystack.agent.core.config; 19 | 20 | import com.typesafe.config.Config; 21 | 22 | import java.util.Map; 23 | 24 | public interface ConfigReader { 25 | /** 26 | * returns the unique name of this config reader, for e.g. 'file' if loading configuration 27 | * from file [ConfigFileReader], 'http' if reading the configuration from http endpoint 28 | * @return unique name for the config reader 29 | */ 30 | String getName(); 31 | 32 | /** 33 | * loads the config and returns the agent config object 34 | * @param args args required by the config reader, for e.g. file based configReader expects filePath 35 | * @return a config object 36 | * @throws Exception 37 | */ 38 | Config read(final Map args) throws Exception; 39 | } 40 | -------------------------------------------------------------------------------- /api/src/main/java/com/expedia/www/haystack/agent/core/Dispatcher.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Expedia, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package com.expedia.www.haystack.agent.core; 19 | 20 | import com.typesafe.config.Config; 21 | 22 | public interface Dispatcher extends AutoCloseable { 23 | /** 24 | * returns the unique name for this dispatcher 25 | */ 26 | String getName(); 27 | 28 | /** 29 | * dispatch the record to the sink 30 | * 31 | * @param partitionKey partitionKey if present, else send null 32 | * @param data in bytes that need to be dispatched to the sink 33 | * @throws Exception throws exception if fails to dispatch 34 | */ 35 | void dispatch(final byte[] partitionKey, final byte[] data) throws Exception; 36 | 37 | /** 38 | * initializes the dispatcher for pushing span records to the sink 39 | * 40 | * @param conf 41 | */ 42 | void initialize(final Config conf); 43 | } -------------------------------------------------------------------------------- /api/src/test/scala/com/expedia/www/haystack/agent/core/ConfigurationHelpersSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Expedia, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package com.expedia.www.haystack.agent.core 18 | 19 | import com.expedia.www.haystack.agent.core.config.ConfigurationHelpers 20 | import org.scalatest.{FunSpec, Matchers} 21 | 22 | 23 | class ConfigurationHelpersSpec extends FunSpec with Matchers { 24 | 25 | describe("Configuration Helpers") { 26 | it("should override from env variables") { 27 | val configStr = 28 | """ 29 | |agents { 30 | | spans { 31 | | enabled = true 32 | | k1 = "v1" 33 | | port = 8080 34 | | 35 | | dispatchers { 36 | | kinesis { 37 | | arn = "arn-1" 38 | | queueName = "myqueue" 39 | | } 40 | | } 41 | | } 42 | |} 43 | """.stripMargin 44 | val cfg = ConfigurationHelpers.load(configStr) 45 | cfg.getString("agents.spans.k1") shouldEqual "v2" 46 | cfg.getInt("agents.spans.other") shouldEqual 100 47 | cfg.getBoolean("agents.spans.enabled") shouldBe true 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /.travis/publish-to-docker-hub.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | cd `dirname $0`/.. 5 | 6 | DOCKER_ORG=expediadotcom 7 | DOCKER_IMAGE_NAME=haystack-agent 8 | DOCKER_IMAGE_TAG=$AGENT_JAR_VERSION 9 | 10 | echo "copying the haystack-agent-$AGENT_JAR_VERSION jar to haystack-agent.jar to simplify the docker build" 11 | cp bundlers/haystack-agent/target/haystack-agent-${AGENT_JAR_VERSION}.jar bundlers/haystack-agent/target/haystack-agent.jar 12 | 13 | docker build -t $DOCKER_IMAGE_NAME -f docker/Dockerfile . 14 | 15 | QUALIFIED_DOCKER_IMAGE_NAME=$DOCKER_ORG/$DOCKER_IMAGE_NAME 16 | echo "DOCKER_ORG=$DOCKER_ORG, DOCKER_IMAGE_NAME=$DOCKER_IMAGE_NAME, QUALIFIED_DOCKER_IMAGE_NAME=$QUALIFIED_DOCKER_IMAGE_NAME" 17 | echo "DOCKER_IMAGE_TAG=$DOCKER_IMAGE_TAG" 18 | 19 | # login 20 | docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD 21 | 22 | # Add tags 23 | if [[ $DOCKER_IMAGE_TAG =~ ([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then 24 | echo "pushing the released haystack agent to docker hub" 25 | 26 | unset MAJOR MINOR PATCH 27 | MAJOR="${BASH_REMATCH[1]}" 28 | MINOR="${BASH_REMATCH[2]}" 29 | PATCH="${BASH_REMATCH[3]}" 30 | 31 | # for tag, add MAJOR, MAJOR.MINOR, MAJOR.MINOR.PATCH and latest as tag 32 | docker tag $DOCKER_IMAGE_NAME $QUALIFIED_DOCKER_IMAGE_NAME:$MAJOR 33 | docker tag $DOCKER_IMAGE_NAME $QUALIFIED_DOCKER_IMAGE_NAME:$MAJOR.$MINOR 34 | docker tag $DOCKER_IMAGE_NAME $QUALIFIED_DOCKER_IMAGE_NAME:$MAJOR.$MINOR.$PATCH 35 | docker tag $DOCKER_IMAGE_NAME $QUALIFIED_DOCKER_IMAGE_NAME:latest 36 | 37 | # publish image with tags 38 | docker push $QUALIFIED_DOCKER_IMAGE_NAME 39 | else 40 | echo "pushing the snapshot version of haystack agent to docker hub" 41 | 42 | docker tag $DOCKER_IMAGE_NAME $QUALIFIED_DOCKER_IMAGE_NAME:$DOCKER_IMAGE_TAG 43 | 44 | # publish image with tags 45 | docker push $QUALIFIED_DOCKER_IMAGE_NAME 46 | fi 47 | -------------------------------------------------------------------------------- /agent-providers/span/src/test/scala/com/expedia/www/haystack/agent/span/helpers/TestDispatcherEmptyConfig.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Expedia, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | 19 | package com.expedia.www.haystack.agent.span.helpers 20 | 21 | import com.expedia.www.haystack.agent.core.Dispatcher 22 | import com.typesafe.config.Config 23 | 24 | class TestDispatcherEmptyConfig extends Dispatcher { 25 | 26 | private var isInitialized = false 27 | 28 | /** 29 | * returns the unique name for this dispatcher 30 | * 31 | * @return 32 | */ 33 | override def getName: String = "test-dispatcher-empty-config" 34 | 35 | /** 36 | * dispatch the record to the sink 37 | * 38 | * @param partitionKey partitionKey if present, else send null 39 | * @param data data bytes that need to be dispatched to the sink 40 | * @throws Exception throws exception if fails to dispatch 41 | */ 42 | override def dispatch(partitionKey: Array[Byte], data: Array[Byte]) = () 43 | 44 | /** 45 | * initializes the dispatcher for pushing span records to the sink 46 | * 47 | * @param conf 48 | */ 49 | override def initialize(conf: Config): Unit = { 50 | isInitialized = true 51 | } 52 | 53 | /** 54 | * close the dispatcher, this is called when the agent is shutting down. 55 | */ 56 | override def close(): Unit = { 57 | assert(isInitialized, "Fail to close as the dispatcher isn't initialized yet") 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /agent-providers/span/src/test/scala/com/expedia/www/haystack/agent/span/helpers/TestDispatcher.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Expedia, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | 19 | package com.expedia.www.haystack.agent.span.helpers 20 | 21 | import com.expedia.www.haystack.agent.core.Dispatcher 22 | import com.typesafe.config.Config 23 | 24 | class TestDispatcher extends Dispatcher { 25 | 26 | private var isInitialized = false 27 | 28 | /** 29 | * returns the unique name for this dispatcher 30 | * 31 | * @return 32 | */ 33 | override def getName: String = "test-dispatcher" 34 | 35 | /** 36 | * dispatch the record to the sink 37 | * 38 | * @param partitionKey partitionKey if present, else send null 39 | * @param data data bytes that need to be dispatched to the sink 40 | * @throws Exception throws exception if fails to dispatch 41 | */ 42 | override def dispatch(partitionKey: Array[Byte], data: Array[Byte]) = () 43 | 44 | /** 45 | * initializes the dispatcher for pushing span records to the sink 46 | * 47 | * @param conf 48 | */ 49 | override def initialize(conf: Config): Unit = { 50 | isInitialized = true 51 | assert(conf != null && conf.getString("queueName") == "myqueue") 52 | } 53 | 54 | /** 55 | * close the dispatcher, this is called when the agent is shutting down. 56 | */ 57 | override def close(): Unit = { 58 | assert(isInitialized, "Fail to close as the dispatcher isn't initialized yet") 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /agent-providers/span/src/test/scala/com/expedia/www/haystack/agent/span/helpers/TestDispatcher2.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Expedia, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | 19 | package com.expedia.www.haystack.agent.span.helpers 20 | 21 | import com.expedia.www.haystack.agent.core.Dispatcher 22 | import com.typesafe.config.Config 23 | 24 | class TestDispatcher2 extends Dispatcher { 25 | 26 | private var isInitialized = false 27 | 28 | /** 29 | * returns the unique name for this dispatcher 30 | * 31 | * @return 32 | */ 33 | override def getName: String = "test-dispatcher-2" 34 | 35 | /** 36 | * dispatch the record to the sink 37 | * 38 | * @param partitionKey partitionKey if present, else send null 39 | * @param data data bytes that need to be dispatched to the sink 40 | * @throws Exception throws exception if fails to dispatch 41 | */ 42 | override def dispatch(partitionKey: Array[Byte], data: Array[Byte]) = () 43 | 44 | /** 45 | * initializes the dispatcher for pushing span records to the sink 46 | * 47 | * @param conf 48 | */ 49 | override def initialize(conf: Config): Unit = { 50 | isInitialized = true 51 | assert(conf != null && conf.getString("queueName") == "myqueue") 52 | } 53 | 54 | /** 55 | * close the dispatcher, this is called when the agent is shutting down. 56 | */ 57 | override def close(): Unit = { 58 | assert(isInitialized, "Fail to close as the dispatcher isn't initialized yet") 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /agent-dispatchers/logger/src/main/java/com/expedia/www/haystack/agent/dispatcher/LoggerDispatcher.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Expedia, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package com.expedia.www.haystack.agent.dispatcher; 19 | 20 | import com.expedia.open.tracing.Span; 21 | import com.expedia.www.haystack.agent.core.Dispatcher; 22 | import com.google.protobuf.util.JsonFormat; 23 | import com.google.protobuf.util.JsonFormat.Printer; 24 | import com.typesafe.config.Config; 25 | import org.slf4j.Logger; 26 | import org.slf4j.LoggerFactory; 27 | 28 | import java.io.IOException; 29 | 30 | /** 31 | * Dispatches the data into stdout. 32 | */ 33 | public class LoggerDispatcher implements Dispatcher { 34 | private final static Logger LOGGER = LoggerFactory.getLogger(LoggerDispatcher.class); 35 | private final static Printer PRINTER = JsonFormat.printer().omittingInsignificantWhitespace(); 36 | 37 | @Override 38 | public String getName() { 39 | return "logger"; 40 | } 41 | 42 | @Override 43 | public void dispatch(final byte[] partitionKey, final byte[] data) { 44 | try { 45 | Span span = Span.parseFrom(data); 46 | StringBuilder builder = new StringBuilder(); 47 | PRINTER.appendTo(span, builder); 48 | LOGGER.debug("{}", builder); 49 | } catch (IOException ex ){ 50 | LOGGER.error("failed to parse span: " + ex); 51 | } 52 | } 53 | 54 | @Override 55 | public void initialize(final Config conf) { 56 | } 57 | 58 | @Override 59 | public void close() { 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /deployment/terraform/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | # ------------------- Deployment ------------------- # 2 | kind: Deployment 3 | apiVersion: apps/v1beta2 4 | metadata: 5 | labels: 6 | k8s-app: ${app_name} 7 | name: ${app_name} 8 | namespace: ${namespace} 9 | spec: 10 | replicas: ${replicas} 11 | revisionHistoryLimit: 10 12 | selector: 13 | matchLabels: 14 | k8s-app: ${app_name} 15 | template: 16 | metadata: 17 | labels: 18 | k8s-app: ${app_name} 19 | spec: 20 | containers: 21 | - name: ${app_name} 22 | image: ${image} 23 | volumeMounts: 24 | # Create on-disk volume to store exec logs 25 | - mountPath: /config 26 | name: config-volume 27 | resources: 28 | limits: 29 | cpu: ${cpu_limit} 30 | memory: ${memory_limit}Mi 31 | requests: 32 | cpu: ${cpu_request} 33 | memory: ${memory_request}Mi 34 | env: 35 | - name: "HAYSTACK_AGENT_CONFIG_FILE_PATH" 36 | value: "/config/haystack-agent.conf" 37 | - name: "HAYSTACK_GRAPHITE_HOST" 38 | value: "${graphite_host}" 39 | - name: "HAYSTACK_GRAPHITE_PORT" 40 | value: "${graphite_port}" 41 | - name: "HAYSTACK_GRAPHITE_ENABLED" 42 | value: "${graphite_enabled}" 43 | - name: "JAVA_XMS" 44 | value: "${jvm_memory_limit}m" 45 | - name: "JAVA_XMX" 46 | value: "${jvm_memory_limit}m" 47 | livenessProbe: 48 | exec: 49 | command: 50 | - /bin/grpc_health_probe 51 | - "-addr=:${blobs_service_port}" 52 | initialDelaySeconds: 30 53 | periodSeconds: 15 54 | failureThreshold: 3 55 | nodeSelector: 56 | ${node_selecter_label} 57 | volumes: 58 | - name: config-volume 59 | configMap: 60 | name: ${configmap_name} 61 | # ------------------- Service ------------------- # 62 | --- 63 | apiVersion: v1 64 | kind: Service 65 | metadata: 66 | labels: 67 | k8s-app: ${app_name} 68 | name: ${app_name} 69 | namespace: ${namespace} 70 | spec: 71 | ports: 72 | - name: "spans" 73 | port: ${spans_service_port} 74 | targetPort: ${spans_service_port} 75 | protocol: "TCP" 76 | - name: "blobs" 77 | port: ${blobs_service_port} 78 | targetPort: ${blobs_service_port} 79 | protocol: "TCP" 80 | selector: 81 | k8s-app: ${app_name} 82 | -------------------------------------------------------------------------------- /agent-providers/span/src/main/java/com/expedia/www/haystack/agent/pitchfork/spi/PitchforkAgent.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Expedia, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package com.expedia.www.haystack.agent.pitchfork.spi; 19 | 20 | import com.expedia.www.haystack.agent.core.BaseAgent; 21 | import com.expedia.www.haystack.agent.span.enricher.Enricher; 22 | import com.expedia.www.haystack.agent.pitchfork.processors.SpanValidator; 23 | import com.expedia.www.haystack.agent.pitchfork.processors.ZipkinSpanProcessorFactory; 24 | import com.expedia.www.haystack.agent.pitchfork.service.PitchforkService; 25 | import com.typesafe.config.Config; 26 | import org.slf4j.LoggerFactory; 27 | 28 | import java.util.List; 29 | 30 | public class PitchforkAgent extends BaseAgent { 31 | 32 | private PitchforkService httpService; 33 | 34 | public PitchforkAgent() { 35 | super(LoggerFactory.getLogger(PitchforkAgent.class)); 36 | } 37 | 38 | @Override 39 | public String getName() { 40 | return "pitchfork"; 41 | } 42 | 43 | @Override 44 | public void initialize(Config config) throws Exception { 45 | dispatchers = loadAndInitializeDispatchers(config, Thread.currentThread().getContextClassLoader(), getName()); 46 | final List enrichers = loadSpanEnrichers(config); 47 | final SpanValidator validator = buildSpanValidator(config); 48 | 49 | final ZipkinSpanProcessorFactory factory = new ZipkinSpanProcessorFactory(validator, dispatchers, enrichers); 50 | httpService = new PitchforkService(config, factory); 51 | httpService.start(); 52 | } 53 | 54 | private SpanValidator buildSpanValidator(final Config config) { 55 | return new SpanValidator(config); 56 | } 57 | 58 | @Override 59 | protected void closeInternal() throws Exception { 60 | httpService.stop(); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /agent-providers/span/src/main/java/com/expedia/www/haystack/agent/pitchfork/processors/ZipkinSpanProcessorFactory.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Expedia, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package com.expedia.www.haystack.agent.pitchfork.processors; 19 | 20 | import com.expedia.www.haystack.agent.core.Dispatcher; 21 | import com.expedia.www.haystack.agent.span.enricher.Enricher; 22 | import com.google.common.collect.ImmutableMap; 23 | import org.apache.commons.lang3.Validate; 24 | import zipkin2.codec.SpanBytesDecoder; 25 | 26 | import java.util.List; 27 | import java.util.Map; 28 | 29 | public class ZipkinSpanProcessorFactory { 30 | public static String JSON_CONTENT_TYPE = "application/json"; 31 | public static String THRIFT_CONTENT_TYPE = "application/x-thrift"; 32 | public static String PROTO_CONTENT_TYPE = "application/x-protobuf"; 33 | 34 | private final List dispatchers; 35 | private final SpanValidator validator; 36 | private final List enrichers; 37 | 38 | public ZipkinSpanProcessorFactory(final SpanValidator validator, 39 | final List dispatchers, 40 | final List enrichers) { 41 | 42 | Validate.notNull(validator, "span validator can't be null"); 43 | Validate.notEmpty(dispatchers, "dispatchers can't be null or empty"); 44 | Validate.notNull(enrichers, "enrichers can't be null or empty"); 45 | 46 | this.validator = validator; 47 | this.dispatchers = dispatchers; 48 | this.enrichers = enrichers; 49 | } 50 | 51 | public Map v1() { 52 | return ImmutableMap.of( 53 | JSON_CONTENT_TYPE, new ZipkinSpanProcessor(SpanBytesDecoder.JSON_V1, validator, dispatchers, enrichers), 54 | THRIFT_CONTENT_TYPE, new ZipkinSpanProcessor(SpanBytesDecoder.THRIFT, validator, dispatchers, enrichers)); 55 | } 56 | 57 | public Map v2() { 58 | return ImmutableMap.of( 59 | JSON_CONTENT_TYPE, new ZipkinSpanProcessor(SpanBytesDecoder.JSON_V2, validator, dispatchers, enrichers), 60 | PROTO_CONTENT_TYPE, new ZipkinSpanProcessor(SpanBytesDecoder.PROTO3, validator, dispatchers, enrichers)); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /agent-providers/span/src/main/java/com/expedia/www/haystack/agent/span/spi/SpanAgent.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Expedia, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package com.expedia.www.haystack.agent.span.spi; 19 | 20 | import com.expedia.www.haystack.agent.core.BaseAgent; 21 | import com.expedia.www.haystack.agent.span.enricher.Enricher; 22 | import com.expedia.www.haystack.agent.span.service.SpanAgentGrpcService; 23 | import com.expedia.www.haystack.agent.span.service.SpanGrpcHealthService; 24 | import com.typesafe.config.Config; 25 | import io.grpc.Server; 26 | import io.grpc.netty.NettyServerBuilder; 27 | import org.slf4j.LoggerFactory; 28 | 29 | import java.io.IOException; 30 | import java.util.List; 31 | import java.util.concurrent.TimeUnit; 32 | 33 | public class SpanAgent extends BaseAgent { 34 | private Server server; 35 | private static final long KEEP_ALIVE_TIME_IN_SECONDS = 30; 36 | 37 | public SpanAgent() { 38 | super(LoggerFactory.getLogger(SpanAgent.class)); 39 | } 40 | 41 | @Override 42 | public String getName() { 43 | return "spans"; 44 | } 45 | 46 | @Override 47 | public void initialize(final Config config) throws IOException { 48 | this.dispatchers = loadAndInitializeDispatchers(config, Thread.currentThread().getContextClassLoader(), getName()); 49 | 50 | final int port = config.getInt("port"); 51 | final List enrichers = loadSpanEnrichers(config); 52 | 53 | this.server = NettyServerBuilder 54 | .forPort(port) 55 | .directExecutor() 56 | .permitKeepAliveWithoutCalls(true) 57 | .permitKeepAliveTime(KEEP_ALIVE_TIME_IN_SECONDS, TimeUnit.SECONDS) 58 | .addService(new SpanAgentGrpcService(dispatchers, enrichers)) 59 | .addService(new SpanGrpcHealthService()) 60 | .build() 61 | .start(); 62 | 63 | logger.info("span agent grpc server started on port {}....", port); 64 | 65 | try { 66 | server.awaitTermination(); 67 | } catch (InterruptedException ex) { 68 | logger.error("span agent server has been interrupted with exception", ex); 69 | } 70 | } 71 | 72 | @Override 73 | protected void closeInternal() throws Exception { 74 | this.server.awaitTermination(); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /config-providers/file/src/main/java/com/expedia/www/haystack/agent/config/spi/FileConfigReader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Expedia, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package com.expedia.www.haystack.agent.config.spi; 19 | 20 | import com.expedia.www.haystack.agent.core.config.ConfigReader; 21 | import com.expedia.www.haystack.agent.core.config.ConfigurationHelpers; 22 | import com.typesafe.config.Config; 23 | import org.apache.commons.io.IOUtils; 24 | import org.apache.commons.lang3.StringUtils; 25 | import org.slf4j.Logger; 26 | import org.slf4j.LoggerFactory; 27 | 28 | import java.io.FileReader; 29 | import java.util.Map; 30 | 31 | /** 32 | * loads the config for all agents from file. It uses system property, environment variable and default config file 33 | * in the given order to find out config file path. 34 | */ 35 | public class FileConfigReader implements ConfigReader { 36 | private final static Logger LOGGER = LoggerFactory.getLogger(FileConfigReader.class); 37 | 38 | private final static String HAYSTACK_AGENT_CONFIG_FILE_PATH = "HAYSTACK_AGENT_CONFIG_FILE_PATH"; 39 | 40 | @Override 41 | public String getName() { 42 | return "file"; 43 | } 44 | 45 | @Override 46 | public Config read(final Map args) throws Exception { 47 | String configFilePath = args.get("--file-path"); 48 | if(StringUtils.isEmpty(configFilePath)) { 49 | configFilePath = System.getProperty(HAYSTACK_AGENT_CONFIG_FILE_PATH); 50 | if (StringUtils.isEmpty(configFilePath)) { 51 | configFilePath = System.getenv(HAYSTACK_AGENT_CONFIG_FILE_PATH); 52 | if (StringUtils.isEmpty(configFilePath)) { 53 | LOGGER.info("Neither system property nor environment variable is found for haystack agent config file, falling to default file path={}", configFilePath); 54 | throw new RuntimeException("Fail to find a valid config file path"); 55 | } else { 56 | LOGGER.info("Environment variable for haystack agent config file path found with value={}", configFilePath); 57 | } 58 | } else { 59 | LOGGER.info("System property for haystack agent config file path found with value={}", configFilePath); 60 | } 61 | } 62 | 63 | return ConfigurationHelpers.load(IOUtils.toString(new FileReader(configFilePath))); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /agent-dispatchers/logger/src/test/scala/com/expedia/www/haystack/agent/dispatcher/LoggerDispatcherSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Expedia, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package com.expedia.www.haystack.agent.dispatcher 19 | 20 | import java.io.{ByteArrayOutputStream, PrintStream} 21 | 22 | import com.expedia.open.tracing.{Span, Tag} 23 | import org.scalatest.{FunSpec, Matchers} 24 | 25 | class LoggerDispatcherSpec extends FunSpec with Matchers { 26 | describe("Logger Dispatcher") { 27 | it("should dispatch span to stdOut") { 28 | val sOut = System.out 29 | try { 30 | val outContent: ByteArrayOutputStream = new ByteArrayOutputStream 31 | System.setOut(new PrintStream(outContent)) 32 | 33 | val dispatcher = new LoggerDispatcher() 34 | 35 | val span = Span.newBuilder() 36 | .setTraceId("7f46165474d11ee5836777d85df2cdab") 37 | .setSpanId("30d1aee5836717f0") 38 | .setStartTime(1580123427000000L) 39 | .addTags(Tag.newBuilder().setKey("http.path").setType(Tag.TagType.STRING).setVStr("/my-path").build()) 40 | .addTags(Tag.newBuilder().setKey("sql.affected_row").setType(Tag.TagType.LONG).setVLong(1).build()) 41 | .build() 42 | 43 | dispatcher.dispatch(span.getTraceId.getBytes("utf-8"), span.toByteArray) 44 | dispatcher.close() 45 | outContent.toString().contains("{\"traceId\":\"7f46165474d11ee5836777d85df2cdab\",\"spanId\":\"30d1aee5836717f0\",\"startTime\":\"1580123427000000\",\"tags\":[{\"key\":\"http.path\",\"vStr\":\"/my-path\"},{\"key\":\"sql.affected_row\",\"type\":\"LONG\",\"vLong\":\"1\"}]}") shouldBe true 46 | } finally { 47 | /* Makes sure the result of the test will be reported to stdOut */ 48 | System.setOut(sOut) 49 | } 50 | } 51 | 52 | it("should fail to dispatch invalid data into stdOut") { 53 | val sOut = System.out 54 | try { 55 | val outContent: ByteArrayOutputStream = new ByteArrayOutputStream 56 | System.setOut(new PrintStream(outContent)) 57 | 58 | val dispatcher = new LoggerDispatcher() 59 | 60 | dispatcher.dispatch("none".getBytes(), "abc".getBytes()) 61 | dispatcher.close() 62 | 63 | outContent.toString().contains("failed to parse span:") shouldBe true 64 | } finally { 65 | /* Makes sure the result of the test will be reported to stdOut */ 66 | System.setOut(sOut) 67 | } 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /api/src/main/java/com/expedia/www/haystack/agent/core/metrics/SharedMetricRegistry.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Expedia, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package com.expedia.www.haystack.agent.core.metrics; 19 | 20 | import com.codahale.metrics.*; 21 | import org.apache.commons.lang3.StringUtils; 22 | 23 | public class SharedMetricRegistry { 24 | private final static String MetricRegistryName = "HAYSTACK_AGENT_METRIC_REGISTRY"; 25 | private final static Object lock = new Object(); 26 | 27 | private static JmxReporter reporter; 28 | 29 | private SharedMetricRegistry() { /* suppress pmd violation */ } 30 | 31 | /** 32 | * start the jmx reporter for shared metric registry 33 | */ 34 | public static void startJmxMetricReporter() { 35 | synchronized (lock) { 36 | if (reporter == null) { 37 | reporter = JmxReporter.forRegistry(get()).build(); 38 | reporter.start(); 39 | } 40 | } 41 | } 42 | 43 | /** 44 | * close the jmx reporter for shared metric registry. 45 | * The agent should close the jmx reporter 46 | */ 47 | public static void closeJmxMetricReporter() { 48 | synchronized (lock) { 49 | if (reporter != null) { 50 | reporter.close(); 51 | reporter = null; 52 | } 53 | } 54 | } 55 | 56 | /** 57 | * @param name timer name 58 | * @return a new timer object 59 | */ 60 | public static Timer newTimer(final String name) { 61 | return get().timer(name); 62 | } 63 | 64 | /** 65 | * @param name meter name 66 | * @return a new meter object 67 | */ 68 | public static Meter newMeter(final String name) { 69 | return get().meter(name); 70 | } 71 | 72 | public static Gauge newGauge(final String name, final Gauge gauge) { 73 | return get().gauge(name, () -> gauge); 74 | } 75 | 76 | /** 77 | * adds agentName(if non-empty) as prefix to metricName 78 | * @param agentName name of agent 79 | * @param metricName name of metric 80 | * @return complete metric name 81 | */ 82 | public static String buildMetricName(final String agentName, final String metricName) { 83 | return StringUtils.isEmpty(agentName) ? metricName : agentName + "." + metricName; 84 | } 85 | 86 | private static MetricRegistry get() { 87 | return SharedMetricRegistries.getOrCreate(MetricRegistryName); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /agent-dispatchers/http/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 4.0.0 5 | haystack-agent-http-dispatcher 6 | 7 | 8 | haystack-agent-core 9 | com.expedia.www 10 | 0.1.15-SNAPSHOT 11 | ../../pom.xml 12 | 13 | 14 | 15 | 3.14.0 16 | 17 | 18 | 19 | 20 | com.squareup.okhttp3 21 | okhttp 22 | ${okhttp.version} 23 | 24 | 25 | com.expedia.www 26 | haystack-agent-api 27 | ${project.version} 28 | 29 | 30 | 31 | 32 | 33 | 34 | org.scalatest 35 | scalatest-maven-plugin 36 | 1.0 37 | 38 | 39 | test 40 | 41 | test 42 | 43 | 44 | 45 | 46 | 47 | 48 | org.apache.maven.plugins 49 | maven-jar-plugin 50 | 3.0.2 51 | 52 | 53 | true 54 | 55 | true 56 | 57 | 58 | ${project.name} 59 | ${project.version} 60 | ${project.name} 61 | Expedia 62 | ${java.version} 63 | ${maven.build.timestamp} 64 | 65 | 66 | 67 | 68 | 69 | 70 | org.apache.maven.plugins 71 | maven-compiler-plugin 72 | 3.8.0 73 | 74 | ${project.jdk.version} 75 | ${project.jdk.version} 76 | 77 | 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /deployment/terraform/main.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | app_name = "haystack-agent" 3 | deployment_yaml_file_path = "${path.module}/templates/deployment.yaml" 4 | count = "${var.haystack-agent["enabled"]?1:0}" 5 | config_file_path = "${path.module}/templates/haystack-agent.conf" 6 | checksum = "${sha1("${data.template_file.config_data.rendered}")}" 7 | configmap_name = "haystack-agent-${local.checksum}" 8 | } 9 | 10 | resource "kubernetes_config_map" "haystack-config" { 11 | metadata { 12 | name = "${local.configmap_name}" 13 | namespace = "${var.namespace}" 14 | } 15 | data { 16 | "haystack-agent.conf" = "${data.template_file.config_data.rendered}" 17 | } 18 | count = "${local.count}" 19 | } 20 | 21 | data "template_file" "config_data" { 22 | template = "${file("${local.config_file_path}")}" 23 | 24 | vars { 25 | kafka_endpoint = "${var.kafka_hostname}:${var.kafka_port}" 26 | aws_bucket_name = "${var.haystack-agent["blobs_aws_bucket_name"]}" 27 | aws_region = "${var.haystack-agent["blobs_aws_region"]}" 28 | blobs_service_port = "${var.blobs_service_port}" 29 | spans_service_port = "${var.spans_service_port}" 30 | enable_spans = "${var.haystack-agent["enable_spans"]}" 31 | enable_ossblobs = "${var.haystack-agent["enable_ossblobs"]}" 32 | enable_pitchfork = "${var.haystack-agent["enable_pitchfork"]}" 33 | use_sts_arn = "${var.haystack-agent["use_sts_arn"]}" 34 | sts_arn_role = "${var.haystack-agent["sts_arn_role"]}" 35 | } 36 | } 37 | 38 | data "template_file" "deployment_yaml" { 39 | template = "${file("${local.deployment_yaml_file_path}")}" 40 | 41 | vars { 42 | app_name = "${local.app_name}" 43 | node_selecter_label = "${var.node_selector_label}" 44 | image = "expediadotcom/haystack-agent:${var.haystack-agent["version"]}" 45 | replicas = "${var.haystack-agent["instances"]}" 46 | enabled = "${var.haystack-agent["enabled"]}" 47 | cpu_limit = "${var.haystack-agent["cpu_limit"]}" 48 | cpu_request = "${var.haystack-agent["cpu_request"]}" 49 | memory_limit = "${var.haystack-agent["memory_limit"]}" 50 | memory_request = "${var.haystack-agent["memory_request"]}" 51 | jvm_memory_limit = "${var.haystack-agent["jvm_memory_limit"]}" 52 | kubectl_context_name = "${var.kubectl_context_name}" 53 | kubectl_executable_name = "${var.kubectl_executable_name}" 54 | namespace = "${var.namespace}" 55 | spans_service_port = "${var.spans_service_port}" 56 | blobs_service_port = "${var.blobs_service_port}" 57 | configmap_name = "${local.configmap_name}" 58 | graphite_port = "${var.graphite_port}" 59 | graphite_host = "${var.graphite_hostname}" 60 | graphite_enabled = "${var.graphite_enabled}" 61 | } 62 | } 63 | 64 | resource "null_resource" "kubectl_apply" { 65 | triggers { 66 | template = "${data.template_file.deployment_yaml.rendered}" 67 | } 68 | provisioner "local-exec" { 69 | command = "echo '${data.template_file.deployment_yaml.rendered}' | ${var.kubectl_executable_name} apply -f - --context ${var.kubectl_context_name}" 70 | } 71 | count = "${local.count}" 72 | } 73 | 74 | 75 | resource "null_resource" "kubectl_destroy" { 76 | 77 | provisioner "local-exec" { 78 | command = "echo '${data.template_file.deployment_yaml.rendered}' | ${var.kubectl_executable_name} delete -f - --context ${var.kubectl_context_name}" 79 | when = "destroy" 80 | } 81 | count = "${local.count}" 82 | } -------------------------------------------------------------------------------- /agent-providers/span/src/test/scala/com/expedia/www/haystack/agent/pitchfork/processors/HaystackDomainConverterSpec.scala: -------------------------------------------------------------------------------- 1 | package com.expedia.www.haystack.agent.pitchfork.processors 2 | 3 | import com.expedia.open.tracing.Tag 4 | import org.scalatest.{FunSpec, Matchers} 5 | import org.scalatest.easymock.EasyMockSugar 6 | import zipkin2.{Endpoint, Span} 7 | 8 | class HaystackDomainConverterSpec extends FunSpec with Matchers with EasyMockSugar { 9 | 10 | private def zipkinSpanBuilder(traceId: String): Span.Builder = { 11 | zipkin2.Span.newBuilder() 12 | .traceId(traceId) 13 | .id(1) 14 | .parentId(2) 15 | .name("/foo") 16 | .localEndpoint(Endpoint.newBuilder().serviceName("foo").build()) 17 | .remoteEndpoint(Endpoint.newBuilder().serviceName("bar").port(8080).ip("10.10.10.10").build()) 18 | .timestamp(System.currentTimeMillis() * 1000) 19 | .duration(100000l) 20 | .putTag("pos", "1") 21 | } 22 | 23 | describe("Haystack Domain Converter") { 24 | it("should create span from Zipkin span") { 25 | val traceId = "bd1068b1bc333ec0" 26 | val zipkinSpan = zipkinSpanBuilder(traceId).clearTags().build() 27 | val span = HaystackDomainConverter.fromZipkinV2(zipkinSpan) 28 | 29 | span.getTraceId shouldBe traceId 30 | span.getTagsList.stream().filter(_.getKey == "error").count() shouldBe 0 31 | } 32 | 33 | it("should create span from Zipkin span with error false") { 34 | val traceId = "bd1068b1bc333ec0" 35 | val zipkinSpan = zipkinSpanBuilder(traceId).putTag("error", "false").build() 36 | val span = HaystackDomainConverter.fromZipkinV2(zipkinSpan) 37 | 38 | span.getTraceId shouldBe traceId 39 | span.getTagsList.stream().filter(_.getKey == "error").count() shouldBe 1 40 | span.getTagsList.stream().filter(tag => tag.getKey == "error" && tag.getType == Tag.TagType.BOOL && !tag.getVBool).count() shouldBe 1 41 | } 42 | 43 | it("should create span from Zipkin span with error true") { 44 | val traceId = "bd1068b1bc333ec0" 45 | val zipkinSpan = zipkinSpanBuilder(traceId).putTag("error", "bad things").build() 46 | val span = HaystackDomainConverter.fromZipkinV2(zipkinSpan) 47 | 48 | span.getTraceId shouldBe traceId 49 | span.getTagsList.stream().filter(_.getKey == "error").count() shouldBe 1 50 | span.getTagsList.stream().filter(tag => tag.getKey == "error" && tag.getType == Tag.TagType.BOOL && tag.getVBool).count() shouldBe 1 51 | span.getTagsList.stream().filter(_.getKey == "error_msg").count() shouldBe 1 52 | } 53 | 54 | it("should create span with kind tag") { 55 | val traceId = "edcb04102634b702" 56 | val zipkinSpan = zipkinSpanBuilder(traceId) 57 | .kind(Span.Kind.SERVER) 58 | .clearTags() 59 | .build() 60 | val span = HaystackDomainConverter.fromZipkinV2(zipkinSpan) 61 | 62 | span.getTraceId shouldBe traceId 63 | span.getTagsList.stream().filter(_.getKey == "span.kind").count() shouldBe 1 64 | } 65 | 66 | it("should create span without duplicate kind tag") { 67 | val traceId = "661e251d4406e110" 68 | val zipkinSpan = zipkinSpanBuilder(traceId).kind(Span.Kind.SERVER).putTag("span.kind", "server").build() 69 | val span = HaystackDomainConverter.fromZipkinV2(zipkinSpan) 70 | 71 | span.getTraceId shouldBe traceId 72 | span.getTagsList.stream().filter(_.getKey == "span.kind").count() shouldBe 1 73 | } 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /agent-providers/span/src/main/java/com/expedia/www/haystack/agent/pitchfork/processors/ZipkinSpanProcessor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Expedia, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package com.expedia.www.haystack.agent.pitchfork.processors; 19 | 20 | import com.codahale.metrics.Meter; 21 | import com.expedia.open.tracing.Span; 22 | import com.expedia.www.haystack.agent.core.Dispatcher; 23 | import com.expedia.www.haystack.agent.core.metrics.SharedMetricRegistry; 24 | import com.expedia.www.haystack.agent.span.enricher.Enricher; 25 | import org.slf4j.Logger; 26 | import org.slf4j.LoggerFactory; 27 | import zipkin2.codec.SpanBytesDecoder; 28 | 29 | import java.util.ArrayList; 30 | import java.util.List; 31 | 32 | public class ZipkinSpanProcessor { 33 | private final static Logger logger = LoggerFactory.getLogger(ZipkinSpanProcessor.class); 34 | private final Meter invalidSpanMeter; 35 | private final SpanBytesDecoder decoder; 36 | private final SpanValidator validator; 37 | private final List dispatchers; 38 | private final List enrichers; 39 | 40 | public ZipkinSpanProcessor(final SpanBytesDecoder decoder, 41 | final SpanValidator validator, 42 | final List dispatchers, 43 | final List enrichers) { 44 | this.decoder = decoder; 45 | this.validator = validator; 46 | this.dispatchers = dispatchers; 47 | this.enrichers = enrichers; 48 | this.invalidSpanMeter = SharedMetricRegistry.newMeter("pitchfork.invalid.spans"); 49 | } 50 | 51 | public void process(byte[] inputBytes) throws Exception { 52 | final List zipkinSpans = decode(inputBytes); 53 | for (final zipkin2.Span span : zipkinSpans) { 54 | if (!validator.isSpanValid(span)) { 55 | logger.warn("invalid zipkin span found !"); 56 | invalidSpanMeter.mark(); 57 | continue; 58 | } 59 | 60 | final Span haystackSpan = enrich(HaystackDomainConverter.fromZipkinV2(span)); 61 | for (final Dispatcher dispatcher : dispatchers) { 62 | logger.debug("dispatching span to dispatcher {}", dispatcher.getName()); 63 | dispatcher.dispatch(haystackSpan.getTraceId().getBytes(), haystackSpan.toByteArray()); 64 | } 65 | } 66 | } 67 | 68 | private List decode(byte[] inputBytes) { 69 | final List decodedSpans = new ArrayList<>(); 70 | try { 71 | decoder.decodeList(inputBytes, decodedSpans); 72 | } catch (Exception ex) { 73 | decoder.decode(inputBytes, decodedSpans); 74 | } 75 | return decodedSpans; 76 | } 77 | 78 | private Span enrich(final Span span) { 79 | return Enricher.enrichSpan(span, enrichers); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /agent-providers/span/src/main/java/com/expedia/www/haystack/agent/pitchfork/processors/SpanValidator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Expedia, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package com.expedia.www.haystack.agent.pitchfork.processors; 19 | 20 | import com.typesafe.config.Config; 21 | import org.slf4j.Logger; 22 | import org.slf4j.LoggerFactory; 23 | 24 | public class SpanValidator { 25 | private static final Logger logger = LoggerFactory.getLogger(SpanValidator.class); 26 | private static final int VALIDATION_DISABLED = -1; 27 | 28 | private static final String ACCEPT_NULL_TIMESTAMPS_CONFIG_KEY = "accept.null.timestamps"; 29 | private static final String MAX_TIMESTAMP_DRIFT_SEC = "max.timestamp.drift.sec"; 30 | 31 | private final boolean acceptNullTimestamps; 32 | private final int maxTimestampDriftSeconds; 33 | 34 | public SpanValidator(final Config config) { 35 | this.acceptNullTimestamps = config.hasPath(ACCEPT_NULL_TIMESTAMPS_CONFIG_KEY) 36 | && config.getBoolean(ACCEPT_NULL_TIMESTAMPS_CONFIG_KEY); 37 | 38 | this.maxTimestampDriftSeconds = config.hasPath(MAX_TIMESTAMP_DRIFT_SEC) ? 39 | config.getInt(MAX_TIMESTAMP_DRIFT_SEC) : VALIDATION_DISABLED; 40 | } 41 | 42 | public boolean isSpanValid(zipkin2.Span span) { 43 | if (span.traceId() == null) { 44 | logger.error("operation=isSpanValid, error='null traceId', service={}, spanId={}", 45 | span.localServiceName(), 46 | span.id()); 47 | 48 | return false; 49 | } 50 | 51 | if (span.timestamp() == null && !acceptNullTimestamps) { 52 | logger.error("operation=isSpanValid, error='null timestamp', service={}, traceId={}, spanId={}", 53 | span.localServiceName(), 54 | span.traceId(), 55 | span.id()); 56 | 57 | return false; 58 | } 59 | 60 | if (span.timestamp() != null && maxTimestampDriftSeconds != VALIDATION_DISABLED) { 61 | long currentTimeInMicros = System.currentTimeMillis() * 1000; 62 | 63 | long driftInMicros = span.timestamp() > currentTimeInMicros 64 | ? span.timestamp() - currentTimeInMicros 65 | : currentTimeInMicros - span.timestamp(); 66 | 67 | long driftInSeconds = driftInMicros / 1000 / 1000; 68 | 69 | if (driftInSeconds > maxTimestampDriftSeconds) { 70 | logger.error("operation=isSpanValid, error='invalid timestamp', driftInSeconds={} timestamp={}, service={}, traceId={}, spanId={}", 71 | driftInSeconds, 72 | span.timestamp(), 73 | span.localServiceName(), 74 | span.traceId(), 75 | span.id()); 76 | 77 | return false; 78 | } 79 | } 80 | 81 | return true; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /agent-dispatchers/kafka/src/test/scala/com/expedia/www/haystack/agent/dispatcher/KafkaDispatcherSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Expedia, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package com.expedia.www.haystack.agent.dispatcher 19 | 20 | import java.util.concurrent.{Future, TimeUnit} 21 | 22 | import com.codahale.metrics.Timer 23 | import com.expedia.open.tracing.Span 24 | import com.typesafe.config.ConfigFactory 25 | import org.apache.kafka.clients.producer._ 26 | import org.easymock.EasyMock 27 | import org.scalatest.mock.EasyMockSugar 28 | import org.scalatest.{FunSpec, Matchers} 29 | 30 | class KafkaDispatcherSpec extends FunSpec with Matchers with EasyMockSugar { 31 | describe("Kafka Span Dispatcher") { 32 | it("should dispatch span to kafka with success") { 33 | val dispatcher = new KafkaDispatcher() 34 | val producer = mock[KafkaProducer[Array[Byte], Array[Byte]]] 35 | val future = mock[Future[RecordMetadata]] 36 | val timer = mock[Timer] 37 | val timerContext = mock[Timer.Context] 38 | 39 | dispatcher.producer = producer 40 | dispatcher.topic = "mytopic" 41 | dispatcher.dispatchTimer = timer 42 | 43 | val capturedProducerRecord = EasyMock.newCapture[ProducerRecord[Array[Byte], Array[Byte]]]() 44 | expecting { 45 | producer.send(EasyMock.capture(capturedProducerRecord), EasyMock.anyObject(classOf[Callback])).andReturn(future).once() 46 | producer.flush().once() 47 | producer.close(10, TimeUnit.SECONDS).once 48 | timer.time().andReturn(timerContext) 49 | } 50 | 51 | whenExecuting(producer, future, timer, timerContext) { 52 | val span = Span.newBuilder().setTraceId("traceid").build() 53 | dispatcher.dispatch(span.getTraceId.getBytes("utf-8"), span.toByteArray) 54 | val producerRecord = capturedProducerRecord.getValue 55 | producerRecord.topic() shouldEqual "mytopic" 56 | new String(producerRecord.key()) shouldEqual "traceid" 57 | producerRecord.value() shouldBe span.toByteArray 58 | 59 | // close the dispatcher and verify if it is flushed and closed 60 | dispatcher.close() 61 | } 62 | } 63 | 64 | it("should fail to initialize kafka if bootstrap.servers property isn't present") { 65 | val kafka = new KafkaDispatcher() 66 | val caught = intercept[Exception] { 67 | kafka.initialize(ConfigFactory.empty()) 68 | } 69 | caught.getMessage shouldEqual "No configuration setting found for key 'bootstrap'" 70 | } 71 | 72 | it("should fail to initialize kafka if producer.topic property isn't present") { 73 | val kafka = new KafkaDispatcher() 74 | 75 | val config = ConfigFactory.parseString( 76 | """ 77 | | bootstrap.servers: "localhost:9092" 78 | """.stripMargin) 79 | val caught = intercept[Exception] { 80 | kafka.initialize(config) 81 | } 82 | caught.getMessage shouldEqual "No configuration setting found for key 'producer'" 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /agent-dispatchers/kafka/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 4.0.0 5 | haystack-agent-kafka-dispatcher 6 | 7 | 8 | haystack-agent-core 9 | com.expedia.www 10 | 0.1.15-SNAPSHOT 11 | ../../pom.xml 12 | 13 | 14 | 15 | 1.1.0 16 | 17 | 18 | 19 | 20 | org.apache.kafka 21 | kafka_${scala.major.minor.version} 22 | ${kafka.version} 23 | 24 | 25 | org.slf4j 26 | slf4j-log4j12 27 | 28 | 29 | 30 | 31 | com.expedia.www 32 | haystack-agent-api 33 | ${project.version} 34 | 35 | 36 | 37 | 38 | 39 | 40 | org.scalatest 41 | scalatest-maven-plugin 42 | 1.0 43 | 44 | 45 | test 46 | 47 | test 48 | 49 | 50 | 51 | 52 | 53 | 54 | org.apache.maven.plugins 55 | maven-jar-plugin 56 | 3.0.2 57 | 58 | 59 | true 60 | 61 | true 62 | 63 | 64 | ${project.name} 65 | ${project.version} 66 | ${project.name} 67 | Expedia 68 | ${java.version} 69 | ${maven.build.timestamp} 70 | 71 | 72 | 73 | 74 | 75 | 76 | org.apache.maven.plugins 77 | maven-compiler-plugin 78 | 3.8.0 79 | 80 | ${project.jdk.version} 81 | ${project.jdk.version} 82 | 83 | 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /config-providers/file/src/test/scala/com/expedia/www/haystack/agent/config/unit/FileConfigReaderSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Expedia, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package com.expedia.www.haystack.agent.config.unit 19 | 20 | import com.expedia.www.haystack.agent.config.spi.FileConfigReader 21 | import com.expedia.www.haystack.agent.core.config.ConfigurationHelpers 22 | import com.typesafe.config.Config 23 | import org.scalatest.{FunSpec, Matchers} 24 | 25 | class FileConfigReaderSpec extends FunSpec with Matchers { 26 | 27 | describe("File ConfigReader spi") { 28 | it("should read the config file from system property and load the agent config") { 29 | val reader = new FileConfigReader() 30 | val confg = reader.read(new java.util.HashMap[String, String]) 31 | reader.getName shouldEqual "file" 32 | validate(confg) 33 | } 34 | 35 | it("should read the config file in args map and load the agent config") { 36 | val reader = new FileConfigReader() 37 | val args = new java.util.HashMap[String, String]() 38 | args.put("--config-path", "src/test/resources/test.conf") 39 | val confg = reader.read(args) 40 | reader.getName shouldEqual "file" 41 | validate(confg) 42 | } 43 | } 44 | 45 | def validate(config: Config): Unit = { 46 | val agentsConfig = ConfigurationHelpers.readAgentConfigs(config) 47 | agentsConfig.size() shouldBe 2 48 | 49 | val spanAgentConfig = agentsConfig.get("spans") 50 | val configStr = spanAgentConfig.toString 51 | println(configStr) 52 | configStr shouldEqual "Config(SimpleConfigObject({\"dispatchers\":{\"kinesis\":{\"arn\":\"arn-1\",\"queueName\":\"myqueue\"}},\"enabled\":true,\"key1\":\"value1\",\"port\":8080}))" 53 | spanAgentConfig.getString("key1") shouldEqual "value1" 54 | spanAgentConfig.getInt("port") shouldBe 8080 55 | 56 | var dispatchersConfig = ConfigurationHelpers.readDispatchersConfig(spanAgentConfig, "spans") 57 | dispatchersConfig.size() shouldBe 1 58 | val kinesisDispatcher = dispatchersConfig.get("kinesis") 59 | kinesisDispatcher.getString(ConfigurationHelpers.AGENT_NAME_KEY) shouldEqual "spans" 60 | kinesisDispatcher.getString("arn") shouldEqual "arn-1" 61 | kinesisDispatcher.getString("queueName") shouldEqual "myqueue" 62 | 63 | val blobsAgentConfig = agentsConfig.get("blobs") 64 | val blobConfStr = blobsAgentConfig.toString 65 | blobConfStr shouldEqual "Config(SimpleConfigObject({\"dispatchers\":{\"s3\":{\"iam\":\"iam-role\"}},\"enabled\":true,\"key2\":\"value2\",\"port\":80}))" 66 | blobsAgentConfig.getString("key2") shouldEqual "value2" 67 | blobsAgentConfig.getInt("port") shouldBe 80 68 | 69 | dispatchersConfig = ConfigurationHelpers.readDispatchersConfig(blobsAgentConfig, "blobs") 70 | 71 | dispatchersConfig.size() shouldBe 1 72 | val s3Dispatcher = dispatchersConfig.get("s3") 73 | s3Dispatcher.getString(ConfigurationHelpers.AGENT_NAME_KEY) shouldEqual "blobs" 74 | s3Dispatcher.getString("iam") shouldEqual "iam-role" 75 | } 76 | } 77 | 78 | -------------------------------------------------------------------------------- /agent-providers/span/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 4.0.0 5 | haystack-span-agent-provider 6 | 7 | 8 | haystack-agent-core 9 | com.expedia.www 10 | 0.1.15-SNAPSHOT 11 | ../../pom.xml 12 | 13 | 14 | 15 | 16 | com.expedia.www 17 | haystack-agent-api 18 | ${project.version} 19 | 20 | 21 | org.eclipse.jetty 22 | jetty-server 23 | 24 | 25 | org.eclipse.jetty 26 | jetty-servlet 27 | 28 | 29 | io.zipkin.zipkin2 30 | zipkin 31 | 32 | 33 | commons-io 34 | commons-io 35 | 36 | 37 | 38 | 39 | 40 | 41 | org.scalatest 42 | scalatest-maven-plugin 43 | 1.0 44 | 45 | 46 | test 47 | 48 | test 49 | 50 | 51 | 52 | 53 | 54 | 55 | org.apache.maven.plugins 56 | maven-jar-plugin 57 | 3.0.2 58 | 59 | 60 | true 61 | 62 | true 63 | 64 | 65 | ${project.name} 66 | ${project.version} 67 | ${project.name} 68 | Expedia 69 | ${java.version} 70 | ${maven.build.timestamp} 71 | 72 | 73 | 74 | 75 | 76 | 77 | org.apache.maven.plugins 78 | maven-compiler-plugin 79 | 3.8.0 80 | 81 | ${project.jdk.version} 82 | ${project.jdk.version} 83 | 84 | 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /agent-providers/span/src/main/java/com/expedia/www/haystack/agent/core/BaseAgent.java: -------------------------------------------------------------------------------- 1 | package com.expedia.www.haystack.agent.core; 2 | 3 | import com.expedia.www.haystack.agent.core.config.ConfigurationHelpers; 4 | import com.expedia.www.haystack.agent.span.enricher.Enricher; 5 | import com.google.common.annotations.VisibleForTesting; 6 | import com.typesafe.config.Config; 7 | import org.apache.commons.lang3.Validate; 8 | import org.slf4j.Logger; 9 | 10 | import java.util.*; 11 | import java.util.stream.Collectors; 12 | 13 | public abstract class BaseAgent implements Agent { 14 | protected List dispatchers; 15 | protected final Logger logger; 16 | 17 | public BaseAgent(Logger logger) { 18 | this.dispatchers = new ArrayList<>(); 19 | this.logger = logger; 20 | } 21 | 22 | @Override 23 | public void close() { 24 | try { 25 | for (final Dispatcher dispatcher : dispatchers) { 26 | dispatcher.close(); 27 | } 28 | logger.info("shutting down gRPC server and jmx reporter"); 29 | closeInternal(); 30 | } catch (Exception ignored) { 31 | } 32 | } 33 | 34 | @VisibleForTesting 35 | public List loadAndInitializeDispatchers(final Config config, ClassLoader cl, String agentName) { 36 | final List dispatchers = new ArrayList<>(); 37 | final ServiceLoader loadedDispatchers = ServiceLoader.load(Dispatcher.class, cl); 38 | for (final Dispatcher dispatcher : loadedDispatchers) { 39 | final Map dispatches = ConfigurationHelpers.readDispatchersConfig(config, agentName); 40 | dispatches 41 | .entrySet() 42 | .stream() 43 | .filter((e) -> e.getKey().equalsIgnoreCase(dispatcher.getName())) 44 | .forEach((conf) -> { 45 | final Config dispatcherConfig = conf.getValue(); 46 | boolean isEnabled = !dispatcherConfig.hasPath("enabled") || dispatcherConfig.getBoolean("enabled"); 47 | if(isEnabled) { 48 | dispatcher.initialize(dispatcherConfig); 49 | dispatchers.add(dispatcher); 50 | } else { 51 | logger.info("dispatcher with name '{}' is disabled", dispatcher.getName()); 52 | } 53 | }); 54 | } 55 | 56 | Validate.notEmpty(dispatchers, "Span agent dispatchers can't be an empty set"); 57 | 58 | return dispatchers; 59 | } 60 | 61 | @VisibleForTesting 62 | public List loadSpanEnrichers(final Config config) { 63 | if (config.hasPath("enrichers")) { 64 | return config.getStringList("enrichers") 65 | .stream() 66 | .map(clazz -> { 67 | try { 68 | final Class c = Class.forName(clazz); 69 | logger.info("Initializing the span enricher with class name '{}'", clazz); 70 | return (Enricher) c.newInstance(); 71 | } catch (Exception e) { 72 | logger.error("Fail to initialize the enricher with clazz name {}", clazz, e); 73 | return null; 74 | } 75 | }) 76 | .filter(Objects::nonNull) 77 | .collect(Collectors.toList()); 78 | } else { 79 | return Collections.emptyList(); 80 | } 81 | } 82 | 83 | protected void closeInternal() throws Exception {} 84 | } 85 | -------------------------------------------------------------------------------- /config-providers/file/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 4.0.0 5 | haystack-agent-file-config-provider 6 | 7 | 8 | haystack-agent-core 9 | com.expedia.www 10 | 0.1.15-SNAPSHOT 11 | ../../pom.xml 12 | 13 | 14 | 15 | 16 | com.expedia.www 17 | haystack-agent-api 18 | ${project.version} 19 | 20 | 21 | com.fasterxml.jackson.dataformat 22 | jackson-dataformat-yaml 23 | 24 | 25 | com.fasterxml.jackson.core 26 | jackson-databind 27 | 28 | 29 | commons-io 30 | commons-io 31 | 32 | 33 | 34 | 35 | 36 | 37 | org.scalatest 38 | scalatest-maven-plugin 39 | 40 | 41 | test 42 | 43 | test 44 | 45 | 46 | 47 | src/test/resources/test.conf 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | org.apache.maven.plugins 56 | maven-jar-plugin 57 | 3.0.2 58 | 59 | 60 | true 61 | 62 | true 63 | 64 | 65 | ${project.name} 66 | ${project.version} 67 | ${project.name} 68 | Expedia 69 | ${java.version} 70 | ${maven.build.timestamp} 71 | 72 | 73 | 74 | 75 | 76 | 77 | org.apache.maven.plugins 78 | maven-compiler-plugin 79 | 3.8.0 80 | 81 | ${project.jdk.version} 82 | ${project.jdk.version} 83 | 84 | 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /agent-providers/span/src/main/java/com/expedia/www/haystack/agent/pitchfork/service/PitchforkService.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Expedia, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package com.expedia.www.haystack.agent.pitchfork.service; 19 | 20 | import com.expedia.www.haystack.agent.pitchfork.processors.ZipkinSpanProcessorFactory; 21 | import com.expedia.www.haystack.agent.pitchfork.service.config.HttpConfig; 22 | import com.google.common.collect.ImmutableMap; 23 | import com.typesafe.config.Config; 24 | import org.eclipse.jetty.server.HttpConfiguration; 25 | import org.eclipse.jetty.server.HttpConnectionFactory; 26 | import org.eclipse.jetty.server.Server; 27 | import org.eclipse.jetty.server.ServerConnector; 28 | import org.eclipse.jetty.server.handler.gzip.GzipHandler; 29 | import org.eclipse.jetty.servlet.ServletContextHandler; 30 | import org.eclipse.jetty.servlet.ServletHolder; 31 | import org.eclipse.jetty.util.thread.QueuedThreadPool; 32 | import org.slf4j.Logger; 33 | import org.slf4j.LoggerFactory; 34 | 35 | public class PitchforkService { 36 | private final static Logger logger = LoggerFactory.getLogger(PitchforkService.class); 37 | private final Server server; 38 | private final HttpConfig cfg; 39 | 40 | public PitchforkService(final Config config, final ZipkinSpanProcessorFactory processorFactory) { 41 | this.cfg = HttpConfig.from(config); 42 | final QueuedThreadPool threadPool = new QueuedThreadPool(cfg.getMaxThreads(), cfg.getMinThreads(), cfg.getIdleTimeout()); 43 | server = new Server(threadPool); 44 | 45 | final ServerConnector httpConnector = new ServerConnector(server, new HttpConnectionFactory(new HttpConfiguration())); 46 | httpConnector.setPort(cfg.getPort()); 47 | httpConnector.setIdleTimeout(cfg.getIdleTimeout()); 48 | server.addConnector(httpConnector); 49 | 50 | final ServletContextHandler context = new ServletContextHandler(server, "/"); 51 | addResources(context, processorFactory); 52 | 53 | if (cfg.isGzipEnabled()) { 54 | final GzipHandler gzipHandler = new GzipHandler(); 55 | gzipHandler.setInflateBufferSize(cfg.getGzipBufferSize()); 56 | context.setGzipHandler(gzipHandler); 57 | } 58 | 59 | server.setStopTimeout(cfg.getStopTimeout()); 60 | logger.info("pitchfork has been initialized successfully !"); 61 | } 62 | 63 | private void addResources(final ServletContextHandler context, final ZipkinSpanProcessorFactory processorFactory) { 64 | ImmutableMap.of( 65 | "/api/v1/spans", new PitchforkServlet("v1", processorFactory.v1()), 66 | "/api/v2/spans", new PitchforkServlet("v2", processorFactory.v2())) 67 | .forEach((endpoint, servlet) -> { 68 | logger.info("adding servlet for endpoint={}", endpoint); 69 | context.addServlet(new ServletHolder(servlet), endpoint); 70 | }); 71 | } 72 | 73 | public void start() throws Exception { 74 | server.start(); 75 | logger.info("pitchfork has been started on port {} ....", cfg.getPort()); 76 | } 77 | 78 | public void stop() throws Exception { 79 | logger.info("shutting down pitchfork ..."); 80 | server.stop(); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /agent-dispatchers/kafka/src/main/java/com/expedia/www/haystack/agent/dispatcher/KafkaDispatcher.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Expedia, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package com.expedia.www.haystack.agent.dispatcher; 19 | 20 | import com.codahale.metrics.Meter; 21 | import com.codahale.metrics.Timer; 22 | import com.expedia.www.haystack.agent.core.Dispatcher; 23 | import com.expedia.www.haystack.agent.core.config.ConfigurationHelpers; 24 | import com.typesafe.config.Config; 25 | import org.apache.commons.lang3.Validate; 26 | import org.apache.kafka.clients.producer.KafkaProducer; 27 | import org.apache.kafka.clients.producer.ProducerConfig; 28 | import org.apache.kafka.clients.producer.ProducerRecord; 29 | import org.apache.kafka.common.serialization.ByteArraySerializer; 30 | import org.slf4j.Logger; 31 | import org.slf4j.LoggerFactory; 32 | 33 | import java.util.concurrent.TimeUnit; 34 | 35 | import static com.expedia.www.haystack.agent.core.metrics.SharedMetricRegistry.*; 36 | 37 | 38 | public class KafkaDispatcher implements Dispatcher { 39 | private final static Logger LOGGER = LoggerFactory.getLogger(KafkaDispatcher.class); 40 | 41 | private final static String PRODUCER_TOPIC = "producer.topic"; 42 | 43 | Timer dispatchTimer; 44 | Meter dispatchFailure; 45 | 46 | KafkaProducer producer; 47 | String topic; 48 | 49 | @Override 50 | public String getName() { 51 | return "kafka"; 52 | } 53 | 54 | @Override 55 | public void dispatch(final byte[] partitionKey, final byte[] data) throws Exception { 56 | final Timer.Context timer = dispatchTimer.time(); 57 | final ProducerRecord rec = new ProducerRecord<>( 58 | topic, 59 | partitionKey, 60 | data); 61 | producer.send(rec, (metadata, exception) -> { 62 | timer.close(); 63 | if(exception != null) { 64 | dispatchFailure.mark(); 65 | LOGGER.error("Fail to produce the record to kafka with exception", exception); 66 | } 67 | }); 68 | } 69 | 70 | @Override 71 | public void initialize(final Config config) { 72 | final String agentName = config.hasPath("agentName") ? config.getString("agentName") : ""; 73 | 74 | Validate.notNull(config.getString(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG)); 75 | 76 | // remove the producer topic from the configuration and use it during send() call 77 | topic = config.getString(PRODUCER_TOPIC); 78 | producer = new KafkaProducer<>(ConfigurationHelpers.generatePropertiesFromMap(ConfigurationHelpers.convertToPropertyMap(config)), 79 | new ByteArraySerializer(), 80 | new ByteArraySerializer()); 81 | 82 | dispatchTimer = newTimer(buildMetricName(agentName, "kafka.dispatch.timer")); 83 | dispatchFailure = newMeter(buildMetricName(agentName, "kafka.dispatch.failure")); 84 | 85 | LOGGER.info("Successfully initialized the kafka dispatcher with config={}", config); 86 | } 87 | 88 | @Override 89 | public void close() { 90 | LOGGER.info("Closing the kafka dispatcher now..."); 91 | if(producer != null) { 92 | producer.flush(); 93 | producer.close(10, TimeUnit.SECONDS); 94 | producer = null; 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /agent-providers/span/src/main/java/com/expedia/www/haystack/agent/span/service/SpanAgentGrpcService.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Expedia, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package com.expedia.www.haystack.agent.span.service; 19 | 20 | import com.codahale.metrics.Meter; 21 | import com.codahale.metrics.Timer; 22 | import com.expedia.open.tracing.Span; 23 | import com.expedia.open.tracing.agent.api.DispatchResult; 24 | import com.expedia.open.tracing.agent.api.SpanAgentGrpc; 25 | import com.expedia.www.haystack.agent.core.Dispatcher; 26 | import com.expedia.www.haystack.agent.core.RateLimitException; 27 | import com.expedia.www.haystack.agent.core.metrics.SharedMetricRegistry; 28 | import com.expedia.www.haystack.agent.span.enricher.Enricher; 29 | import io.grpc.stub.StreamObserver; 30 | import org.apache.commons.lang3.StringUtils; 31 | import org.apache.commons.lang3.Validate; 32 | import org.slf4j.Logger; 33 | import org.slf4j.LoggerFactory; 34 | 35 | import java.util.List; 36 | 37 | public class SpanAgentGrpcService extends SpanAgentGrpc.SpanAgentImplBase { 38 | 39 | private final Logger LOGGER = LoggerFactory.getLogger(SpanAgentGrpcService.class); 40 | 41 | private final List dispatchers; 42 | private final Timer dispatchTimer; 43 | private final Meter dispatchFailureMeter; 44 | private final List enrichers; 45 | 46 | public SpanAgentGrpcService(final List dispatchers, final List enrichers) { 47 | Validate.notEmpty(dispatchers, "Dispatchers can't be empty"); 48 | this.dispatchers = dispatchers; 49 | this.enrichers = enrichers; 50 | dispatchTimer = SharedMetricRegistry.newTimer("span.agent.dispatch.timer"); 51 | dispatchFailureMeter = SharedMetricRegistry.newMeter("span.agent.dispatch.failures"); 52 | } 53 | 54 | @Override 55 | public void dispatch(final Span span, final StreamObserver responseObserver) { 56 | final DispatchResult.Builder result = DispatchResult.newBuilder().setCode(DispatchResult.ResultCode.SUCCESS); 57 | final StringBuilder failedDispatchers = new StringBuilder(); 58 | 59 | final Timer.Context timer = dispatchTimer.time(); 60 | 61 | final Span enrichedSpan = Enricher.enrichSpan(span, enrichers); 62 | 63 | for(final Dispatcher d : dispatchers) { 64 | try { 65 | d.dispatch(enrichedSpan.getTraceId().getBytes("utf-8"), enrichedSpan.toByteArray()); 66 | } catch (RateLimitException r) { 67 | result.setCode(DispatchResult.ResultCode.RATE_LIMIT_ERROR); 68 | dispatchFailureMeter.mark(); 69 | LOGGER.error("Fail to dispatch the span record due to rate limit errors", r); 70 | failedDispatchers.append(d.getName()).append(','); 71 | } 72 | catch (Exception ex) { 73 | result.setCode(DispatchResult.ResultCode.UNKNOWN_ERROR); 74 | dispatchFailureMeter.mark(); 75 | LOGGER.error("Fail to dispatch the span record to the dispatcher with name={}", d.getName(), ex); 76 | failedDispatchers.append(d.getName()).append(','); 77 | } 78 | } 79 | 80 | if(failedDispatchers.length() > 0) { 81 | result.setErrorMessage("Fail to dispatch the span record to the dispatchers=" + 82 | StringUtils.removeEnd(failedDispatchers.toString(), ",")); 83 | } 84 | 85 | timer.close(); 86 | responseObserver.onNext(result.build()); 87 | responseObserver.onCompleted(); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /agent-providers/span/src/main/java/com/expedia/www/haystack/agent/pitchfork/service/config/HttpConfig.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Expedia, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package com.expedia.www.haystack.agent.pitchfork.service.config; 19 | 20 | import com.typesafe.config.Config; 21 | import org.apache.commons.lang3.Validate; 22 | 23 | public class HttpConfig { 24 | private final static String PORT_CONFIG_KEY = "port"; 25 | private final static String MAX_THREADS_CONFIG_KEY = "http.threads.max"; 26 | private final static String MIN_THREADS_CONFIG_KEY = "http.threads.min"; 27 | private final static String IDLE_TIMEOUT_MILLIS_CONFIG_KEY = "idle.timeout.ms"; 28 | private final static String STOP_TIMEOUT_MILLIS_CONFIG_KEY = "stop.timeout.ms"; 29 | private final static String GZIP_ENABLED_KEY = "gzip.enabled"; 30 | private final static String GZIP_BUFFER_SIZE = "gzip.buffer.size"; 31 | 32 | 33 | private final int port; 34 | private final int maxThreads; 35 | private final int minThreads; 36 | private final int idleTimeout; 37 | private final int stopTimeout; 38 | private final boolean gzipEnabled; 39 | private final int gzipBufferSize; 40 | 41 | HttpConfig(int port, int minThreads, int maxThreads, int idleTimeout, int stopTimeout, boolean gzipEnabled, int gzipBufferSize) { 42 | Validate.isTrue(minThreads <= maxThreads, "min threads has to be less than or equal to max threads count"); 43 | Validate.isTrue(port > 0, "http port should be > 0"); 44 | Validate.isTrue(idleTimeout > 0, "idle timeout should be > 0"); 45 | Validate.isTrue(stopTimeout > 0, "stop timeout should be > 0"); 46 | Validate.isTrue(gzipBufferSize > 0, "gzipbufferSize should be > 0"); 47 | 48 | this.port = port; 49 | this.maxThreads = maxThreads; 50 | this.minThreads = minThreads; 51 | this.idleTimeout = idleTimeout; 52 | this.stopTimeout = stopTimeout; 53 | this.gzipEnabled = gzipEnabled; 54 | this.gzipBufferSize = gzipBufferSize; 55 | } 56 | 57 | public int getPort() { 58 | return port; 59 | } 60 | 61 | public int getMaxThreads() { 62 | return maxThreads; 63 | } 64 | 65 | public int getMinThreads() { 66 | return minThreads; 67 | } 68 | 69 | public int getIdleTimeout() { 70 | return idleTimeout; 71 | } 72 | 73 | public int getStopTimeout() { 74 | return stopTimeout; 75 | } 76 | 77 | public boolean isGzipEnabled() { 78 | return gzipEnabled; 79 | } 80 | 81 | public int getGzipBufferSize() { 82 | return gzipBufferSize; 83 | } 84 | 85 | @SuppressWarnings("PMD.NPathComplexity") 86 | public static HttpConfig from(Config config) { 87 | final int port = config.hasPath(PORT_CONFIG_KEY) ? config.getInt(PORT_CONFIG_KEY) : 9411; 88 | final int maxThreads = config.hasPath(MAX_THREADS_CONFIG_KEY) ? config.getInt(MAX_THREADS_CONFIG_KEY) : 16; 89 | final int minThreads = config.hasPath(MIN_THREADS_CONFIG_KEY) ? config.getInt(MIN_THREADS_CONFIG_KEY) : 2; 90 | final int idleTimeout = config.hasPath(IDLE_TIMEOUT_MILLIS_CONFIG_KEY) ? config.getInt(IDLE_TIMEOUT_MILLIS_CONFIG_KEY) : 60000; 91 | final int stopTimeout = config.hasPath(STOP_TIMEOUT_MILLIS_CONFIG_KEY) ? config.getInt(STOP_TIMEOUT_MILLIS_CONFIG_KEY) : 30000; 92 | final int gzipBufferSize = config.hasPath(GZIP_BUFFER_SIZE) ? config.getInt(GZIP_BUFFER_SIZE) : 16384; 93 | final boolean gzipEnabled = !config.hasPath(GZIP_ENABLED_KEY) || config.getBoolean(GZIP_ENABLED_KEY); // default is true 94 | return new HttpConfig(port, minThreads, maxThreads, idleTimeout, stopTimeout, gzipEnabled, gzipBufferSize); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /agent-providers/span/src/main/java/com/expedia/www/haystack/agent/pitchfork/service/PitchforkServlet.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Expedia, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package com.expedia.www.haystack.agent.pitchfork.service; 19 | 20 | import com.codahale.metrics.Meter; 21 | import com.expedia.www.haystack.agent.core.metrics.SharedMetricRegistry; 22 | import com.expedia.www.haystack.agent.pitchfork.processors.ZipkinSpanProcessor; 23 | import org.apache.commons.io.IOUtils; 24 | import org.apache.commons.lang3.Validate; 25 | import org.slf4j.Logger; 26 | import org.slf4j.LoggerFactory; 27 | 28 | import javax.servlet.http.HttpServlet; 29 | import javax.servlet.http.HttpServletRequest; 30 | import javax.servlet.http.HttpServletResponse; 31 | import java.io.ByteArrayOutputStream; 32 | import java.io.IOException; 33 | import java.io.InputStream; 34 | import java.util.Map; 35 | 36 | import static com.expedia.www.haystack.agent.pitchfork.processors.ZipkinSpanProcessorFactory.*; 37 | import static org.apache.commons.lang3.StringUtils.isEmpty; 38 | 39 | public class PitchforkServlet extends HttpServlet { 40 | private final static Logger logger = LoggerFactory.getLogger(PitchforkServlet.class); 41 | private final Map processors; 42 | private final Meter requestRateMeter; 43 | private final Meter errorMeter; 44 | 45 | public PitchforkServlet(final String name, 46 | final Map processors) { 47 | Validate.notEmpty(name, "pitchfork servlet name can't be empty or null"); 48 | Validate.isTrue(processors != null && !processors.isEmpty(), "span processors can't be null"); 49 | 50 | this.processors = processors; 51 | requestRateMeter = SharedMetricRegistry.newMeter("pitchfork." + name + ".request.rate"); 52 | errorMeter = SharedMetricRegistry.newMeter("pitchfork." + name + ".error.rate"); 53 | 54 | logger.info("Initializing http servlet with name = {}", name); 55 | } 56 | 57 | protected void doPost(final HttpServletRequest request, final HttpServletResponse response) throws IOException { 58 | requestRateMeter.mark(); 59 | logger.info("zipkin span dispatch request at {}", request.getRequestURI()); 60 | 61 | final ZipkinSpanProcessor processor = getProcessor(request.getContentType()); 62 | if (processor != null) { 63 | try { 64 | final byte[] inputBytes = readFromStream(request.getInputStream()).toByteArray(); 65 | processor.process(inputBytes); 66 | response.setStatus(200); 67 | } catch (Exception ex) { 68 | errorMeter.mark(); 69 | logger.error("Fail to process/forward the zipkin span, request made at {}", request.getRequestURI(), ex); 70 | response.sendError(503, "Fail to process/forward the zipkin span!"); 71 | } 72 | } else { 73 | response.sendError(400, String.format("invalid content-type, supported values are %s, %s, %s, got '%s'", 74 | JSON_CONTENT_TYPE, THRIFT_CONTENT_TYPE, PROTO_CONTENT_TYPE, request.getContentType())); 75 | } 76 | } 77 | 78 | private ZipkinSpanProcessor getProcessor(String contentType) { 79 | if (isEmpty(contentType)) { 80 | return null; 81 | } 82 | final String[] contentTypes = contentType.split(";"); 83 | for (final String ctype : contentTypes) { 84 | final ZipkinSpanProcessor processor = processors.get(ctype.toLowerCase()); 85 | if (processor != null) { 86 | return processor; 87 | } 88 | } 89 | return null; 90 | } 91 | 92 | private ByteArrayOutputStream readFromStream(final InputStream input) throws IOException { 93 | final ByteArrayOutputStream output = new ByteArrayOutputStream(); 94 | IOUtils.copy(input, output); 95 | return output; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /agent-dispatchers/http/src/main/java/com/expedia/www/haystack/agent/dispatcher/HttpDispatcher.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Expedia, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package com.expedia.www.haystack.agent.dispatcher; 19 | 20 | import static com.expedia.www.haystack.agent.core.metrics.SharedMetricRegistry.buildMetricName; 21 | import static com.expedia.www.haystack.agent.core.metrics.SharedMetricRegistry.newMeter; 22 | import static com.expedia.www.haystack.agent.core.metrics.SharedMetricRegistry.newTimer; 23 | 24 | import java.util.concurrent.TimeUnit; 25 | 26 | import org.apache.commons.lang3.Validate; 27 | import org.slf4j.Logger; 28 | import org.slf4j.LoggerFactory; 29 | 30 | import com.codahale.metrics.Meter; 31 | import com.codahale.metrics.Timer; 32 | import com.expedia.www.haystack.agent.core.Dispatcher; 33 | import com.typesafe.config.Config; 34 | import okhttp3.ConnectionPool; 35 | import okhttp3.MediaType; 36 | import okhttp3.OkHttpClient; 37 | import okhttp3.Request; 38 | import okhttp3.RequestBody; 39 | import okhttp3.Response; 40 | 41 | public class HttpDispatcher implements Dispatcher { 42 | private static final Logger LOGGER = LoggerFactory.getLogger(HttpDispatcher.class); 43 | 44 | private static final MediaType PROTOBUF = MediaType.get("application/octet-stream"); 45 | private static final String URL = "url"; 46 | private static final String CALL_TIMEOUT_IN_MILLIS = "client.timeout.millis"; 47 | private static final String MAX_IDLE_CONNECTIONS = "client.connectionpool.idle.max"; 48 | private static final String KEEP_ALIVE_DURATION_IN_MINUTES = "client.connectionpool.keepalive.minutes"; 49 | 50 | Timer dispatchTimer; 51 | Meter dispatchFailure; 52 | 53 | OkHttpClient client; 54 | String url; 55 | 56 | @Override 57 | public String getName() { 58 | return "http"; 59 | } 60 | 61 | @Override 62 | public void dispatch(final byte[] ignored, final byte[] data) throws Exception { 63 | RequestBody body = RequestBody.create(PROTOBUF, data); 64 | Request request = new Request.Builder() 65 | .url(url) 66 | .post(body) 67 | .build(); 68 | 69 | try (Timer.Context timer = dispatchTimer.time(); Response response = client.newCall(request).execute()) { 70 | if (!response.isSuccessful()) { 71 | dispatchFailure.mark(); 72 | LOGGER.error("Fail to post the record to the http collector with status code {}", response.code()); 73 | } 74 | } catch (Exception e) { 75 | dispatchFailure.mark(); 76 | LOGGER.error("Fail to post the record to the http collector", e); 77 | } 78 | } 79 | 80 | @Override 81 | public void initialize(final Config config) { 82 | final String agentName = config.hasPath("agentName") ? config.getString("agentName") : ""; 83 | 84 | Validate.notNull(config.getString(URL)); 85 | url = config.getString(URL); 86 | 87 | client = buildClient(config); 88 | 89 | dispatchTimer = newTimer(buildMetricName(agentName, "http.dispatch.timer")); 90 | dispatchFailure = newMeter(buildMetricName(agentName, "http.dispatch.failure")); 91 | 92 | LOGGER.info("Successfully initialized the http dispatcher with config={}", config); 93 | } 94 | 95 | private OkHttpClient buildClient(Config config) { 96 | final int callTimeoutInMilliseconds = config.hasPath(CALL_TIMEOUT_IN_MILLIS) ? config.getInt(CALL_TIMEOUT_IN_MILLIS) : 1000; 97 | final int maxIdleConnections = config.hasPath(MAX_IDLE_CONNECTIONS) ? config.getInt(MAX_IDLE_CONNECTIONS) : 5; 98 | final int keepAliveDuration = config.hasPath(KEEP_ALIVE_DURATION_IN_MINUTES) ? config.getInt(KEEP_ALIVE_DURATION_IN_MINUTES) : 5; 99 | 100 | return new OkHttpClient.Builder() 101 | .callTimeout(callTimeoutInMilliseconds, TimeUnit.MILLISECONDS) 102 | .connectionPool(new ConnectionPool(maxIdleConnections, keepAliveDuration, TimeUnit.SECONDS)) 103 | .build(); 104 | } 105 | 106 | @Override 107 | public void close() { 108 | LOGGER.info("Closing the http dispatcher now..."); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /api/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 4.0.0 5 | haystack-agent-api 6 | 7 | 8 | haystack-agent-core 9 | com.expedia.www 10 | 0.1.15-SNAPSHOT 11 | ../pom.xml 12 | 13 | 14 | 15 | 16 | com.fasterxml.jackson.core 17 | jackson-databind 18 | 19 | 20 | 21 | 22 | io.grpc 23 | grpc-all 24 | 25 | 26 | io.grpc 27 | grpc-services 28 | 29 | 30 | 31 | 32 | 33 | 34 | org.scalatest 35 | scalatest-maven-plugin 36 | 37 | 38 | test 39 | 40 | test 41 | 42 | 43 | 44 | v2 45 | 100 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | com.github.os72 55 | protoc-jar-maven-plugin 56 | 57 | 58 | generate-sources 59 | 60 | run 61 | 62 | 63 | com.google.protobuf:protoc:3.0.0 64 | 65 | ${project.basedir}/../haystack-idl/proto 66 | ${project.basedir}/../haystack-idl/proto/api 67 | 68 | 69 | ${project.basedir}/../haystack-idl/proto 70 | ${project.basedir}/../haystack-idl/proto/api 71 | 72 | 73 | 74 | java 75 | 76 | 77 | grpc-java 78 | io.grpc:protoc-gen-grpc-java:1.0.1 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | org.apache.maven.plugins 88 | maven-jar-plugin 89 | 3.0.2 90 | 91 | 92 | true 93 | 94 | true 95 | 96 | 97 | ${project.name} 98 | ${project.version} 99 | ${project.name} 100 | Expedia 101 | ${java.version} 102 | ${maven.build.timestamp} 103 | 104 | 105 | 106 | 107 | 108 | 109 | org.apache.maven.plugins 110 | maven-compiler-plugin 111 | 3.8.0 112 | 113 | ${project.jdk.version} 114 | ${project.jdk.version} 115 | 116 | 117 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /api/src/test/scala/com/expedia/www/haystack/agent/core/AgentLoaderSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Expedia, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package com.expedia.www.haystack.agent.core 19 | 20 | import java.util 21 | import java.util.ServiceConfigurationError 22 | 23 | import com.expedia.www.haystack.agent.core.helpers.ReplacingClassLoader 24 | import com.typesafe.config.ConfigFactory 25 | import org.scalatest.{Entry, FunSpec, Matchers} 26 | 27 | class AgentLoaderSpec extends FunSpec with Matchers { 28 | 29 | private val configServiceFile = "META-INF/services/com.expedia.www.haystack.agent.core.config.ConfigReader" 30 | private val agentServiceFile = "META-INF/services/com.expedia.www.haystack.agent.core.Agent" 31 | 32 | describe("Agent Loader") { 33 | it("should load the config spi") { 34 | val cl = new ReplacingClassLoader(getClass.getClassLoader, configServiceFile, "configProvider.txt") 35 | val cfg = new AgentLoader().loadConfig("file", new util.HashMap[String, String], cl) 36 | cfg.getConfig("agents") should not be null 37 | cfg.getConfig("agents").hasPath("spans") shouldBe true 38 | 39 | val agentConfig = cfg.getConfig("agents").getConfig("spans") 40 | 41 | agentConfig.getInt("port") shouldBe 8080 42 | agentConfig.getString("k1") shouldEqual "v1" 43 | 44 | agentConfig.getConfig("dispatchers") should not be null 45 | agentConfig.getConfig("dispatchers").hasPath("kinesis") shouldBe true 46 | } 47 | 48 | it("should fail to load the config spi with unknown name") { 49 | val cl = new ReplacingClassLoader(getClass.getClassLoader, configServiceFile, "configProvider.txt") 50 | val caught = intercept[ServiceConfigurationError] { 51 | new AgentLoader().loadConfig("http", new util.HashMap[String, String], cl) 52 | } 53 | caught.getMessage shouldEqual "Fail to load the config provider for type = http" 54 | } 55 | 56 | it("should load and initialize the agent using the config object") { 57 | val cl = new ReplacingClassLoader(getClass.getClassLoader, agentServiceFile, "singleAgentProvider.txt") 58 | 59 | val cfg = ConfigFactory.parseString( 60 | """ 61 | |agents { 62 | | spans { 63 | | enabled = true 64 | | k1 = "v1" 65 | | port = 8080 66 | | 67 | | dispatchers { 68 | | kinesis { 69 | | arn = "arn-1" 70 | | queueName = "myqueue" 71 | | } 72 | | } 73 | | } 74 | |} 75 | """.stripMargin) 76 | 77 | val runningAgents = new AgentLoader().loadAgents(cfg, cl, false) 78 | runningAgents.size() shouldBe 1 79 | runningAgents.get(0).close() 80 | } 81 | 82 | 83 | it("should not load the agent if disabled") { 84 | val cl = new ReplacingClassLoader(getClass.getClassLoader, agentServiceFile, "singleAgentProvider.txt") 85 | 86 | val cfg = ConfigFactory.parseString( 87 | """ 88 | |agents { 89 | | spans { 90 | | enabled = false 91 | | k1 = "v1" 92 | | port = 8080 93 | | 94 | | dispatchers { 95 | | kinesis { 96 | | arn = "arn-1" 97 | | queueName = "myqueue" 98 | | } 99 | | } 100 | | } 101 | |} 102 | """.stripMargin) 103 | 104 | val runningAgents = new AgentLoader().loadAgents(cfg, cl, false) 105 | runningAgents.size() shouldBe 0 106 | } 107 | 108 | it("should fail to load the agent for unidentified name") { 109 | val cl = new ReplacingClassLoader(getClass.getClassLoader, agentServiceFile, "singleAgentProvider.txt") 110 | 111 | val cfg = ConfigFactory.parseString( 112 | """ 113 | |agents { 114 | | blobs { 115 | | enabled = true 116 | | port = 8085 117 | | 118 | | dispatchers { 119 | | kafka { 120 | | queueName = "myqueue" 121 | | } 122 | | } 123 | | } 124 | |} 125 | """.stripMargin) 126 | 127 | val caught = intercept[ServiceConfigurationError] { 128 | new AgentLoader().loadAgents(cfg, cl, false) 129 | } 130 | caught.getMessage shouldEqual "Fail to load the agents with names=blobs" 131 | } 132 | 133 | it("should parse the null config reader args") { 134 | val result = AgentLoader.parseArgs(null) 135 | result.getKey shouldEqual "file" 136 | result.getValue shouldBe 'empty 137 | } 138 | 139 | it("should parse config reader args for file based reader") { 140 | val args = Array("--config-provider", "file", "--file-path", "/tmp/config.yaml") 141 | val result = AgentLoader.parseArgs(args) 142 | result.getKey shouldEqual "file" 143 | result.getValue should contain (Entry("--file-path", "/tmp/config.yaml")) 144 | } 145 | } 146 | } -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM http://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Maven2 Start Up Batch script 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM M2_HOME - location of maven2's installed home dir 28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending 30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | @REM e.g. to debug Maven itself, use 32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | @REM ---------------------------------------------------------------------------- 35 | 36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 37 | @echo off 38 | @REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' 39 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 40 | 41 | @REM set %HOME% to equivalent of $HOME 42 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 43 | 44 | @REM Execute a user defined script before this one 45 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 46 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 47 | if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" 48 | if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" 49 | :skipRcPre 50 | 51 | @setlocal 52 | 53 | set ERROR_CODE=0 54 | 55 | @REM To isolate internal variables from possible post scripts, we use another setlocal 56 | @setlocal 57 | 58 | @REM ==== START VALIDATION ==== 59 | if not "%JAVA_HOME%" == "" goto OkJHome 60 | 61 | echo. 62 | echo Error: JAVA_HOME not found in your environment. >&2 63 | echo Please set the JAVA_HOME variable in your environment to match the >&2 64 | echo location of your Java installation. >&2 65 | echo. 66 | goto error 67 | 68 | :OkJHome 69 | if exist "%JAVA_HOME%\bin\java.exe" goto init 70 | 71 | echo. 72 | echo Error: JAVA_HOME is set to an invalid directory. >&2 73 | echo JAVA_HOME = "%JAVA_HOME%" >&2 74 | echo Please set the JAVA_HOME variable in your environment to match the >&2 75 | echo location of your Java installation. >&2 76 | echo. 77 | goto error 78 | 79 | @REM ==== END VALIDATION ==== 80 | 81 | :init 82 | 83 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 84 | @REM Fallback to current working directory if not found. 85 | 86 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 87 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 88 | 89 | set EXEC_DIR=%CD% 90 | set WDIR=%EXEC_DIR% 91 | :findBaseDir 92 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 93 | cd .. 94 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 95 | set WDIR=%CD% 96 | goto findBaseDir 97 | 98 | :baseDirFound 99 | set MAVEN_PROJECTBASEDIR=%WDIR% 100 | cd "%EXEC_DIR%" 101 | goto endDetectBaseDir 102 | 103 | :baseDirNotFound 104 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 105 | cd "%EXEC_DIR%" 106 | 107 | :endDetectBaseDir 108 | 109 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 110 | 111 | @setlocal EnableExtensions EnableDelayedExpansion 112 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 113 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 114 | 115 | :endReadAdditionalConfig 116 | 117 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 118 | 119 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" 120 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 121 | 122 | %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 123 | if ERRORLEVEL 1 goto error 124 | goto end 125 | 126 | :error 127 | set ERROR_CODE=1 128 | 129 | :end 130 | @endlocal & set ERROR_CODE=%ERROR_CODE% 131 | 132 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost 133 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 134 | if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" 135 | if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" 136 | :skipRcPost 137 | 138 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 139 | if "%MAVEN_BATCH_PAUSE%" == "on" pause 140 | 141 | if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% 142 | 143 | exit /B %ERROR_CODE% 144 | -------------------------------------------------------------------------------- /agent-providers/span/src/test/scala/com/expedia/www/haystack/agent/span/spi/SpanAgentSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Expedia, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | 19 | package com.expedia.www.haystack.agent.span.spi 20 | 21 | import java.io.IOException 22 | import java.net.URL 23 | import java.util 24 | 25 | import com.expedia.open.tracing.Span 26 | import com.expedia.www.haystack.agent.span.enricher.Enricher 27 | import com.typesafe.config.ConfigFactory 28 | import org.scalatest.easymock.EasyMockSugar 29 | import org.scalatest.{FunSpec, Matchers} 30 | 31 | import scala.collection.JavaConversions._ 32 | 33 | class DummyEnricher extends Enricher { 34 | override def apply(span: Span.Builder): Unit = () 35 | } 36 | 37 | class SpanAgentSpec extends FunSpec with Matchers with EasyMockSugar { 38 | 39 | private val dispatcherLoadFile = "META-INF/services/com.expedia.www.haystack.agent.core.Dispatcher" 40 | 41 | describe("Span Agent") { 42 | it("should return the 'spans' as agent name") { 43 | new SpanAgent().getName shouldEqual "spans" 44 | } 45 | 46 | it("should load the dispatchers from the config") { 47 | val agent = new SpanAgent() 48 | val cfg = ConfigFactory.parseString( 49 | """ 50 | | k1 = "v1" 51 | | port = 8080 52 | | 53 | | dispatchers { 54 | | test-dispatcher { 55 | | queueName = "myqueue" 56 | | } 57 | | test-dispatcher-empty-config { 58 | | } 59 | | } 60 | """.stripMargin) 61 | 62 | val cl = new ReplacingClassLoader(getClass.getClassLoader, dispatcherLoadFile, "dispatcherProvider.txt") 63 | val dispatchers = agent.loadAndInitializeDispatchers(cfg, cl, "spans") 64 | dispatchers.size() shouldBe 2 65 | dispatchers.map(_.getName) should contain allOf ("test-dispatcher", "test-dispatcher-empty-config") 66 | dispatchers.foreach(_.close()) 67 | } 68 | 69 | it("should not load an unknown dispatcher") { 70 | val agent = new SpanAgent() 71 | val cfg = ConfigFactory.parseString( 72 | """ 73 | | k1 = "v1" 74 | | port = 8080 75 | | 76 | | dispatchers { 77 | | test-dispatcher { 78 | | queueName = "myqueue" 79 | | } 80 | | test-dispatcher-3 { 81 | | enabled = false 82 | | } 83 | | } 84 | """.stripMargin) 85 | 86 | val cl = new ReplacingClassLoader(getClass.getClassLoader, dispatcherLoadFile, "dispatcherProvider.txt") 87 | val dispatchers = agent.loadAndInitializeDispatchers(cfg, cl, "spans") 88 | dispatchers.size() shouldBe 1 89 | dispatchers.head.getName shouldBe "test-dispatcher" 90 | dispatchers.foreach(_.close()) 91 | } 92 | 93 | it("should not load a 'disabled' dispatcher") { 94 | val agent = new SpanAgent() 95 | val cfg = ConfigFactory.parseString( 96 | """ 97 | | k1 = "v1" 98 | | port = 8080 99 | | 100 | | dispatchers { 101 | | test-dispatcher { 102 | | queueName = "myqueue" 103 | | } 104 | | test-dispatcher-2 { 105 | | enabled = false 106 | | } 107 | | } 108 | """.stripMargin) 109 | 110 | val cl = new ReplacingClassLoader(getClass.getClassLoader, dispatcherLoadFile, "dispatcherProvider.txt") 111 | val dispatchers = agent.loadAndInitializeDispatchers(cfg, cl, "spans") 112 | dispatchers.size() shouldBe 1 113 | dispatchers.head.getName shouldBe "test-dispatcher" 114 | dispatchers.foreach(_.close()) 115 | } 116 | 117 | 118 | it("initialization should fail if no dispatchers exist") { 119 | val agent = new SpanAgent() 120 | val cfg = ConfigFactory.parseString( 121 | """ 122 | | k1 = "v1" 123 | | port = 8080 124 | | 125 | | dispatchers { 126 | | test-dispatcher { 127 | | queueName = "myqueue" 128 | | } 129 | | } 130 | """.stripMargin) 131 | 132 | val caught = intercept[Exception] { 133 | agent.loadAndInitializeDispatchers(cfg, getClass.getClassLoader, "spans") 134 | } 135 | 136 | caught.getMessage shouldEqual "Span agent dispatchers can't be an empty set" 137 | } 138 | 139 | it ("should load enrichers") { 140 | val agent = new SpanAgent() 141 | val cfg = ConfigFactory.parseString( 142 | """ 143 | | k1 = "v1" 144 | | port = 8080 145 | | 146 | | enrichers = [ 147 | | "com.expedia.www.haystack.agent.span.spi.DummyEnricher" 148 | | ] 149 | """.stripMargin) 150 | val enrichers = agent.loadSpanEnrichers(cfg) 151 | enrichers.length shouldBe 1 152 | } 153 | } 154 | 155 | class ReplacingClassLoader(val parent: ClassLoader, val resource: String, val replacement: String) extends ClassLoader(parent) { 156 | override def getResource(name: String): URL = { 157 | if (resource == name) { 158 | return getParent.getResource(replacement) 159 | } 160 | super.getResource(name) 161 | } 162 | 163 | @throws[IOException] 164 | override def getResources(name: String): util.Enumeration[URL] = { 165 | if (resource == name) { 166 | return getParent.getResources(replacement) 167 | } 168 | super.getResources(name) 169 | } 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /agent-dispatchers/http/src/test/scala/com/expedia/www/haystack/agent/dispatcher/HttpDispatcherSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Expedia, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package com.expedia.www.haystack.agent.dispatcher 19 | 20 | import com.codahale.metrics.{Meter, Timer} 21 | import com.expedia.open.tracing.Span 22 | import com.typesafe.config.ConfigFactory 23 | import okhttp3._ 24 | import org.easymock.EasyMock 25 | import org.scalatest.mock.EasyMockSugar 26 | import org.scalatest.{FunSpec, Matchers} 27 | 28 | class HttpDispatcherSpec extends FunSpec with Matchers with EasyMockSugar { 29 | describe("Http Span Dispatcher") { 30 | it("should dispatch span to http collector with success") { 31 | val dispatcher = new HttpDispatcher() 32 | val client = mock[OkHttpClient] 33 | val httpCall = mock[Call] 34 | val timer = mock[Timer] 35 | val dispatchFailure = mock[Meter] 36 | val responseBody = mock[ResponseBody] 37 | val httpResponse = new Response.Builder() 38 | .protocol(Protocol.HTTP_1_1) 39 | .request(new Request.Builder() 40 | .url("http://localhost:8080/span") 41 | .build()) 42 | .code(200) 43 | .body(responseBody) 44 | .message("ok") 45 | .build 46 | val timerContext = mock[Timer.Context] 47 | 48 | dispatcher.client = client 49 | dispatcher.url = "http://localhost:8080/span" 50 | dispatcher.dispatchTimer = timer 51 | dispatcher.dispatchFailure = dispatchFailure 52 | 53 | val capturedRequest = EasyMock.newCapture[Request]() 54 | 55 | httpCall.execute().andReturn(httpResponse) 56 | timer.time().andReturn(timerContext) 57 | 58 | expecting { 59 | client.newCall(EasyMock.capture(capturedRequest)).andReturn(httpCall).once() 60 | timerContext.close().once() 61 | } 62 | 63 | whenExecuting(client, httpCall, timer, dispatchFailure, timerContext) { 64 | val span = Span.newBuilder().setTraceId("traceid").build() 65 | dispatcher.dispatch(span.getTraceId.getBytes("utf-8"), span.toByteArray) 66 | dispatcher.close() 67 | } 68 | } 69 | 70 | it("should should record an error if the request was not successful") { 71 | val dispatcher = new HttpDispatcher() 72 | val client = mock[OkHttpClient] 73 | val httpCall = mock[Call] 74 | val timer = mock[Timer] 75 | val dispatchFailure = mock[Meter] 76 | val responseBody = mock[ResponseBody] 77 | val httpResponse = new Response.Builder() 78 | .protocol(Protocol.HTTP_1_1) 79 | .request(new Request.Builder() 80 | .url("http://localhost:8080/span") 81 | .build()) 82 | .code(500) // error status code 83 | .body(responseBody) 84 | .message("not ok") 85 | .build 86 | val timerContext = mock[Timer.Context] 87 | 88 | dispatcher.client = client 89 | dispatcher.url = "http://localhost:8080/span" 90 | dispatcher.dispatchTimer = timer 91 | dispatcher.dispatchFailure = dispatchFailure 92 | 93 | val capturedRequest = EasyMock.newCapture[Request]() 94 | 95 | httpCall.execute().andReturn(httpResponse) 96 | timer.time().andReturn(timerContext) 97 | 98 | expecting { 99 | client.newCall(EasyMock.capture(capturedRequest)).andReturn(httpCall).once() 100 | timerContext.close().once() 101 | dispatchFailure.mark().once() 102 | } 103 | 104 | whenExecuting(client, httpCall, timer, dispatchFailure, timerContext) { 105 | val span = Span.newBuilder().setTraceId("traceid").build() 106 | dispatcher.dispatch(span.getTraceId.getBytes("utf-8"), span.toByteArray) 107 | dispatcher.close() 108 | } 109 | } 110 | 111 | it("should should record an error if an exception is thrown") { 112 | val dispatcher = new HttpDispatcher() 113 | val client = mock[OkHttpClient] 114 | val httpCall = mock[Call] 115 | val timer = mock[Timer] 116 | val dispatchFailure = mock[Meter] 117 | val timerContext = mock[Timer.Context] 118 | 119 | dispatcher.client = client 120 | dispatcher.url = "http://localhost:8080/span" 121 | dispatcher.dispatchTimer = timer 122 | dispatcher.dispatchFailure = dispatchFailure 123 | 124 | val capturedRequest = EasyMock.newCapture[Request]() 125 | 126 | httpCall.execute().andStubThrow(new RuntimeException("error")) 127 | timer.time().andReturn(timerContext) 128 | 129 | expecting { 130 | client.newCall(EasyMock.capture(capturedRequest)).andReturn(httpCall).once() 131 | timerContext.close().once() 132 | dispatchFailure.mark().once() 133 | } 134 | 135 | whenExecuting(client, httpCall, timer, dispatchFailure, timerContext) { 136 | val span = Span.newBuilder().setTraceId("traceid").build() 137 | dispatcher.dispatch(span.getTraceId.getBytes("utf-8"), span.toByteArray) 138 | dispatcher.close() 139 | } 140 | } 141 | 142 | it("should fail to initialize http dispatcher if url property isn't present") { 143 | val dispatcher = new HttpDispatcher() 144 | val caught = intercept[Exception] { 145 | dispatcher.initialize(ConfigFactory.empty()) 146 | } 147 | caught.getMessage shouldEqual "No configuration setting found for key 'url'" 148 | } 149 | 150 | it("should set configs from input") { 151 | val dispatcher = new HttpDispatcher() 152 | 153 | val config = ConfigFactory.parseString( 154 | """ 155 | | url: "http://test:8080" 156 | | client.timeout.millis: 123 157 | """.stripMargin) 158 | 159 | dispatcher.initialize(config) 160 | 161 | dispatcher.url shouldBe "http://test:8080" 162 | dispatcher.client.callTimeoutMillis() shouldBe 123 163 | } 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /api/src/main/java/com/expedia/www/haystack/agent/core/AgentLoader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Expedia, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | package com.expedia.www.haystack.agent.core; 18 | 19 | import com.expedia.www.haystack.agent.core.config.ConfigReader; 20 | import com.expedia.www.haystack.agent.core.config.ConfigurationHelpers; 21 | import com.expedia.www.haystack.agent.core.metrics.SharedMetricRegistry; 22 | import com.google.common.annotations.VisibleForTesting; 23 | import com.typesafe.config.Config; 24 | import org.apache.commons.lang3.StringUtils; 25 | import org.apache.commons.lang3.tuple.ImmutablePair; 26 | import org.slf4j.Logger; 27 | import org.slf4j.LoggerFactory; 28 | 29 | import java.util.*; 30 | 31 | public class AgentLoader { 32 | 33 | private static Logger LOGGER = LoggerFactory.getLogger(AgentLoader.class); 34 | private final static String CONFIG_PROVIDER_ARG_NAME = "--config-provider"; 35 | 36 | AgentLoader() { } 37 | 38 | void run(String configProviderName, final Map configProviderArgs) throws Exception { 39 | SharedMetricRegistry.startJmxMetricReporter(); 40 | 41 | final ClassLoader cl = Thread.currentThread().getContextClassLoader(); 42 | final Config config = loadConfig(configProviderName, configProviderArgs, cl); 43 | final List runningAgents = loadAgents(config, cl, true); 44 | 45 | Runtime.getRuntime().addShutdownHook(new Thread(() -> { 46 | for (final Agent agent : runningAgents) { 47 | try { 48 | agent.close(); 49 | } catch(Exception ignored) { } 50 | } 51 | SharedMetricRegistry.closeJmxMetricReporter(); 52 | })); 53 | } 54 | 55 | @VisibleForTesting 56 | List loadAgents(final Config config, final ClassLoader cl, final boolean loadAgentOnSeparateThread) throws Exception { 57 | final ServiceLoader agentLoader = ServiceLoader.load(Agent.class, cl); 58 | final List loadedAgents = new ArrayList<>(); 59 | for (final Agent agent : agentLoader) { 60 | loadedAgents.add(agent); 61 | } 62 | 63 | final List runningAgents = new ArrayList<>(); 64 | final List missingAgents = new ArrayList<>(); 65 | 66 | for(final Map.Entry cfg : ConfigurationHelpers.readAgentConfigs(config).entrySet()) { 67 | final String agentName = cfg.getKey(); 68 | if(cfg.getValue().getBoolean("enabled")) { 69 | final Optional maybeAgent = loadedAgents 70 | .stream() 71 | .filter((l) -> l.getName().equalsIgnoreCase(agentName)) 72 | .findFirst(); 73 | 74 | if (maybeAgent.isPresent()) { 75 | final Agent agent = maybeAgent.get(); 76 | LOGGER.info("Initializing agent with name={} and config={}", agentName, cfg); 77 | if(loadAgentOnSeparateThread) { 78 | startAgentOnSeparateThread(agent, cfg.getValue()); 79 | } else { 80 | agent.initialize(cfg.getValue()); 81 | } 82 | runningAgents.add(agent); 83 | } else { 84 | missingAgents.add(agentName); 85 | } 86 | } else { 87 | LOGGER.info("Agent with name='{}' and config='{}' is disabled", agentName, cfg); 88 | } 89 | } 90 | 91 | if(!missingAgents.isEmpty()) { 92 | throw new ServiceConfigurationError("Fail to load the agents with names=" 93 | + StringUtils.join(missingAgents, ",")); 94 | } 95 | 96 | return runningAgents; 97 | } 98 | 99 | private void startAgentOnSeparateThread(final Agent agent, final Config config) { 100 | new Thread(() -> { 101 | try { 102 | agent.initialize(config); 103 | } catch (Exception e) { 104 | LOGGER.error("Fail to initialize the agent with config {}", config, e); 105 | } 106 | }).start(); 107 | } 108 | 109 | @VisibleForTesting 110 | Config loadConfig(final String configProviderName, 111 | final Map configProviderArgs, 112 | final ClassLoader cl) throws Exception { 113 | final ServiceLoader configLoader = ServiceLoader.load(ConfigReader.class, cl); 114 | for (final ConfigReader reader : configLoader) { 115 | if (reader.getName().equalsIgnoreCase(configProviderName)) { 116 | return reader.read(configProviderArgs); 117 | } 118 | } 119 | throw new ServiceConfigurationError("Fail to load the config provider for type = " + configProviderName); 120 | } 121 | 122 | @VisibleForTesting 123 | static ImmutablePair> parseArgs(String[] args) { 124 | final Map configProviderArgs = new HashMap<>(); 125 | String configProviderName = "file"; 126 | 127 | if(args != null) { 128 | for(int idx = 0; idx < args.length; idx = idx + 2) { 129 | if(Objects.equals(args[idx], CONFIG_PROVIDER_ARG_NAME)) { 130 | configProviderName = args[idx + 1]; 131 | } else { 132 | configProviderArgs.put(args[idx], args[idx + 1]); 133 | } 134 | } 135 | } 136 | 137 | return ImmutablePair.of(configProviderName, configProviderArgs); 138 | } 139 | 140 | public static void main(String[] args) throws Exception { 141 | final ImmutablePair> configReader = parseArgs(args); 142 | new AgentLoader().run(configReader.getKey(), configReader.getValue()); 143 | } 144 | } -------------------------------------------------------------------------------- /api/src/main/java/com/expedia/www/haystack/agent/core/config/ConfigurationHelpers.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Expedia, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package com.expedia.www.haystack.agent.core.config; 19 | 20 | import com.typesafe.config.Config; 21 | import com.typesafe.config.ConfigFactory; 22 | import org.apache.commons.lang3.StringUtils; 23 | 24 | import java.util.*; 25 | 26 | public class ConfigurationHelpers { 27 | // this property is injected into the config object passed to every dispatcher 28 | // it can be used by the logger or build metric names with agent name as prefix 29 | public static String AGENT_NAME_KEY = "agentName"; 30 | 31 | private final static String DEFAULT_HAYSTACK_AGENT_ENV_VAR_PREFIX = "HAYSTACK_PROP_"; 32 | private final static Boolean SHOULD_LOWERCASE_NORMALIZE_KEY = true; 33 | 34 | private static String envVarPrefix = DEFAULT_HAYSTACK_AGENT_ENV_VAR_PREFIX; 35 | private static Boolean shouldLowercaseNormalizeKey = SHOULD_LOWERCASE_NORMALIZE_KEY; 36 | 37 | private ConfigurationHelpers() { /* suppress pmd violation */ } 38 | 39 | public static void setEnvVarPrefix(final String prefix) { 40 | envVarPrefix = prefix; 41 | } 42 | 43 | public static void setShouldLowercaseNormalizeKey(Boolean shouldLowercaseNormalizeKey) { 44 | ConfigurationHelpers.shouldLowercaseNormalizeKey = shouldLowercaseNormalizeKey; 45 | } 46 | 47 | /** 48 | * convert the map of [string,string] to properties object 49 | * @param config map of key value pairs 50 | * @return a properties object 51 | */ 52 | public static Properties generatePropertiesFromMap(Map config) { 53 | final Properties properties = new Properties(); 54 | properties.putAll(config); 55 | return properties; 56 | } 57 | 58 | /** 59 | * create typesafe config object by first reading the configuration from environment variables 60 | * and then doing a fallback on the actual configuration string passed as argument. 61 | * Environment variables can be used to override the config. 62 | * @param configStr configuration passed to the app 63 | * @return final config object with env variables as overrides over actual configuration passed to the app 64 | */ 65 | public static Config load(final String configStr) { 66 | return loadFromEnvVars().withFallback(ConfigFactory.parseString(configStr)); 67 | } 68 | 69 | /** 70 | * parse the agent configurations from the root config 71 | * @param config main configuration 72 | * @return map of agentNames and their corresponding config object 73 | */ 74 | public static Map readAgentConfigs(final Config config) { 75 | final Map agentConfigs = new HashMap<>(); 76 | final Config agentsConfig = config.getConfig("agents"); 77 | 78 | final Set agentNames = new HashSet<>(); 79 | agentsConfig.entrySet().forEach((e) -> agentNames.add(findRootKeyName(e.getKey()))); 80 | agentNames.forEach((name) -> agentConfigs.put(name, agentsConfig.getConfig(name))); 81 | return agentConfigs; 82 | } 83 | 84 | /** 85 | * parse the dispatcher configurations from the agent's config section 86 | * agent's name is injected into each dispatcher config object, by default 87 | * @param agentConfig agent's config section 88 | * @param agentName name of agent 89 | * @return map of dispatcherNames and their corresponding config object 90 | */ 91 | public static Map readDispatchersConfig(final Config agentConfig, final String agentName) { 92 | final Config dispatchers = agentConfig.getConfig("dispatchers"); 93 | final Map dispatchersConfigMap = new HashMap<>(); 94 | 95 | final Set dispatcherNames = new HashSet<>(); 96 | dispatchers.root().keySet().forEach((e) -> dispatcherNames.add(findRootKeyName(e))); 97 | 98 | dispatcherNames.forEach((name) -> dispatchersConfigMap.put(name, addAgentNameToConfig(dispatchers.getConfig(name), agentName))); 99 | return dispatchersConfigMap; 100 | } 101 | 102 | /** 103 | * converts typesafe config object to a map of string,string 104 | * @param conf typesafe config object 105 | * @return map of key, value pairs 106 | */ 107 | public static Map convertToPropertyMap(final Config conf) { 108 | final Map props = new HashMap<>(); 109 | conf.entrySet().forEach((e) -> props.put(e.getKey(), e.getValue().unwrapped().toString())); 110 | return props; 111 | } 112 | 113 | private static Config addAgentNameToConfig(final Config config, final String agentName) { 114 | return config.withFallback(ConfigFactory.parseString(AGENT_NAME_KEY + " = " + agentName)); 115 | } 116 | 117 | private static boolean isHaystackAgentEnvVar(final String envKey) { 118 | return envKey.startsWith(envVarPrefix); 119 | } 120 | 121 | private static Config loadFromEnvVars() { 122 | final Map envMap = new HashMap<>(); 123 | System.getenv().entrySet().stream() 124 | .filter((e) -> isHaystackAgentEnvVar(e.getKey())) 125 | .forEach((e) -> { 126 | final String normalizedKey = getNormalizedKey(e.getKey()); 127 | envMap.put(normalizedKey, e.getValue()); 128 | }); 129 | 130 | return ConfigFactory.parseMap(envMap); 131 | } 132 | 133 | private static String getNormalizedKey(String key) { 134 | String normalizedKey = key.replaceFirst(envVarPrefix, "") 135 | .replace('_', '.'); 136 | return shouldLowercaseNormalizeKey ? normalizedKey.toLowerCase() : normalizedKey ; 137 | } 138 | 139 | // extracts the root keyname, for e.g. if the path given is 'x.y.z' then rootKey is 'x' 140 | private static String findRootKeyName(final String path) { 141 | return StringUtils.split(path, ".")[0]; 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /agent-providers/span/src/test/scala/com/expedia/www/haystack/agent/pitchfork/PitchforkServiceSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Expedia, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package com.expedia.www.haystack.agent.pitchfork 19 | 20 | import java.io.ByteArrayOutputStream 21 | import java.util 22 | import java.util.Collections 23 | import java.util.zip.GZIPOutputStream 24 | 25 | import com.expedia.www.haystack.agent.core.Dispatcher 26 | import com.expedia.www.haystack.agent.pitchfork.processors.{SpanValidator, ZipkinSpanProcessorFactory} 27 | import com.expedia.www.haystack.agent.pitchfork.service.PitchforkService 28 | import com.squareup.okhttp.{MediaType, OkHttpClient, Request, RequestBody} 29 | import com.typesafe.config.ConfigFactory 30 | import org.easymock.EasyMock 31 | import org.scalatest.easymock.EasyMockSugar 32 | import org.scalatest.{FunSpec, Matchers} 33 | import zipkin2.Endpoint 34 | import zipkin2.codec.SpanBytesEncoder 35 | 36 | import scala.collection.JavaConverters._ 37 | 38 | class PitchforkServiceSpec extends FunSpec with Matchers with EasyMockSugar { 39 | 40 | private val client = new OkHttpClient() 41 | 42 | private def zipkinSpan(traceId: String): zipkin2.Span = { 43 | zipkin2.Span.newBuilder() 44 | .traceId(traceId) 45 | .id(1) 46 | .parentId(2) 47 | .name("/foo") 48 | .localEndpoint(Endpoint.newBuilder().serviceName("foo").build()) 49 | .remoteEndpoint(Endpoint.newBuilder().serviceName("bar").port(8080).ip("10.10.10.10").build()) 50 | .timestamp(System.currentTimeMillis() * 1000) 51 | .duration(100000l) 52 | .putTag("error", "true") 53 | .putTag("pos", "1").build() 54 | } 55 | 56 | describe("Pitchfork Agent Http service") { 57 | it("should dispatch the zipkinv2 span successfully") { 58 | runZipkinV2SpanTest(false) 59 | } 60 | 61 | it("should dispatch compressed zipkinv2 span successfully") { 62 | runZipkinV2SpanTest(true) 63 | } 64 | 65 | def runZipkinV2SpanTest(compress: Boolean) : Unit = { 66 | val mockDispatcher = mock[Dispatcher] 67 | val config = ConfigFactory.parseMap(Map("port" -> 9115, "http.threads.min" -> 2, "http.threads.max" -> 4, "gzip.enabled" -> compress).asJava) 68 | 69 | val keyCapture = EasyMock.newCapture[Array[Byte]]() 70 | val haystackSpanCapture = EasyMock.newCapture[Array[Byte]]() 71 | 72 | expecting { 73 | mockDispatcher.getName.andReturn("mock") 74 | mockDispatcher.dispatch(EasyMock.capture(keyCapture), EasyMock.capture(haystackSpanCapture)) 75 | } 76 | 77 | whenExecuting(mockDispatcher) { 78 | val service = new PitchforkService(config, new ZipkinSpanProcessorFactory(new SpanValidator(config), 79 | Collections.singletonList(mockDispatcher), Collections.emptyList())) 80 | 81 | service.start() 82 | 83 | // let the server start 84 | Thread.sleep(5000) 85 | 86 | val request = newRequest(compress) 87 | 88 | val response = client.newCall(request).execute() 89 | response.code() shouldBe 200 90 | service.stop() 91 | } 92 | } 93 | 94 | def newRequest(compress: Boolean) : Request = { 95 | val requestBuilder = new Request.Builder() 96 | .url("http://localhost:9115" + "/api/v2/spans") 97 | 98 | var data = SpanBytesEncoder.JSON_V2.encode(zipkinSpan("0000000000000064")) 99 | if (compress) { 100 | val bos = new ByteArrayOutputStream() 101 | val gzip = new GZIPOutputStream(bos) 102 | try { 103 | gzip.write(data) 104 | gzip.finish() 105 | } finally { 106 | gzip.close() 107 | bos.close() 108 | } 109 | data = bos.toByteArray 110 | requestBuilder.addHeader("Content-Encoding", "gzip") 111 | } 112 | 113 | val body = RequestBody.create(MediaType.parse("application/json"), data) 114 | requestBuilder.post(body) 115 | 116 | requestBuilder.build() 117 | } 118 | 119 | it("should dispatch the proto spans successfully") { 120 | val mockDispatcher = mock[Dispatcher] 121 | val config = ConfigFactory.parseMap(Map("port" -> 9112, "http.threads.min" -> 2, "http.threads.max" -> 4).asJava) 122 | 123 | val keyCapture_1 = EasyMock.newCapture[Array[Byte]]() 124 | val haystackSpanCapture_1 = EasyMock.newCapture[Array[Byte]]() 125 | 126 | val keyCapture_2 = EasyMock.newCapture[Array[Byte]]() 127 | val haystackSpanCapture_2 = EasyMock.newCapture[Array[Byte]]() 128 | 129 | expecting { 130 | mockDispatcher.getName.andReturn("mock").times(2) 131 | mockDispatcher.dispatch(EasyMock.capture(keyCapture_1), EasyMock.capture(haystackSpanCapture_1)) 132 | mockDispatcher.dispatch(EasyMock.capture(keyCapture_2), EasyMock.capture(haystackSpanCapture_2)) 133 | } 134 | 135 | whenExecuting(mockDispatcher) { 136 | val service = new PitchforkService(config, new ZipkinSpanProcessorFactory(new SpanValidator(config), 137 | Collections.singletonList(mockDispatcher), Collections.emptyList())) 138 | 139 | service.start() 140 | 141 | // let the server start 142 | Thread.sleep(5000) 143 | 144 | val body = RequestBody.create( 145 | MediaType.parse("application/x-protobuf"), SpanBytesEncoder.PROTO3.encodeList(util.Arrays.asList( 146 | zipkinSpan("0000000000000065"), 147 | zipkinSpan("0000000000000066")))) 148 | 149 | val request = new Request.Builder() 150 | .url("http://localhost:9112" + "/api/v2/spans") 151 | .post(body) 152 | .build() 153 | 154 | val response = client.newCall(request).execute() 155 | // response.code() shouldBe 200 156 | // 157 | // new String(keyCapture_1.getValue) shouldEqual "0000000000000065" 158 | // val haystackSpan_1 = Span.parseFrom(haystackSpanCapture_1.getValue) 159 | // haystackSpan_1.getTraceId shouldEqual "0000000000000065" 160 | // 161 | // 162 | // new String(keyCapture_2.getValue) shouldEqual "0000000000000066" 163 | // val haystackSpan_2 = Span.parseFrom(haystackSpanCapture_2.getValue) 164 | // haystackSpan_2.getTraceId shouldEqual "0000000000000066" 165 | 166 | service.stop() 167 | } 168 | } 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Maven2 Start Up Batch script 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # M2_HOME - location of maven2's installed home dir 31 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 32 | # e.g. to debug Maven itself, use 33 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 34 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 35 | # ---------------------------------------------------------------------------- 36 | 37 | if [ -z "$MAVEN_SKIP_RC" ] ; then 38 | 39 | if [ -f /etc/mavenrc ] ; then 40 | . /etc/mavenrc 41 | fi 42 | 43 | if [ -f "$HOME/.mavenrc" ] ; then 44 | . "$HOME/.mavenrc" 45 | fi 46 | 47 | fi 48 | 49 | # OS specific support. $var _must_ be set to either true or false. 50 | cygwin=false; 51 | darwin=false; 52 | mingw=false 53 | case "`uname`" in 54 | CYGWIN*) cygwin=true ;; 55 | MINGW*) mingw=true;; 56 | Darwin*) darwin=true 57 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home 58 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html 59 | if [ -z "$JAVA_HOME" ]; then 60 | if [ -x "/usr/libexec/java_home" ]; then 61 | export JAVA_HOME="`/usr/libexec/java_home`" 62 | else 63 | export JAVA_HOME="/Library/Java/Home" 64 | fi 65 | fi 66 | ;; 67 | esac 68 | 69 | if [ -z "$JAVA_HOME" ] ; then 70 | if [ -r /etc/gentoo-release ] ; then 71 | JAVA_HOME=`java-config --jre-home` 72 | fi 73 | fi 74 | 75 | if [ -z "$M2_HOME" ] ; then 76 | ## resolve links - $0 may be a link to maven's home 77 | PRG="$0" 78 | 79 | # need this for relative symlinks 80 | while [ -h "$PRG" ] ; do 81 | ls=`ls -ld "$PRG"` 82 | link=`expr "$ls" : '.*-> \(.*\)$'` 83 | if expr "$link" : '/.*' > /dev/null; then 84 | PRG="$link" 85 | else 86 | PRG="`dirname "$PRG"`/$link" 87 | fi 88 | done 89 | 90 | saveddir=`pwd` 91 | 92 | M2_HOME=`dirname "$PRG"`/.. 93 | 94 | # make it fully qualified 95 | M2_HOME=`cd "$M2_HOME" && pwd` 96 | 97 | cd "$saveddir" 98 | # echo Using m2 at $M2_HOME 99 | fi 100 | 101 | # For Cygwin, ensure paths are in UNIX format before anything is touched 102 | if $cygwin ; then 103 | [ -n "$M2_HOME" ] && 104 | M2_HOME=`cygpath --unix "$M2_HOME"` 105 | [ -n "$JAVA_HOME" ] && 106 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 107 | [ -n "$CLASSPATH" ] && 108 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"` 109 | fi 110 | 111 | # For Mingw, ensure paths are in UNIX format before anything is touched 112 | if $mingw ; then 113 | [ -n "$M2_HOME" ] && 114 | M2_HOME="`(cd "$M2_HOME"; pwd)`" 115 | [ -n "$JAVA_HOME" ] && 116 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" 117 | # TODO classpath? 118 | fi 119 | 120 | if [ -z "$JAVA_HOME" ]; then 121 | javaExecutable="`which javac`" 122 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then 123 | # readlink(1) is not available as standard on Solaris 10. 124 | readLink=`which readlink` 125 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then 126 | if $darwin ; then 127 | javaHome="`dirname \"$javaExecutable\"`" 128 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" 129 | else 130 | javaExecutable="`readlink -f \"$javaExecutable\"`" 131 | fi 132 | javaHome="`dirname \"$javaExecutable\"`" 133 | javaHome=`expr "$javaHome" : '\(.*\)/bin'` 134 | JAVA_HOME="$javaHome" 135 | export JAVA_HOME 136 | fi 137 | fi 138 | fi 139 | 140 | if [ -z "$JAVACMD" ] ; then 141 | if [ -n "$JAVA_HOME" ] ; then 142 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 143 | # IBM's JDK on AIX uses strange locations for the executables 144 | JAVACMD="$JAVA_HOME/jre/sh/java" 145 | else 146 | JAVACMD="$JAVA_HOME/bin/java" 147 | fi 148 | else 149 | JAVACMD="`which java`" 150 | fi 151 | fi 152 | 153 | if [ ! -x "$JAVACMD" ] ; then 154 | echo "Error: JAVA_HOME is not defined correctly." >&2 155 | echo " We cannot execute $JAVACMD" >&2 156 | exit 1 157 | fi 158 | 159 | if [ -z "$JAVA_HOME" ] ; then 160 | echo "Warning: JAVA_HOME environment variable is not set." 161 | fi 162 | 163 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher 164 | 165 | # traverses directory structure from process work directory to filesystem root 166 | # first directory with .mvn subdirectory is considered project base directory 167 | find_maven_basedir() { 168 | 169 | if [ -z "$1" ] 170 | then 171 | echo "Path not specified to find_maven_basedir" 172 | return 1 173 | fi 174 | 175 | basedir="$1" 176 | wdir="$1" 177 | while [ "$wdir" != '/' ] ; do 178 | if [ -d "$wdir"/.mvn ] ; then 179 | basedir=$wdir 180 | break 181 | fi 182 | # workaround for JBEAP-8937 (on Solaris 10/Sparc) 183 | if [ -d "${wdir}" ]; then 184 | wdir=`cd "$wdir/.."; pwd` 185 | fi 186 | # end of workaround 187 | done 188 | echo "${basedir}" 189 | } 190 | 191 | # concatenates all lines of a file 192 | concat_lines() { 193 | if [ -f "$1" ]; then 194 | echo "$(tr -s '\n' ' ' < "$1")" 195 | fi 196 | } 197 | 198 | BASE_DIR=`find_maven_basedir "$(pwd)"` 199 | if [ -z "$BASE_DIR" ]; then 200 | exit 1; 201 | fi 202 | 203 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} 204 | if [ "$MVNW_VERBOSE" = true ]; then 205 | echo $MAVEN_PROJECTBASEDIR 206 | fi 207 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 208 | 209 | # For Cygwin, switch paths to Windows format before running java 210 | if $cygwin; then 211 | [ -n "$M2_HOME" ] && 212 | M2_HOME=`cygpath --path --windows "$M2_HOME"` 213 | [ -n "$JAVA_HOME" ] && 214 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 215 | [ -n "$CLASSPATH" ] && 216 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 217 | [ -n "$MAVEN_PROJECTBASEDIR" ] && 218 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` 219 | fi 220 | 221 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 222 | 223 | exec "$JAVACMD" \ 224 | $MAVEN_OPTS \ 225 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 226 | "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 227 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" 228 | -------------------------------------------------------------------------------- /agent-dispatchers/kinesis/src/test/scala/com/expedia/www/haystack/agent/dispatcher/KinesisSpanDispatcherSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Expedia, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package com.expedia.www.haystack.agent.dispatcher 19 | 20 | import java.nio.ByteBuffer 21 | import java.util 22 | 23 | import com.amazonaws.auth.profile.internal.securitytoken.STSProfileCredentialsServiceProvider 24 | import com.amazonaws.auth.{AWSStaticCredentialsProvider, DefaultAWSCredentialsProviderChain} 25 | import com.amazonaws.services.kinesis.producer.{KinesisProducer, UserRecordResult} 26 | import com.codahale.metrics.{Meter, Timer} 27 | import com.expedia.open.tracing.Span 28 | import com.expedia.www.haystack.agent.dispatcher.KinesisDispatcher._ 29 | import com.google.common.util.concurrent.ListenableFuture 30 | import org.easymock.EasyMock 31 | import org.scalatest.easymock.EasyMockSugar 32 | import org.scalatest.{FunSpec, Matchers} 33 | 34 | class KinesisSpanDispatcherSpec extends FunSpec with Matchers with EasyMockSugar { 35 | 36 | private val REGION_NAME_KEY = "Region" 37 | private val METRIC_LEVEL_KEY = "MetricsLevel" 38 | protected val DEFAULT_OUTSTANDING_RECORDS_LIMIT = 10000L 39 | 40 | val streamName = "test" 41 | val region = "us-west-2" 42 | val metricLevel = "detailed" 43 | val outstandingRecordsLimit: Integer = 1000 44 | 45 | describe("Kinesis Dispatcher") { 46 | 47 | it("given a config object to be initialized it should be able to fetch the stream name and outstandin records limit") { 48 | val props = new util.HashMap[String, String]() 49 | props.put(STREAM_NAME_KEY, streamName) 50 | props.put(REGION_NAME_KEY, region) 51 | props.put(OUTSTANDING_RECORD_LIMIT_KEY, outstandingRecordsLimit.toString) 52 | props.put(METRIC_LEVEL_KEY, metricLevel) 53 | 54 | val dispatcher = new KinesisDispatcher() 55 | 56 | dispatcher.getAndRemoveStreamNameKey(props) shouldEqual streamName 57 | dispatcher.getAndRemoveOutstandingRecordLimitKey(props) shouldEqual outstandingRecordsLimit 58 | props.containsKey(KinesisDispatcher.STREAM_NAME_KEY) shouldBe false 59 | props.containsKey(KinesisDispatcher.OUTSTANDING_RECORD_LIMIT_KEY) shouldBe false 60 | } 61 | 62 | it("should be able to build the kinesis producer configuration using the same keys as in the config passed") { 63 | val streamName = "test" 64 | val region = "us-west-2" 65 | val metricLevel = "detailed" 66 | val props = new util.HashMap[String, String]() 67 | props.put(STREAM_NAME_KEY, streamName) 68 | props.put(REGION_NAME_KEY, region) 69 | props.put(OUTSTANDING_RECORD_LIMIT_KEY, outstandingRecordsLimit.toString) 70 | props.put(METRIC_LEVEL_KEY, metricLevel) 71 | 72 | val dispatcher = new KinesisDispatcher() 73 | val config = dispatcher.buildKinesisProducerConfiguration(props) 74 | 75 | config.getRegion shouldEqual region 76 | config.getMetricsLevel shouldEqual metricLevel 77 | config.getCredentialsProvider shouldBe DefaultAWSCredentialsProviderChain.getInstance() 78 | } 79 | 80 | 81 | it("should be able to build the kinesis producer configuration with sts role arn") { 82 | val streamName = "test" 83 | val region = "us-west-2" 84 | val props = new util.HashMap[String, String]() 85 | props.put(STREAM_NAME_KEY, streamName) 86 | props.put(REGION_NAME_KEY, region) 87 | props.put(OUTSTANDING_RECORD_LIMIT_KEY, outstandingRecordsLimit.toString) 88 | props.put(STS_ROLE_ARN, "some-arn") 89 | 90 | val dispatcher = new KinesisDispatcher() 91 | val config = dispatcher.buildKinesisProducerConfiguration(props) 92 | 93 | config.getRegion shouldEqual region 94 | config.getCredentialsProvider.getClass shouldBe classOf[STSProfileCredentialsServiceProvider] 95 | } 96 | 97 | it("should be able to build the kinesis producer configuration with aws access and secret keys") { 98 | val streamName = "test" 99 | val region = "us-west-2" 100 | val props = new util.HashMap[String, String]() 101 | props.put(STREAM_NAME_KEY, streamName) 102 | props.put(REGION_NAME_KEY, region) 103 | props.put(OUTSTANDING_RECORD_LIMIT_KEY, outstandingRecordsLimit.toString) 104 | props.put(AWS_ACCESS_KEY, "my-access-key") 105 | props.put(AWS_SECRET_KEY, "my-secret-key") 106 | 107 | val dispatcher = new KinesisDispatcher() 108 | val config = dispatcher.buildKinesisProducerConfiguration(props) 109 | 110 | config.getRegion shouldEqual region 111 | config.getCredentialsProvider.getClass shouldBe classOf[AWSStaticCredentialsProvider] 112 | val credsProvider = config.getCredentialsProvider.asInstanceOf[AWSStaticCredentialsProvider].getCredentials 113 | credsProvider.getAWSAccessKeyId shouldEqual "my-access-key" 114 | credsProvider.getAWSSecretKey shouldEqual "my-secret-key" 115 | } 116 | 117 | it("should dispatch span to kinesis") { 118 | val dispatcher = new KinesisDispatcher() 119 | val kinesisProducer = mock[KinesisProducer] 120 | val responseFuture = mock[ListenableFuture[UserRecordResult]] 121 | val timer = mock[Timer] 122 | val timerContext = mock[Timer.Context] 123 | 124 | dispatcher.producer = kinesisProducer 125 | dispatcher.streamName = "mystream" 126 | dispatcher.outstandingRecordsLimit = 1000 127 | dispatcher.dispatchTimer = timer 128 | 129 | val span = Span.newBuilder().setTraceId("traceid").build() 130 | 131 | expecting { 132 | kinesisProducer.getOutstandingRecordsCount.andReturn(10).once 133 | kinesisProducer.addUserRecord("mystream", "traceid", ByteBuffer.wrap(span.toByteArray)).andReturn(responseFuture).once() 134 | kinesisProducer.flushSync().once() 135 | kinesisProducer.destroy().once() 136 | responseFuture.addListener(EasyMock.anyObject(), EasyMock.anyObject()) 137 | timer.time().andReturn(timerContext) 138 | } 139 | 140 | whenExecuting(kinesisProducer, responseFuture, timer, timerContext) { 141 | dispatcher.dispatch(span.getTraceId.getBytes("utf-8"), span.toByteArray) 142 | dispatcher.close() 143 | } 144 | } 145 | 146 | it("should fail dispatch span to kinesis with outstanding limit error") { 147 | val dispatcher = new KinesisDispatcher() 148 | val kinesisProducer = mock[KinesisProducer] 149 | val outstandRecErrorMeter = mock[Meter] 150 | 151 | dispatcher.producer = kinesisProducer 152 | dispatcher.streamName = "mystream" 153 | dispatcher.outstandingRecordsLimit = 1000 154 | dispatcher.outstandingRecordsError = outstandRecErrorMeter 155 | 156 | val span = Span.newBuilder().setTraceId("traceid").build() 157 | 158 | expecting { 159 | kinesisProducer.getOutstandingRecordsCount.andReturn(1001).anyTimes() 160 | outstandRecErrorMeter.mark() 161 | } 162 | 163 | whenExecuting(kinesisProducer, outstandRecErrorMeter) { 164 | val caught = intercept[Exception] { 165 | dispatcher.dispatch(span.getTraceId.getBytes("utf-8"), span.toByteArray) 166 | } 167 | 168 | caught.getMessage shouldEqual "fail to dispatch to kinesis due to rate limit, outstanding records: 1001" 169 | } 170 | } 171 | } 172 | } 173 | 174 | 175 | 176 | -------------------------------------------------------------------------------- /agent-providers/span/src/main/java/com/expedia/www/haystack/agent/pitchfork/processors/HaystackDomainConverter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Expedia, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package com.expedia.www.haystack.agent.pitchfork.processors; 19 | 20 | 21 | import com.expedia.open.tracing.Log; 22 | import com.expedia.open.tracing.Span; 23 | import com.expedia.open.tracing.Tag; 24 | import org.apache.commons.lang3.StringUtils; 25 | import zipkin2.Endpoint; 26 | 27 | import java.util.*; 28 | import java.util.function.Consumer; 29 | import java.util.function.Supplier; 30 | 31 | import static java.util.Optional.empty; 32 | 33 | /** 34 | * Converter between {@code Zipkin} and {@code Haystack} domains. 35 | */ 36 | @SuppressWarnings("PMD.UnusedPrivateMethod") 37 | class HaystackDomainConverter { 38 | 39 | private static final String SPAN_KIND_TAG_KEY = "span.kind"; 40 | 41 | private HaystackDomainConverter() { } 42 | /** 43 | * Accepts a span in {@code Zipkin V2} format and returns a span in {@code Haystack} format. 44 | */ 45 | static Span fromZipkinV2(final zipkin2.Span zipkin) { 46 | Span.Builder builder = Span.newBuilder() 47 | .setTraceId(zipkin.traceId()) 48 | .setSpanId(zipkin.id()); 49 | 50 | doIfNotNull(zipkin.name(), builder::setOperationName); 51 | doIfNotNull(zipkin.timestamp(), builder::setStartTime); 52 | doIfNotNull(zipkin.duration(), builder::setDuration); 53 | doIfNotNull(zipkin.parentId(), builder::setParentSpanId); 54 | doIfNotNull(zipkin.localServiceName(), builder::setServiceName); 55 | 56 | builder.addAllTags(addRemoteEndpointAsTags(zipkin.remoteEndpoint())); 57 | 58 | if (!spanKindTagPresent(zipkin)) { 59 | getTagForKind(zipkin.kind()).ifPresent(builder::addTags); 60 | } 61 | 62 | if (zipkin.tags() != null && !zipkin.tags().isEmpty()) { 63 | zipkin.tags().forEach((key, value) -> { 64 | List tagStream = fromZipkinTag(key, value); 65 | builder.addAllTags(tagStream); 66 | }); 67 | } 68 | 69 | if (zipkin.annotations() != null && !zipkin.annotations().isEmpty()) { 70 | zipkin.annotations().forEach(annotation -> { 71 | final Tag tag = Tag.newBuilder().setKey("annotation").setVStr(annotation.value()).build(); 72 | final Log log = Log.newBuilder().setTimestamp(annotation.timestamp()).addFields(tag).build(); 73 | builder.addLogs(log); 74 | }); 75 | } 76 | return builder.build(); 77 | } 78 | 79 | private static List addRemoteEndpointAsTags(Endpoint remote) { 80 | final List remoteTags = new ArrayList<>(); 81 | if (remote != null) { 82 | buildStringTag("remote.service.name", remote::serviceName).ifPresent(remoteTags::add); 83 | buildStringTag("remote.service.ipv4", remote::ipv4).ifPresent(remoteTags::add); 84 | buildStringTag("remote.service.ipv6", remote::ipv6).ifPresent(remoteTags::add); 85 | buildIntTag("remote.service.port", remote::port).ifPresent(remoteTags::add); 86 | } 87 | return remoteTags; 88 | } 89 | 90 | private static Optional buildIntTag(final String tagKey, final Supplier tagValueSupplier) { 91 | final Number tagValue = tagValueSupplier.get(); 92 | if (tagValue != null) { 93 | return Optional.of(Tag.newBuilder() 94 | .setKey(tagKey) 95 | .setVLong((Integer)tagValue) 96 | .setType(Tag.TagType.LONG).build()); 97 | } 98 | return Optional.empty(); 99 | } 100 | 101 | private static Optional buildStringTag(final String tagKey, final Supplier tagValueSupplier) { 102 | final String tagValue = tagValueSupplier.get(); 103 | if (StringUtils.isNotEmpty(tagValue)) { 104 | return Optional.of(Tag.newBuilder() 105 | .setKey(tagKey) 106 | .setVStr(tagValue) 107 | .setType(Tag.TagType.STRING).build()); 108 | } 109 | return Optional.empty(); 110 | } 111 | 112 | private static void doIfNotNull(T nullable, Consumer runnable) { 113 | if (nullable != null) { 114 | runnable.accept(nullable); 115 | } 116 | } 117 | 118 | private static boolean spanKindTagPresent(zipkin2.Span zipkinSpan) { 119 | return zipkinSpan.tags() != null && 120 | !zipkinSpan.tags().isEmpty() && 121 | zipkinSpan.tags().keySet().contains(SPAN_KIND_TAG_KEY); 122 | } 123 | 124 | private static Optional getTagForKind(zipkin2.Span.Kind kind) { 125 | String value; 126 | 127 | if (kind != null) { 128 | switch (kind) { 129 | case CLIENT: 130 | value = "client"; 131 | break; 132 | case SERVER: 133 | value = "server"; 134 | break; 135 | case CONSUMER: 136 | value = "consumer"; 137 | break; 138 | case PRODUCER: 139 | value = "producer"; 140 | break; 141 | default: 142 | throw new RuntimeException(String.format("Fail to recognize the span kind %s!", kind)); 143 | } 144 | 145 | return Optional.of(Tag.newBuilder() 146 | .setKey(SPAN_KIND_TAG_KEY) 147 | .setVStr(value) 148 | .setType(Tag.TagType.STRING) 149 | .build()); 150 | } else { 151 | return empty(); 152 | } 153 | } 154 | 155 | private static List fromZipkinTag(String key, String value) { 156 | if ("error".equalsIgnoreCase(key)) { 157 | // Zipkin error tags are Strings where as in Haystack they're Booleans 158 | // Since a Zipkin error tag may contain relevant information about the error we expand it into two tags (error + error message) 159 | if (!"false".equalsIgnoreCase(value)) { 160 | Tag errorTag = Tag.newBuilder() 161 | .setKey(key) 162 | .setVBool(true) 163 | .setType(Tag.TagType.BOOL) 164 | .build(); 165 | 166 | Tag errorMessageTag = Tag.newBuilder() 167 | .setKey("error_msg") 168 | .setVStr(value) 169 | .setType(Tag.TagType.STRING) 170 | .build(); 171 | 172 | return Arrays.asList(errorTag, errorMessageTag); 173 | } else { 174 | final Tag errorTag = Tag.newBuilder() 175 | .setKey(key) 176 | .setVBool(false) 177 | .setType(Tag.TagType.BOOL) 178 | .build(); 179 | 180 | return Collections.singletonList(errorTag); 181 | } 182 | } 183 | 184 | final Tag tag = Tag.newBuilder() 185 | .setKey(key) 186 | .setVStr(value) 187 | .setType(Tag.TagType.STRING) 188 | .build(); 189 | 190 | return Collections.singletonList(tag); 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /agent-providers/span/src/test/scala/com/expedia/www/haystack/agent/span/service/SpanAgentGrpcServiceSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Expedia, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package com.expedia.www.haystack.agent.span.service 19 | 20 | import java.util 21 | import java.util.Collections 22 | import java.util.function.Predicate 23 | 24 | import com.expedia.open.tracing.{Span, Tag} 25 | import com.expedia.open.tracing.agent.api.DispatchResult 26 | import com.expedia.open.tracing.agent.api.DispatchResult.ResultCode 27 | import com.expedia.www.haystack.agent.core.{Dispatcher, RateLimitException} 28 | import com.expedia.www.haystack.agent.span.enricher.Enricher 29 | import io.grpc.stub.StreamObserver 30 | import org.easymock.EasyMock 31 | import org.scalatest.easymock.EasyMockSugar 32 | import org.scalatest.{FunSpec, Matchers} 33 | 34 | class SpanAgentGrpcServiceSpec extends FunSpec with Matchers with EasyMockSugar { 35 | 36 | describe("Span Agent Grpc service") { 37 | it("should dispatch the span successfully") { 38 | val span = Span.newBuilder().setTraceId("traceid").build() 39 | val dispatcher = mock[Dispatcher] 40 | val responseObserver = mock[StreamObserver[DispatchResult]] 41 | val service = new SpanAgentGrpcService(Collections.singletonList(dispatcher), Collections.emptyList()) 42 | 43 | val dispatchResult = EasyMock.newCapture[DispatchResult]() 44 | val capturedSpan = EasyMock.newCapture[Array[Byte]]() 45 | val capturedSpanPartitionKey = EasyMock.newCapture[Array[Byte]]() 46 | 47 | expecting { 48 | dispatcher.dispatch(EasyMock.capture(capturedSpanPartitionKey), EasyMock.capture(capturedSpan)).once() 49 | responseObserver.onNext(EasyMock.capture(dispatchResult)).once() 50 | responseObserver.onCompleted().once() 51 | } 52 | whenExecuting(dispatcher, responseObserver) { 53 | service.dispatch(span, responseObserver) 54 | dispatchResult.getValue.getCode shouldBe ResultCode.SUCCESS 55 | dispatchResult.getValue.getErrorMessage shouldBe "" 56 | capturedSpanPartitionKey.getValue shouldBe span.getTraceId.getBytes("utf-8") 57 | capturedSpan.getValue shouldBe span.toByteArray 58 | } 59 | } 60 | 61 | it("should dispatch the span with error if dispatcher fails") { 62 | val span = Span.newBuilder().setTraceId("traceid").build() 63 | val dispatcher = mock[Dispatcher] 64 | val responseObserver = mock[StreamObserver[DispatchResult]] 65 | val service = new SpanAgentGrpcService(Collections.singletonList(dispatcher), Collections.emptyList()) 66 | 67 | val dispatchResult = EasyMock.newCapture[DispatchResult]() 68 | val capturedSpan = EasyMock.newCapture[Array[Byte]]() 69 | val capturedSpanPartitionKey = EasyMock.newCapture[Array[Byte]]() 70 | 71 | expecting { 72 | dispatcher.getName.andReturn("test-dispatcher").anyTimes() 73 | dispatcher.dispatch(EasyMock.capture(capturedSpanPartitionKey), EasyMock.capture(capturedSpan)).andThrow(new RuntimeException("Fail to dispatch")) 74 | responseObserver.onNext(EasyMock.capture(dispatchResult)).once() 75 | responseObserver.onCompleted().once() 76 | } 77 | 78 | whenExecuting(dispatcher, responseObserver) { 79 | service.dispatch(span, responseObserver) 80 | dispatchResult.getValue.getCode shouldBe ResultCode.UNKNOWN_ERROR 81 | dispatchResult.getValue.getErrorMessage shouldEqual "Fail to dispatch the span record to the dispatchers=test-dispatcher" 82 | capturedSpanPartitionKey.getValue shouldBe span.getTraceId.getBytes("utf-8") 83 | capturedSpan.getValue shouldBe span.toByteArray 84 | } 85 | } 86 | 87 | it("should dispatch the span with rate limit error if dispatcher throws RateLimitException") { 88 | val span = Span.newBuilder().setTraceId("traceid").build() 89 | val dispatcher = mock[Dispatcher] 90 | val responseObserver = mock[StreamObserver[DispatchResult]] 91 | val service = new SpanAgentGrpcService(Collections.singletonList(dispatcher), Collections.emptyList()) 92 | 93 | val dispatchResult = EasyMock.newCapture[DispatchResult]() 94 | val capturedSpan = EasyMock.newCapture[Array[Byte]]() 95 | val capturedSpanPartitionKey = EasyMock.newCapture[Array[Byte]]() 96 | 97 | expecting { 98 | dispatcher.getName.andReturn("test-dispatcher").anyTimes() 99 | dispatcher.dispatch(EasyMock.capture(capturedSpanPartitionKey), EasyMock.capture(capturedSpan)).andThrow(new RateLimitException("Rate Limit Error!")) 100 | responseObserver.onNext(EasyMock.capture(dispatchResult)).once() 101 | responseObserver.onCompleted().once() 102 | } 103 | 104 | whenExecuting(dispatcher, responseObserver) { 105 | service.dispatch(span, responseObserver) 106 | dispatchResult.getValue.getCode shouldBe ResultCode.RATE_LIMIT_ERROR 107 | dispatchResult.getValue.getErrorMessage shouldEqual "Fail to dispatch the span record to the dispatchers=test-dispatcher" 108 | capturedSpanPartitionKey.getValue shouldBe span.getTraceId.getBytes("utf-8") 109 | capturedSpan.getValue shouldBe span.toByteArray 110 | } 111 | } 112 | 113 | it("should fail in constructing grpc service object if no dispatchers exist") { 114 | val caught = intercept[Exception] { 115 | new SpanAgentGrpcService(Collections.emptyList(), Collections.emptyList()) 116 | } 117 | caught.getMessage shouldEqual "Dispatchers can't be empty" 118 | } 119 | 120 | it("should enrich the span before dispatch") { 121 | val span = Span.newBuilder().setTraceId("traceid").build() 122 | val dispatcher = mock[Dispatcher] 123 | val responseObserver = mock[StreamObserver[DispatchResult]] 124 | val service = new SpanAgentGrpcService(Collections.singletonList(dispatcher), util.Arrays.asList(new Enricher { 125 | override def apply(span: Span.Builder): Unit = { 126 | span.addTags(Tag.newBuilder().setKey("ip").setType(Tag.TagType.STRING).setVStr("10.1.10.1")) 127 | } 128 | }, new Enricher { 129 | override def apply(span: Span.Builder): Unit = { 130 | span.addTags(Tag.newBuilder().setKey("region").setType(Tag.TagType.STRING).setVStr("us-east-1")) 131 | } 132 | })) 133 | 134 | val dispatchResult = EasyMock.newCapture[DispatchResult]() 135 | val capturedSpan = EasyMock.newCapture[Array[Byte]]() 136 | val capturedSpanPartitionKey = EasyMock.newCapture[Array[Byte]]() 137 | 138 | expecting { 139 | dispatcher.getName.andReturn("test-dispatcher").anyTimes() 140 | dispatcher.dispatch(EasyMock.capture(capturedSpanPartitionKey), EasyMock.capture(capturedSpan)) 141 | responseObserver.onNext(EasyMock.capture(dispatchResult)).once() 142 | responseObserver.onCompleted().once() 143 | } 144 | 145 | whenExecuting(dispatcher, responseObserver) { 146 | service.dispatch(span, responseObserver) 147 | capturedSpanPartitionKey.getValue shouldBe span.getTraceId.getBytes("utf-8") 148 | val enrichedSpan = Span.parseFrom(capturedSpan.getValue) 149 | enrichedSpan.getTraceId shouldBe "traceid" 150 | enrichedSpan.getTags(0).getKey shouldBe "ip" 151 | enrichedSpan.getTags(0).getVStr shouldBe "10.1.10.1" 152 | enrichedSpan.getTags(1).getKey shouldBe "region" 153 | enrichedSpan.getTags(1).getVStr shouldBe "us-east-1" 154 | } 155 | } 156 | } 157 | } -------------------------------------------------------------------------------- /bundlers/haystack-agent/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | haystack-agent-core 6 | com.expedia.www 7 | 0.1.15-SNAPSHOT 8 | ../../pom.xml 9 | 10 | 11 | 4.0.0 12 | haystack-agent 13 | 14 | 15 | 16 | com.expedia.www 17 | haystack-agent-api 18 | ${project.version} 19 | 20 | 21 | com.expedia.www 22 | haystack-span-agent-provider 23 | ${project.version} 24 | 25 | 26 | com.expedia.www 27 | haystack-agent-file-config-provider 28 | ${project.version} 29 | 30 | 31 | com.expedia.www 32 | haystack-agent-kafka-dispatcher 33 | ${project.version} 34 | 35 | 36 | com.expedia.www 37 | haystack-agent-kinesis-dispatcher 38 | ${project.version} 39 | 40 | 41 | com.expedia.www 42 | haystack-agent-http-dispatcher 43 | ${project.version} 44 | 45 | 46 | com.expedia.www 47 | haystack-agent-logger-dispatcher 48 | ${project.version} 49 | 50 | 51 | com.expedia.www 52 | blobs-agent-dispatchers 53 | ${blobs.version} 54 | 55 | 56 | com.expedia.www 57 | blobs-agent-server 58 | ${blobs.version} 59 | 60 | 61 | 62 | io.grpc 63 | grpc-services 64 | ${grpc.version} 65 | 66 | 67 | 68 | 69 | com.expedia.www.haystack.agent.core.AgentLoader 70 | 71 | 72 | 73 | 74 | ${basedir}/src/main/resources 75 | true 76 | 77 | 78 | 79 | 80 | org.apache.maven.plugins 81 | maven-shade-plugin 82 | 83 | true 84 | 85 | 86 | *:* 87 | 88 | META-INF/*.SF 89 | META-INF/*.DSA 90 | META-INF/*.RSA 91 | 92 | 93 | 94 | 95 | 96 | 97 | package 98 | 99 | shade 100 | 101 | 102 | 103 | 104 | reference.conf 105 | 106 | 107 | ${mainClass} 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | org.scalatest 117 | scalatest-maven-plugin 118 | 1.0 119 | 120 | 121 | test 122 | 123 | test 124 | 125 | 126 | 127 | 128 | 129 | 130 | org.apache.maven.plugins 131 | maven-jar-plugin 132 | 3.0.2 133 | 134 | 135 | true 136 | 137 | true 138 | 139 | 140 | ${project.name} 141 | ${project.version} 142 | ${project.name} 143 | Expedia 144 | ${java.version} 145 | ${maven.build.timestamp} 146 | 147 | 148 | 149 | 150 | 151 | 152 | org.apache.maven.plugins 153 | maven-compiler-plugin 154 | 3.8.0 155 | 156 | ${project.jdk.version} 157 | ${project.jdk.version} 158 | 159 | 160 | 161 | 162 | org.jacoco 163 | jacoco-maven-plugin 164 | ${jacoco.version} 165 | 166 | 167 | default-prepare-agent 168 | 169 | prepare-agent 170 | 171 | 172 | 173 | default-report 174 | 175 | report 176 | 177 | 178 | 179 | default-check 180 | 181 | check 182 | 183 | 184 | 185 | **/com/expedia/open/tracing/**/* 186 | 187 | true 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2017 Expedia, Inc. 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------