├── 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 |
--------------------------------------------------------------------------------