├── .circleci ├── Dockerfile-openjdk7 └── config.yml ├── .github ├── CODEOWNERS └── workflows │ └── codeql-analysis.yml ├── .gitignore ├── .gitlab-ci.yml ├── CHANGELOG.md ├── DEPS.md ├── LICENSE ├── README.md ├── THIRDPARTY.md ├── gen_notice.py ├── pom.xml ├── settings.xml ├── src ├── main │ ├── java │ │ └── com │ │ │ └── timgroup │ │ │ └── statsd │ │ │ ├── AlphaNumericMessage.java │ │ │ ├── BufferPool.java │ │ │ ├── CgroupReader.java │ │ │ ├── ClientChannel.java │ │ │ ├── DatagramClientChannel.java │ │ │ ├── DirectStatsDClient.java │ │ │ ├── Event.java │ │ │ ├── InvalidMessageException.java │ │ │ ├── MapUtils.java │ │ │ ├── Message.java │ │ │ ├── NamedPipeClientChannel.java │ │ │ ├── NamedPipeSocketAddress.java │ │ │ ├── NoOpDirectStatsDClient.java │ │ │ ├── NoOpStatsDClient.java │ │ │ ├── NonBlockingDirectStatsDClient.java │ │ │ ├── NonBlockingStatsDClient.java │ │ │ ├── NonBlockingStatsDClientBuilder.java │ │ │ ├── NumericMessage.java │ │ │ ├── ServiceCheck.java │ │ │ ├── StatsDAggregator.java │ │ │ ├── StatsDBlockingProcessor.java │ │ │ ├── StatsDClient.java │ │ │ ├── StatsDClientErrorHandler.java │ │ │ ├── StatsDClientException.java │ │ │ ├── StatsDNonBlockingProcessor.java │ │ │ ├── StatsDProcessor.java │ │ │ ├── StatsDSender.java │ │ │ ├── StatsDThreadFactory.java │ │ │ ├── Telemetry.java │ │ │ ├── UnixDatagramClientChannel.java │ │ │ ├── UnixSocketAddressWithTransport.java │ │ │ ├── UnixStreamClientChannel.java │ │ │ └── Utf8.java │ └── resources │ │ ├── META-INF │ │ └── NOTICE │ │ └── dogstatsd │ │ └── version.properties └── test │ └── java │ └── com │ └── timgroup │ └── statsd │ ├── BuilderAddressTest.java │ ├── CgroupReaderTest.java │ ├── DummyLowMemStatsDServer.java │ ├── DummyStatsDServer.java │ ├── EventTest.java │ ├── NamedPipeDummyStatsDServer.java │ ├── NamedPipeTest.java │ ├── NonBlockingDirectStatsDClientTest.java │ ├── NonBlockingStatsDClientMaxPerfTest.java │ ├── NonBlockingStatsDClientPerfTest.java │ ├── NonBlockingStatsDClientTest.java │ ├── RecordingErrorHandler.java │ ├── StatsDAggregatorTest.java │ ├── StatsDTestMessage.java │ ├── TelemetryTest.java │ ├── TestHelpers.java │ ├── UDPDummyStatsDServer.java │ ├── UnixDatagramSocketDummyStatsDServer.java │ ├── UnixSocketTest.java │ ├── UnixStreamSocketDummyStatsDServer.java │ ├── UnixStreamSocketTest.java │ └── Utf8Test.java ├── style.xml └── vendor ├── buildlib ├── hamcrest-core-1.3.0RC2.jar ├── hamcrest-library-1.3.0RC2.jar └── junit-dep-4.10.jar └── src └── junit-dep-4.10-sources.jar /.circleci/Dockerfile-openjdk7: -------------------------------------------------------------------------------- 1 | FROM openjdk:7-jdk 2 | 3 | RUN apt update && apt install -y maven 4 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | orbs: 4 | win: circleci/windows@2.4.1 5 | 6 | commands: 7 | create_custom_cache_lock: 8 | description: "Create custom cache lock for java version." 9 | parameters: 10 | filename: 11 | type: string 12 | steps: 13 | - run: 14 | name: Grab java version and dump to file 15 | command: java -version > << parameters.filename >> 16 | 17 | default_steps: &default_steps 18 | steps: 19 | - checkout 20 | 21 | - run: | 22 | mvn clean install $MVN_EXTRA_OPTS 23 | 24 | jobs: 25 | openjdk7: 26 | docker: 27 | - image: jfullaondo/openjdk:7 28 | environment: 29 | MVN_EXTRA_OPTS: -Dcheckstyle.version=2.15 -Dcheckstyle.skip 30 | <<: *default_steps 31 | openjdk8: 32 | docker: &jdk8 33 | - image: cimg/openjdk:8.0 34 | <<: *default_steps 35 | openjdk11: 36 | docker: 37 | - image: cimg/openjdk:11.0 38 | <<: *default_steps 39 | openjdk13: 40 | docker: 41 | - image: cimg/openjdk:13.0 42 | <<: *default_steps 43 | openjdk17: 44 | docker: 45 | - image: cimg/openjdk:17.0 46 | <<: *default_steps 47 | 48 | ## Fails with "Source option 7 is no longer supported. Use 8 or later." 49 | # openjdk21: 50 | # docker: 51 | # - image: cimg/openjdk:21.0 52 | # <<: *default_steps 53 | 54 | windows-openjdk12: 55 | executor: 56 | # https://github.com/CircleCI-Public/windows-orb/blob/v2.4.1/src/executors/default.yml 57 | name: win/default 58 | # https://circleci.com/developer/machine/image/windows-server-2019 59 | version: 2023.04.1 60 | steps: 61 | - checkout 62 | - run: java -version 63 | - run: | 64 | choco install maven 65 | - run: | 66 | mvn clean install 67 | 68 | openjdk8-jnr-exclude: 69 | docker: *jdk8 70 | steps: 71 | - checkout 72 | - run: "mvn -Pjnr-exclude clean test" 73 | openjdk8-jnr-latest: 74 | docker: *jdk8 75 | steps: 76 | - checkout 77 | - run: "mvn clean test -DskipTests" # build main and test with default deps 78 | - run: "mvn -Pjnr-latest test" # run with modified deps 79 | 80 | workflows: 81 | version: 2 82 | agent-tests: 83 | jobs: 84 | - openjdk7 85 | - openjdk8 86 | - openjdk11 87 | - openjdk13 88 | - openjdk17 89 | # - openjdk21 90 | - windows-openjdk12 91 | - openjdk8-jnr-exclude 92 | - openjdk8-jnr-latest 93 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/about-codeowners/ for syntax 2 | # Rules are matched bottom-to-top, so one team can own subdirectories 3 | # and another team can own the rest of the directory. 4 | 5 | * @DataDog/agent-metric-pipelines 6 | *.md @DataDog/documentation 7 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: "CodeQL" 3 | 4 | on: 5 | push: 6 | branches: [ master ] 7 | pull_request: 8 | # The branches below must be a subset of the branches above 9 | branches: [ master ] 10 | 11 | jobs: 12 | analyze: 13 | name: Analyze 14 | runs-on: ubuntu-latest 15 | permissions: 16 | actions: read 17 | contents: read 18 | security-events: write 19 | 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | language: [ 'java', 'python' ] 24 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 25 | # Learn more about CodeQL language support at https://git.io/codeql-language-support 26 | 27 | steps: 28 | - name: Checkout repository 29 | uses: actions/checkout@v3 30 | 31 | # Initializes the CodeQL tools for scanning. 32 | - name: Initialize CodeQL 33 | uses: github/codeql-action/init@v2 34 | with: 35 | languages: ${{ matrix.language }} 36 | # If you wish to specify custom queries, you can do so here or in a config file. 37 | # By default, queries listed here will override any specified in a config file. 38 | # Prefix the list here with "+" to use these queries and those in the config file. 39 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 40 | 41 | - name: Autobuild 42 | uses: github/codeql-action/autobuild@v2 43 | 44 | - name: Perform CodeQL Analysis 45 | uses: github/codeql-action/analyze@v2 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | build 3 | *.iml 4 | /target/ 5 | .idea 6 | .classpath 7 | .project 8 | /.settings/ 9 | *.swp 10 | *.swo 11 | 12 | # jenv 13 | .java-version 14 | 15 | # rbenv for pimpmychangelog 16 | .ruby-version 17 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | stages: 2 | - deploy_to_sonatype 3 | - create_key 4 | 5 | variables: 6 | REGISTRY: registry.ddbuild.io 7 | 8 | # From the tagged repo, push the release artifact 9 | deploy_to_sonatype: 10 | stage: deploy_to_sonatype 11 | 12 | rules: 13 | # All releases are manual 14 | - when: manual 15 | allow_failure: true 16 | 17 | tags: 18 | - "runner:docker" 19 | 20 | image: maven:3.6.3-jdk-8-slim 21 | 22 | script: 23 | # Ensure we don't print commands being run to the logs during credential 24 | # operations 25 | - set +x 26 | 27 | - echo "Installing AWSCLI..." 28 | - apt update 29 | - apt install -y python3 python3-pip 30 | - python3 -m pip install awscli 31 | 32 | - echo "Fetching Sonatype user..." 33 | - export SONATYPE_USER=$(aws ssm get-parameter --region us-east-1 --name ci.java-dogstatsd-client.publishing.sonatype_username --with-decryption --query "Parameter.Value" --out text) 34 | - echo "Fetching Sonatype password..." 35 | - export SONATYPE_PASS=$(aws ssm get-parameter --region us-east-1 --name ci.java-dogstatsd-client.publishing.sonatype_password --with-decryption --query "Parameter.Value" --out text) 36 | 37 | - echo "Fetching signing key password..." 38 | - export GPG_PASSPHRASE=$(aws ssm get-parameter --region us-east-1 --name ci.java-dogstatsd-client.signing.gpg_passphrase --with-decryption --query "Parameter.Value" --out text) 39 | 40 | - echo "Fetching signing key..." 41 | - gpg_key=$(aws ssm get-parameter --region us-east-1 --name ci.java-dogstatsd-client.signing.gpg_private_key --with-decryption --query "Parameter.Value" --out text) 42 | - printf -- "$gpg_key" | gpg --import --batch 43 | 44 | - set -x 45 | 46 | - echo "Building release..." 47 | - mvn -DperformRelease=true --settings ./settings.xml clean deploy 48 | 49 | artifacts: 50 | expire_in: 6 mos 51 | paths: 52 | - ./target/*.jar 53 | - ./target/*.pom 54 | - ./target/*.asc 55 | - ./target/*.md5 56 | - ./target/*.sha1 57 | - ./target/*.sha256 58 | - ./target/*.sha512 59 | 60 | # This job creates the GPG key used to sign the releases 61 | create_key: 62 | stage: create_key 63 | when: manual 64 | 65 | tags: 66 | - "runner:docker" 67 | 68 | image: $REGISTRY/ci/agent-key-management-tools/gpg:1 69 | 70 | variables: 71 | PROJECT_NAME: "java-dogstatsd-client" 72 | EXPORT_TO_KEYSERVER: "false" 73 | 74 | script: 75 | - /create.sh 76 | 77 | artifacts: 78 | expire_in: 13 mos 79 | paths: 80 | - ./pubkeys/ 81 | -------------------------------------------------------------------------------- /DEPS.md: -------------------------------------------------------------------------------- 1 | # Overriding dependencies 2 | 3 | ## Java 8+ 4 | 5 | As of version v4.1.0, this library can be used with the latest version 6 | of the `jnr-unixsocket` library. 7 | 8 | ### Maven 9 | 10 | ```xml 11 | 12 | 13 | com.datadoghq 14 | java-dogstatsd-client 15 | 4.1.0 16 | 17 | 18 | com.github.jnr 19 | jnr-unixsocket 20 | 0.38.17 21 | 22 | 23 | ``` 24 | 25 | ### Gradle 26 | 27 | ```groovy 28 | dependencies { 29 | implementation('com.datadoghq:java-dogstatsd-client:4.1.0') 30 | implementation('com.github.jnr:jnr-unixsocket:0.38.17') 31 | } 32 | ``` 33 | 34 | ## Without dependencies 35 | 36 | As of version v4.1.0, this library can be used without dependencies 37 | when unix sockets support is not required. Trying to instantiate a 38 | client with port set to zero to enable unix socket mode will cause a 39 | `ClassNotFound` exception. 40 | 41 | ### Maven 42 | 43 | ```xml 44 | 45 | 46 | com.datadoghq 47 | java-dogstatsd-client 48 | 4.1.0 49 | 50 | 51 | jnr-unixsocket 52 | com.github.jnr 53 | 54 | 55 | 56 | 57 | ``` 58 | 59 | ### Gradle 60 | 61 | ```groovy 62 | dependencies { 63 | implementation('com.datadoghq:java-dogstatsd-client:4.1.0') { 64 | exclude group: 'com.github.jnr', module: 'jnr-unixsocket' 65 | } 66 | } 67 | ``` 68 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2012 youDevise, Ltd. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Java DogStatsD Client 2 | 3 | [![CircleCI Build Status](https://circleci.com/gh/DataDog/java-dogstatsd-client/tree/master.svg?style=svg)](https://circleci.com/gh/DataDog/java-dogstatsd-client) 4 | 5 | A DogStatsD client library implemented in Java. Allows for Java applications to easily communicate with the DataDog Agent. The library supports Java 1.7+. 6 | 7 | This version was originally forked from [java-dogstatsd-client](https://github.com/indeedeng/java-dogstatsd-client) and [java-statsd-client](https://github.com/youdevise/java-statsd-client) but it is now the canonical home for the `java-dogstatsd-client`. Collaborating with the former upstream projects we have now combined efforts to provide a single release. 8 | 9 | See [CHANGELOG.md](CHANGELOG.md) for changes. 10 | 11 | ## Installation 12 | 13 | The client jar is distributed via Maven central, and can be downloaded [from Maven](http://search.maven.org/#search%7Cga%7C1%7Cg%3Acom.datadoghq%20a%3Ajava-dogstatsd-client). 14 | 15 | ```xml 16 | 17 | com.datadoghq 18 | java-dogstatsd-client 19 | 4.4.4 20 | 21 | ``` 22 | 23 | ### Unix Domain Socket support 24 | 25 | As an alternative to UDP, Agent v6 can receive metrics via a UNIX Socket (on Linux only). This library supports transmission via this protocol. To use it 26 | use the `address()` method of the builder and pass the path to the socket with the `unix://` prefix: 27 | 28 | ```java 29 | StatsDClient client = new NonBlockingStatsDClientBuilder() 30 | .address("unix:///var/run/datadog/dsd.socket") 31 | .build(); 32 | ``` 33 | 34 | By default, all exceptions are ignored, mimicking UDP behaviour. When using Unix Sockets, transmission errors trigger exceptions you can choose to handle by passing a `StatsDClientErrorHandler`: 35 | 36 | - Connection error because of an invalid/missing socket triggers a `java.io.IOException: No such file or directory`. 37 | - If DogStatsD's reception buffer were to fill up and the non blocking client is used, the send times out after 100ms and throw either a `java.io.IOException: No buffer space available` or a `java.io.IOException: Resource temporarily unavailable`. 38 | 39 | The default UDS transport is using `SOCK_DATAGRAM` sockets. We also have experimental support for `SOCK_STREAM` sockets which can 40 | be enabled by using the `unixstream://` instead of `unix://`. This is not recommended for production use at this time. 41 | 42 | ## Configuration 43 | 44 | Once your DogStatsD client is installed, instantiate it in your code: 45 | 46 | ```java 47 | import com.timgroup.statsd.NonBlockingStatsDClientBuilder; 48 | import com.timgroup.statsd.NonBlockingStatsDClient; 49 | import com.timgroup.statsd.StatsDClient; 50 | 51 | public class DogStatsdClient { 52 | 53 | public static void main(String[] args) throws Exception { 54 | 55 | StatsDClient client = new NonBlockingStatsDClientBuilder() 56 | .prefix("statsd") 57 | .hostname("localhost") 58 | .port(8125) 59 | .build(); 60 | 61 | // use your client... 62 | } 63 | } 64 | ``` 65 | 66 | ### v2.x 67 | 68 | Client version `v3.x` is now preferred over the older client `v2.x` release line. We do suggest you upgrade to the newer `v3.x` at 69 | your earliest convenience. 70 | 71 | The builder pattern described above was introduced with `v2.10.0` in the `v2.x` series. Earlier releases require you use the 72 | deprecated overloaded constructors. 73 | 74 | 75 | **DEPRECATED** 76 | ```java 77 | import com.timgroup.statsd.NonBlockingStatsDClient; 78 | import com.timgroup.statsd.StatsDClient; 79 | 80 | public class DogStatsdClient { 81 | 82 | public static void main(String[] args) throws Exception { 83 | 84 | StatsDClient Statsd = new NonBlockingStatsDClient("statsd", "localhost", 8125); 85 | 86 | } 87 | } 88 | ``` 89 | 90 | See the full list of available [DogStatsD Client instantiation parameters](https://docs.datadoghq.com/developers/dogstatsd/?tab=hostagent#client-instantiation-parameters). 91 | 92 | ### Migrating from 2.x to 3.x 93 | 94 | Though there are very few breaking changes in `3.x`, some code changes might be required for some user to migrate to the latest version. If you are migrating from the `v2.x` series to `v3.x` and were using the deprecated constructors, please use the following table to ease your migration to utilizing the builder pattern. 95 | 96 | | v2.x constructor parameter | v2.10.0+ builder method | 97 | |----------------------------|-------------------------| 98 | | final Callable addressLookup | NonBlockingStatsDClientBuilder addressLookup(Callable val) | 99 | | final boolean blocking | NonBlockingStatsDClientBuilder blocking(boolean val) | 100 | | final int bufferSize | NonBlockingStatsDClientBuilder socketBufferSize(int val) | 101 | | final String... constantTags | NonBlockingStatsDClientBuilder constantTags(String... val) | 102 | | final boolean enableTelemetry | NonBlockingStatsDClientBuilder enableTelemetry(boolean val) | 103 | | final String entityID | NonBlockingStatsDClientBuilder entityID(String val) | 104 | | final StatsDClientErrorHandler errorHandler | NonBlockingStatsDClientBuilder errorHandler(StatsDClientErrorHandler val) | 105 | | final String hostname | NonBlockingStatsDClientBuilder hostname(String val) | 106 | | final int maxPacketSizeBytes | NonBlockingStatsDClientBuilder maxPacketSizeBytes(String... val) | 107 | | final int processorWorkers | NonBlockingStatsDClientBuilder processorWorkers(int val) | 108 | | final int poolSize | NonBlockingStatsDClientBuilder bufferPoolSize(int val) | 109 | | final int port | NonBlockingStatsDClientBuilder port(int val) | 110 | | final String prefix | NonBlockingStatsDClientBuilder prefix(String val) | 111 | | final int queueSize | NonBlockingStatsDClientBuilder queueSize(int val) | 112 | | final int senderWorkers | NonBlockingStatsDClientBuilder senderWorkers(int val) | 113 | | final Callable telemetryAddressLookup | NonBlockingStatsDClientBuilder telemetryAddressLookup(Callable val) | 114 | | final int telemetryFlushInterval | NonBlockingStatsDClientBuilder telemetryFlushInterval(int val) | 115 | | final int timeout | NonBlockingStatsDClientBuilder timeout(int val) | 116 | 117 | ### Transport and Maximum Packet Size 118 | 119 | As mentioned above the client currently supports two forms of transport: UDP and Unix Domain Sockets (UDS). 120 | 121 | The preferred setup for local transport is UDS, while remote setups will require the use of UDP. For both setups we have tried to set convenient maximum default packet sizes that should help with performance by packing multiple statsd metrics into each network packet all while playing nicely with the respective environments. For this reason we have set the following defaults for the max packet size: 122 | - UDS: 8192 bytes - recommended default. 123 | - UDP: 1432 bytes - largest possible size given the Ethernet MTU of 1514 Bytes. This should help avoid UDP fragmentation. 124 | 125 | These are both configurable should you have other needs: 126 | ```java 127 | StatsDClient client = new NonBlockingStatsDClientBuilder() 128 | .hostname("/var/run/datadog/dsd.socket") 129 | .port(0) // Necessary for unix socket 130 | .maxPacketSizeBytes(16384) // 16kB maximum custom value 131 | .build(); 132 | ``` 133 | 134 | #### Origin detection over UDP and UDS 135 | 136 | Origin detection is a method to detect which pod `DogStatsD` packets are coming from in order to add the pod's tags to the tag list. 137 | The `DogStatsD` client attaches an internal tag, `entity_id`. The value of this tag is the content of the `DD_ENTITY_ID` environment variable if found, which is the pod's UID. The Datadog Agent uses this tag to add container tags to the metrics. To avoid overwriting this global tag, make sure to only `append` to the `constant_tags` list. 138 | 139 | To enable origin detection over UDP, add the following lines to your application manifest 140 | ```yaml 141 | env: 142 | - name: DD_ENTITY_ID 143 | valueFrom: 144 | fieldRef: 145 | fieldPath: metadata.uid 146 | ``` 147 | 148 | ## Aggregation 149 | 150 | As of version `v2.11.0`, client-side aggregation has been introduced in the java client side for basic types (gauges, counts, sets). Aggregation remains unavailable at the 151 | time of this writing for histograms, distributions, service checks and events due to message relevance and statistical significance of these types. The feature is enabled by default as of `v3.0.0`, and remains available but disabled by default for prior versions. 152 | 153 | The goal of this feature is to reduce the number of messages submitted to the Datadog Agent. Minimizing message volume allows us to reduce load on the dogstatsd server side 154 | and mitigate packet drops. The feature has been implemented such that impact on CPU and memory should be quite minimal on the client side. Users might be concerned with 155 | what could be perceived as a loss of resolution by resorting to aggregation on the client, this should not be the case. It's worth noting the dogstatsd server implemented 156 | in the Datadog Agent already aggregates messages over a certain flush period, therefore so long as the flush interval configured on the client side is smaller 157 | than said flush interval on the server side there should no loss in resolution. 158 | 159 | ### Configuration 160 | 161 | Enabling aggregation is simple, you just need to set the appropriate options with the client builder. 162 | 163 | You can just enable aggregation by calling the `enableAggregation(bool)` method on the builder. 164 | 165 | There are two clent-side aggregation knobs available: 166 | - `aggregationShards(int)`: determines the number of shards in the aggregator, this 167 | feature is aimed at mitigating the effects of map locking in highly concurrent scenarios. Defaults to 4. 168 | - `aggregationFlushInterval(int)`: sets the period of time in milliseconds in which the 169 | aggregator will flush its metrics into the sender. Defaults to 3000 milliseconds. 170 | 171 | ```java 172 | StatsDClient client = new NonBlockingStatsDClientBuilder() 173 | .hostname("localhost") 174 | .port(8125) 175 | .enableAggregation(true) 176 | .aggregationFlushInterval(3000) // optional: in milliseconds 177 | .aggregationShards(8) // optional: defaults to 4 178 | .build(); 179 | ``` 180 | 181 | ## Usage 182 | 183 | In order to use DogStatsD metrics, events, and Service Checks the Agent must be [running and available](https://docs.datadoghq.com/developers/dogstatsd/). 184 | 185 | ### Metrics 186 | 187 | After the client is created, you can start sending custom metrics to Datadog. See the dedicated [Metric Submission: DogStatsD documentation](https://docs.datadoghq.com/metrics/dogstatsd_metrics_submission/) to see how to submit all supported metric types to Datadog with working code examples: 188 | 189 | * [Submit a COUNT metric](https://docs.datadoghq.com/metrics/dogstatsd_metrics_submission/#count). 190 | * [Submit a GAUGE metric](https://docs.datadoghq.com/metrics/dogstatsd_metrics_submission/#gauge). 191 | * [Submit a HISTOGRAM metric](https://docs.datadoghq.com/metrics/dogstatsd_metrics_submission/#histogram) 192 | * [Submit a DISTRIBUTION metric](https://docs.datadoghq.com/metrics/dogstatsd_metrics_submission/#distribution) 193 | 194 | Some options are suppported when submitting metrics, like [applying a Sample Rate to your metrics](https://docs.datadoghq.com/metrics/dogstatsd_metrics_submission/#metric-submission-options) or [tagging your metrics with your custom tags](https://docs.datadoghq.com/metrics/dogstatsd_metrics_submission/#metric-tagging). 195 | 196 | ### Events 197 | 198 | After the client is created, you can start sending events to your Datadog Event Stream. See the dedicated [Event Submission: DogStatsD documentation](https://docs.datadoghq.com/developers/events/dogstatsd/) to see how to submit an event to your Datadog Event Stream. 199 | 200 | ### Service Checks 201 | 202 | After the client is created, you can start sending Service Checks to Datadog. See the dedicated [Service Check Submission: DogStatsD documentation](https://docs.datadoghq.com/developers/service_checks/dogstatsd_service_checks_submission/) to see how to submit a Service Check to Datadog. 203 | -------------------------------------------------------------------------------- /THIRDPARTY.md: -------------------------------------------------------------------------------- 1 | This file lists third-party software included in 2 | 'jar-with-dependencies' binary distribution. Where multiple licenses 3 | are available, we choose the most permissive license to apply to the 4 | corresponding part of the binary distribution. 5 | 6 | For full license text and copyrights (where applicable) please refer 7 | to the [NOTICE](src/main/resources/META-INF/NOTICE) file. 8 | 9 | | Group | Artifact | Version | Developers | License | License URL | 10 | |-|-|-|-|-|-| 11 | | com.github.jnr | jffi | 1.2.23 | Wayne Meissner | The Apache Software License, Version 2.0 | http://www.apache.org/licenses/LICENSE-2.0.txt | 12 | | com.github.jnr | jnr-a64asm | 1.0.0 | ossdev | The Apache Software License, Version 2.0 | http://www.apache.org/licenses/LICENSE-2.0.txt | 13 | | com.github.jnr | jnr-constants | 0.9.17 | Wayne Meissner, Charles Oliver Nutter | The Apache Software License, Version 2.0 | http://www.apache.org/licenses/LICENSE-2.0.txt | 14 | | com.github.jnr | jnr-enxio | 0.30 | Wayne Meissner | The Apache Software License, Version 2.0 | http://www.apache.org/licenses/LICENSE-2.0.txt | 15 | | com.github.jnr | jnr-ffi | 2.1.16 | Wayne Meissner, Charles Oliver Nutter | The Apache Software License, Version 2.0 | http://www.apache.org/licenses/LICENSE-2.0.txt | 16 | | com.github.jnr | jnr-posix | 3.0.61 | Thomas E Enebo, Wayne Meissner, Charles Oliver Nutter | Eclipse Public License - v 2.0 | https://www.eclipse.org/legal/epl-2.0/ | 17 | | com.github.jnr | jnr-unixsocket | 0.36 | Wayne Meissner, Fritz Elfert | The Apache Software License, Version 2.0 | http://www.apache.org/licenses/LICENSE-2.0.txt | 18 | | com.github.jnr | jnr-x86asm | 1.0.2 | Wayne Meissner | MIT License | http://www.opensource.org/licenses/mit-license.php | 19 | | org.ow2.asm | asm | 7.1 | Eric Bruneton, Eugene Kuleshov, Remi Forax | BSD | http://asm.ow2.org/license.html | 20 | | org.ow2.asm | asm-analysis | 7.1 | Eric Bruneton, Eugene Kuleshov, Remi Forax | BSD | http://asm.ow2.org/license.html | 21 | | org.ow2.asm | asm-commons | 7.1 | Eric Bruneton, Eugene Kuleshov, Remi Forax | BSD | http://asm.ow2.org/license.html | 22 | | org.ow2.asm | asm-tree | 7.1 | Eric Bruneton, Eugene Kuleshov, Remi Forax | BSD | http://asm.ow2.org/license.html | 23 | | org.ow2.asm | asm-util | 7.1 | Eric Bruneton, Eugene Kuleshov, Remi Forax | BSD | http://asm.ow2.org/license.html | 24 | -------------------------------------------------------------------------------- /gen_notice.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | Generate third-party license information. 5 | - NOTICE file for the binary distribution 6 | - THIRDPARTY.md to be displayed on github 7 | 8 | When using this script to update license information, verify that the 9 | sources included (this script unpacks them into target/dependency) 10 | matches the license. 11 | """ 12 | 13 | import functools 14 | import os, os.path, re 15 | import xml.etree.ElementTree as ElementTree 16 | import urllib.request 17 | from collections import namedtuple 18 | 19 | Dep = namedtuple('Dep', ['groupId', 'artifactId', 'version']) 20 | def list_dependencies(): 21 | pattern = re.compile(".* (?P[^:]*):(?P[^:]*):jar:(?P[^:]*):compile") 22 | with os.popen("mvn dependency:list") as l: 23 | for line in l: 24 | m = pattern.match(line) 25 | if not m: 26 | continue 27 | yield Dep(**{k: m[k] for k in ['groupId', 'artifactId', 'version']}) 28 | 29 | def unpack_dependencies(): 30 | if not os.path.isdir("target/dependency"): 31 | os.system("mvn dependency:copy-dependencies -Dmdep.copyPom -Dmdep.useRepositoryLayout") 32 | os.system("mvn dependency:unpack-dependencies -DincludeScope=compile -Dmdep.useRepositoryLayout") 33 | os.system("mvn dependency:unpack-dependencies -Dclassifier=sources -DincludeScope=compile -Dmdep.useRepositoryLayout") 34 | 35 | def dep_path(dep): 36 | return os.path.join( 37 | 'target', 'dependency', 38 | *dep.groupId.split('.'), 39 | dep.artifactId, 40 | dep.version) 41 | 42 | def scan_unpacked(dep, ignored={'module-info.class'}): 43 | classdirs = set() 44 | prefix = dep_path(dep) 45 | for dirpath, _, filenames in os.walk(prefix): 46 | for f in filenames: 47 | if f.endswith('.class') and f not in ignored: 48 | if dirpath.removeprefix(prefix).removeprefix('/') == '': 49 | raise ValueError(dirpath, prefix) 50 | classdirs.add(dirpath.removeprefix(prefix).removeprefix('/')) 51 | 52 | return unify_paths(classdirs) 53 | 54 | def unify_paths(paths): 55 | res = set() 56 | for p in sorted(paths): 57 | if not any(p.startswith(r) for r in res): 58 | res.add(p) 59 | return res 60 | 61 | Pom = namedtuple('Pom', ['authors', 'license']) 62 | PomLicense = namedtuple('PomLicense', ['name', 'url']) 63 | def read_pom(dep, ns={'pom': 'http://maven.apache.org/POM/4.0.0'}): 64 | path = os.path.join(dep_path(dep), f"{dep.artifactId}-{dep.version}.pom") 65 | pom = ElementTree.parse(path) 66 | auth = pom.findall('.//pom:developers/pom:developer/pom:name', ns) 67 | auth = ', '.join(a.text for a in auth) 68 | lic = pom.find('.//pom:licenses/pom:license', ns) 69 | return Pom(auth, PomLicense( 70 | lic.findtext('./pom:name', None, ns), 71 | lic.findtext('./pom:url', None, ns), 72 | )) 73 | 74 | LICENSES = { 75 | # Links to the project front-page. 76 | 'org.ow2.asm': 'https://raw.githubusercontent.com/llbit/ow2-asm/master/LICENSE.txt', 77 | # These two link to generic license text without copyrights. 78 | 'com.github.jnr/jnr-posix': 'https://raw.githubusercontent.com/jnr/jnr-posix/jnr-posix-{version}/LICENSE.txt', 79 | 'com.github.jnr/jnr-x86asm': 'https://raw.githubusercontent.com/jnr/jnr-x86asm/{version}/LICENSE', 80 | } 81 | 82 | @functools.cache 83 | def fetch(url): 84 | print(f'... fetching {url}') 85 | with urllib.request.urlopen(url) as f: 86 | return clean(f.read().decode('ascii')) 87 | 88 | def clean(s, remove=re.compile('[^\n\t !-~]', re.A)): 89 | return remove.sub('', s) 90 | 91 | def write_notice(notice, dep, classdirs, license_text): 92 | """ 93 | NOTICE file to be included with the binary distribution and 94 | fullfill third-party license requirements. 95 | """ 96 | header = f'{dep.groupId} {dep.artifactId} {dep.version}' 97 | print(header, file=notice) 98 | print('-' * len(header), file=notice) 99 | 100 | repo_path = '/'.join([*dep.groupId.split('.'), dep.artifactId, dep.version]) 101 | print(f'Sources are available at https://repo.maven.apache.org/maven2/{repo_path}/\n', file=notice) 102 | 103 | print('Files in the following directories:', ' '.join(sorted(classdirs)), file=notice) 104 | print('are distributed under the terms of the following license:\n', file=notice) 105 | 106 | print(license_text, file=notice) 107 | print('\n', file=notice) 108 | 109 | LISTING_HEADER = """\ 110 | This file lists third-party software included in 111 | 'jar-with-dependencies' binary distribution. Where multiple licenses 112 | are available, we choose the most permissive license to apply to the 113 | corresponding part of the binary distribution. 114 | 115 | For full license text and copyrights (where applicable) please refer 116 | to the [NOTICE](src/main/resources/META-INF/NOTICE) file. 117 | 118 | | Group | Artifact | Version | Developers | License | License URL | 119 | |-|-|-|-|-|-| 120 | """ 121 | 122 | def write_listing(listing, dep, pom): 123 | """ 124 | THIRDPARTY.md provides an overview of third-party licenses that 125 | will apply to the binary distribution. 126 | """ 127 | print(f'| {dep.groupId} | {dep.artifactId} | {dep.version} ', end='', file=listing) 128 | print(f'| {pom.authors} ', end='', file=listing) 129 | print(f'| {pom.license.name} | {pom.license.url} ', end='', file=listing) 130 | print(f'|', file=listing) 131 | 132 | def gen_all(notice, listing): 133 | deps = list_dependencies() 134 | unpack_dependencies() 135 | 136 | print(LISTING_HEADER, end='', file=listing) 137 | 138 | for dep in sorted(deps): 139 | classdirs = scan_unpacked(dep) 140 | pom = read_pom(dep) 141 | 142 | license_url = LICENSES.get(f'{dep.groupId}/{dep.artifactId}') or LICENSES.get(dep.groupId) 143 | if license_url: 144 | license_url = license_url.format(**dep._asdict()) 145 | else: 146 | license_url = pom.license.url 147 | if not license_url: 148 | print(f'License is missing for {dep}') 149 | break 150 | 151 | license_text = fetch(license_url) 152 | 153 | write_notice(notice, dep, classdirs, license_text) 154 | write_listing(listing, dep, pom) 155 | 156 | if __name__ == '__main__': 157 | with open('src/main/resources/META-INF/NOTICE', 'w') as notice: 158 | with open('THIRDPARTY.md', 'w') as listing: 159 | gen_all(notice, listing) 160 | -------------------------------------------------------------------------------- /settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | nexus 8 | ${env.SONATYPE_USER} 9 | ${env.SONATYPE_PASS} 10 | 11 | 12 | github 13 | datadog 14 | ${env.GITHUB_TOKEN} 15 | 16 | 17 | gpg.passphrase 18 | ${env.GPG_PASSPHRASE} 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/main/java/com/timgroup/statsd/AlphaNumericMessage.java: -------------------------------------------------------------------------------- 1 | package com.timgroup.statsd; 2 | 3 | public abstract class AlphaNumericMessage extends Message { 4 | 5 | protected final String value; 6 | 7 | protected AlphaNumericMessage(Message.Type type, String value) { 8 | super(type); 9 | this.value = value; 10 | } 11 | 12 | protected AlphaNumericMessage(String aspect, Message.Type type, String value, String[] tags) { 13 | super(aspect, type, tags); 14 | this.value = value; 15 | } 16 | 17 | /** 18 | * Aggregate message. 19 | * 20 | * @param message 21 | * Message to aggregate. 22 | */ 23 | @Override 24 | public void aggregate(Message message) { } 25 | 26 | /** 27 | * Get underlying message value. 28 | * 29 | * @return returns the value for the Message 30 | */ 31 | public String getValue() { 32 | return this.value; 33 | } 34 | 35 | @Override 36 | public int hashCode() { 37 | return super.hashCode() * HASH_MULTIPLIER + this.value.hashCode(); 38 | } 39 | 40 | @Override 41 | public boolean equals(Object object) { 42 | boolean equal = super.equals(object); 43 | if (!equal) { 44 | return false; 45 | } 46 | 47 | if (object instanceof AlphaNumericMessage ) { 48 | AlphaNumericMessage msg = (AlphaNumericMessage)object; 49 | return this.value.equals(msg.getValue()); 50 | } 51 | 52 | return false; 53 | } 54 | 55 | @Override 56 | public int compareTo(Message message) { 57 | int comparison = super.compareTo(message); 58 | if (comparison == 0 && message instanceof AlphaNumericMessage) { 59 | return value.compareTo(((AlphaNumericMessage) message).getValue()); 60 | } 61 | return comparison; 62 | } 63 | } 64 | 65 | -------------------------------------------------------------------------------- /src/main/java/com/timgroup/statsd/BufferPool.java: -------------------------------------------------------------------------------- 1 | package com.timgroup.statsd; 2 | 3 | import java.nio.ByteBuffer; 4 | 5 | import java.util.concurrent.ArrayBlockingQueue; 6 | import java.util.concurrent.BlockingQueue; 7 | 8 | public class BufferPool { 9 | private final BlockingQueue pool; 10 | private final int size; 11 | private final int bufferSize; 12 | private final boolean direct; 13 | 14 | 15 | BufferPool(final int poolSize, int bufferSize, final boolean direct) throws InterruptedException { 16 | 17 | size = poolSize; 18 | this.bufferSize = bufferSize; 19 | this.direct = direct; 20 | 21 | pool = new ArrayBlockingQueue(poolSize); 22 | for (int i = 0; i < size ; i++) { 23 | if (direct) { 24 | pool.put(ByteBuffer.allocateDirect(bufferSize)); 25 | } else { 26 | pool.put(ByteBuffer.allocate(bufferSize)); 27 | } 28 | } 29 | } 30 | 31 | BufferPool(final BufferPool pool) throws InterruptedException { 32 | this.size = pool.size; 33 | this.bufferSize = pool.bufferSize; 34 | this.direct = pool.direct; 35 | this.pool = new ArrayBlockingQueue(pool.size); 36 | for (int i = 0; i < size ; i++) { 37 | if (direct) { 38 | this.pool.put(ByteBuffer.allocateDirect(bufferSize)); 39 | } else { 40 | this.pool.put(ByteBuffer.allocate(bufferSize)); 41 | } 42 | } 43 | } 44 | 45 | ByteBuffer borrow() throws InterruptedException { 46 | return pool.take(); 47 | } 48 | 49 | void put(ByteBuffer buffer) throws InterruptedException { 50 | pool.put(buffer); 51 | } 52 | 53 | int getSize() { 54 | return size; 55 | } 56 | 57 | int getBufferSize() { 58 | return bufferSize; 59 | } 60 | 61 | int available() { 62 | return pool.size(); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/main/java/com/timgroup/statsd/CgroupReader.java: -------------------------------------------------------------------------------- 1 | package com.timgroup.statsd; 2 | 3 | import java.io.BufferedReader; 4 | import java.io.IOException; 5 | import java.io.StringReader; 6 | import java.nio.file.Files; 7 | import java.nio.file.Path; 8 | import java.nio.file.Paths; 9 | import java.util.Arrays; 10 | import java.util.HashMap; 11 | import java.util.List; 12 | import java.util.Map; 13 | import java.util.regex.Matcher; 14 | import java.util.regex.Pattern; 15 | 16 | /** 17 | * A reader class that retrieves the current container ID or the cgroup controller 18 | * inode parsed from the cgroup file. 19 | * 20 | */ 21 | class CgroupReader { 22 | private static final Path CGROUP_PATH = Paths.get("/proc/self/cgroup"); 23 | /** 24 | * DEFAULT_CGROUP_MOUNT_PATH is the default cgroup mount path. 25 | **/ 26 | private static final Path DEFAULT_CGROUP_MOUNT_PATH = Paths.get("/sys/fs/cgroup"); 27 | /** 28 | * CGROUP_NS_PATH is the path to the cgroup namespace file. 29 | **/ 30 | private static final Path CGROUP_NS_PATH = Paths.get("/proc/self/ns/cgroup"); 31 | private static final String CONTAINER_SOURCE = "[0-9a-f]{64}"; 32 | private static final String TASK_SOURCE = "[0-9a-f]{32}-\\d+"; 33 | private static final Pattern LINE_RE = Pattern.compile("^\\d+:[^:]*:(.+)$", Pattern.MULTILINE | Pattern.UNIX_LINES); 34 | private static final Pattern CONTAINER_RE = Pattern.compile( 35 | "(" + CONTAINER_SOURCE + "|" + TASK_SOURCE + ")(?:.scope)?$"); 36 | 37 | /** 38 | * CGROUPV1_BASE_CONTROLLER is the controller used to identify the container-id 39 | * in cgroup v1 (memory). 40 | **/ 41 | private static final String CGROUPV1_BASE_CONTROLLER = "memory"; 42 | /** 43 | * CGROUPV2_BASE_CONTROLLER is the controller used to identify the container-id 44 | * in cgroup v2. 45 | **/ 46 | private static final String CGROUPV2_BASE_CONTROLLER = ""; 47 | /** 48 | * HOST_CGROUP_NAMESPACE_INODE is the inode of the host cgroup namespace. 49 | **/ 50 | private static final long HOST_CGROUP_NAMESPACE_INODE = 0xEFFFFFFBL; 51 | 52 | private boolean readOnce = false; 53 | /** 54 | * containerID holds either the container ID or the cgroup controller inode. 55 | **/ 56 | public String containerID; 57 | 58 | /** 59 | * Returns the container ID if available or the cgroup controller inode. 60 | * 61 | * @throws IOException if /proc/self/cgroup is readable and still an I/O error 62 | * occurs reading from the stream. 63 | */ 64 | public String getContainerID() throws IOException { 65 | if (readOnce) { 66 | return containerID; 67 | } 68 | 69 | final String cgroupContent = read(CGROUP_PATH); 70 | if (cgroupContent == null || cgroupContent.isEmpty()) { 71 | return null; 72 | } 73 | containerID = parse(cgroupContent); 74 | /* 75 | * If the container ID is not available it means that the application is either 76 | * not running in a container or running is private cgroup namespace, we 77 | * fallback to the cgroup controller inode. The agent (7.51+) will use it to get 78 | * the container ID. 79 | * In Host cgroup namespace, the container ID should be found. If it is not 80 | * found, it means that the application is running on a host/vm. 81 | * 82 | */ 83 | if ((containerID == null || containerID.equals("")) && !isHostCgroupNamespace(CGROUP_NS_PATH)) { 84 | containerID = getCgroupInode(DEFAULT_CGROUP_MOUNT_PATH, cgroupContent); 85 | } 86 | return containerID; 87 | } 88 | 89 | /** 90 | * Returns the content of `path` (=/proc/self/cgroup). 91 | * 92 | * @throws IOException if /proc/self/cgroup is readable and still an I/O error 93 | * occurs reading from the stream. 94 | */ 95 | private String read(Path path) throws IOException { 96 | readOnce = true; 97 | if (!Files.isReadable(path)) { 98 | return null; 99 | } 100 | 101 | return new String(Files.readAllBytes(path)); 102 | } 103 | 104 | /** 105 | * Parses a Cgroup file (=/proc/self/cgroup) content and returns the 106 | * corresponding container ID. It can be found only if the container 107 | * is running in host cgroup namespace. 108 | * 109 | * @param cgroupsContent Cgroup file content 110 | */ 111 | public static String parse(final String cgroupsContent) { 112 | final Matcher lines = LINE_RE.matcher(cgroupsContent); 113 | while (lines.find()) { 114 | final String path = lines.group(1); 115 | final Matcher matcher = CONTAINER_RE.matcher(path); 116 | if (matcher.find()) { 117 | return matcher.group(1); 118 | } 119 | } 120 | 121 | return null; 122 | } 123 | 124 | /** 125 | * Returns true if the host cgroup namespace is used. 126 | * It looks at the inode of `/proc/self/ns/cgroup` and compares it to 127 | * HOST_CGROUP_NAMESPACE_INODE. 128 | * 129 | * @param path Path to the cgroup namespace file. 130 | */ 131 | private static boolean isHostCgroupNamespace(final Path path) { 132 | long hostCgroupInode = inodeForPath(path); 133 | return hostCgroupInode == HOST_CGROUP_NAMESPACE_INODE; 134 | } 135 | 136 | /** 137 | * Returns the inode for the given path. 138 | * 139 | * @param path Path to the cgroup namespace file. 140 | */ 141 | private static long inodeForPath(final Path path) { 142 | try { 143 | long inode = (long) Files.getAttribute(path, "unix:ino"); 144 | return inode; 145 | } catch (Exception e) { 146 | return 0; 147 | } 148 | } 149 | 150 | /** 151 | * Returns the cgroup controller inode for the given cgroup mount path and 152 | * procSelfCgroupPath. 153 | * 154 | * @param cgroupMountPath Path to the cgroup mount point. 155 | * @param cgroupContent String content of the cgroup file. 156 | */ 157 | public static String getCgroupInode(final Path cgroupMountPath, final String cgroupContent) throws IOException { 158 | Map cgroupControllersPaths = parseCgroupNodePath(cgroupContent); 159 | if (cgroupControllersPaths == null) { 160 | return null; 161 | } 162 | 163 | // Retrieve the cgroup inode from /sys/fs/cgroup+controller+cgroupNodePath 164 | List controllers = Arrays.asList(CGROUPV1_BASE_CONTROLLER, CGROUPV2_BASE_CONTROLLER); 165 | for (String controller : controllers) { 166 | String cgroupNodePath = cgroupControllersPaths.get(controller); 167 | if (cgroupNodePath == null) { 168 | continue; 169 | } 170 | Path path = Paths.get(cgroupMountPath.toString(), controller, cgroupNodePath); 171 | long inode = inodeForPath(path); 172 | /* 173 | * Inode 0 is not a valid inode. Inode 1 is a bad block inode and inode 2 is the 174 | * root of a filesystem. We can safely ignore them. 175 | */ 176 | if (inode > 2) { 177 | return "in-" + inode; 178 | } 179 | } 180 | 181 | return null; 182 | } 183 | 184 | /** 185 | * Returns a map of cgroup controllers and their corresponding cgroup path. 186 | * 187 | * @param cgroupContent Cgroup file content. 188 | */ 189 | public static Map parseCgroupNodePath(final String cgroupContent) throws IOException { 190 | Map res = new HashMap<>(); 191 | BufferedReader br = new BufferedReader(new StringReader(cgroupContent)); 192 | 193 | String line; 194 | while ((line = br.readLine()) != null) { 195 | String[] tokens = line.split(":"); 196 | if (tokens.length != 3) { 197 | continue; 198 | } 199 | if (CGROUPV1_BASE_CONTROLLER.equals(tokens[1]) || CGROUPV2_BASE_CONTROLLER.equals(tokens[1])) { 200 | res.put(tokens[1], tokens[2]); 201 | } 202 | } 203 | 204 | br.close(); 205 | return res; 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /src/main/java/com/timgroup/statsd/ClientChannel.java: -------------------------------------------------------------------------------- 1 | package com.timgroup.statsd; 2 | 3 | import java.nio.channels.WritableByteChannel; 4 | 5 | interface ClientChannel extends WritableByteChannel { 6 | String getTransportType(); 7 | 8 | int getMaxPacketSizeBytes(); 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/timgroup/statsd/DatagramClientChannel.java: -------------------------------------------------------------------------------- 1 | package com.timgroup.statsd; 2 | 3 | import java.io.IOException; 4 | import java.net.SocketAddress; 5 | import java.nio.ByteBuffer; 6 | import java.nio.channels.DatagramChannel; 7 | 8 | class DatagramClientChannel implements ClientChannel { 9 | protected final DatagramChannel delegate; 10 | private final SocketAddress address; 11 | 12 | /** 13 | * Creates a new DatagramClientChannel using the default DatagramChannel. 14 | * @param address Address to connect the channel to 15 | * @throws IOException if an I/O error occurs 16 | */ 17 | DatagramClientChannel(SocketAddress address) throws IOException { 18 | this(DatagramChannel.open(), address); 19 | } 20 | 21 | /** 22 | * Creates a new DatagramClientChannel that wraps the delegate. 23 | * @param delegate Implementation this instance wraps 24 | * @param address Address to connect the channel to 25 | */ 26 | DatagramClientChannel(DatagramChannel delegate, SocketAddress address) { 27 | this.delegate = delegate; 28 | this.address = address; 29 | } 30 | 31 | @Override 32 | public boolean isOpen() { 33 | return delegate.isOpen(); 34 | } 35 | 36 | @Override 37 | public int write(ByteBuffer src) throws IOException { 38 | return delegate.send(src, address); 39 | } 40 | 41 | @Override 42 | public void close() throws IOException { 43 | delegate.close(); 44 | } 45 | 46 | @Override 47 | public String getTransportType() { 48 | return "udp"; 49 | } 50 | 51 | @Override 52 | public String toString() { 53 | return "[" + getTransportType() + "] " + address; 54 | } 55 | 56 | @Override 57 | public int getMaxPacketSizeBytes() { 58 | return NonBlockingStatsDClient.DEFAULT_UDP_MAX_PACKET_SIZE_BYTES; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/com/timgroup/statsd/DirectStatsDClient.java: -------------------------------------------------------------------------------- 1 | package com.timgroup.statsd; 2 | 3 | /** 4 | * DirectStatsDClient is an experimental extension of {@link StatsDClient} that allows for direct access to some 5 | * dogstatsd features. 6 | * 7 | *

It is not recommended to use this client in production. This client might allow you to take advantage of 8 | * new features in the agent before they are released, but it might also break your application. 9 | */ 10 | public interface DirectStatsDClient extends StatsDClient { 11 | 12 | /** 13 | * Records values for the specified named distribution. 14 | * 15 | *

The method doesn't take care of breaking down the values array if it is too large. It's up to the caller to 16 | * make sure the size is kept reasonable.

17 | * 18 | *

This method is a DataDog extension, and may not work with other servers.

19 | * 20 | *

This method is non-blocking and is guaranteed not to throw an exception.

21 | * 22 | * @param aspect the name of the distribution 23 | * @param values the values to be incorporated in the distribution 24 | * @param sampleRate percentage of time metric to be sent 25 | * @param tags array of tags to be added to the data 26 | */ 27 | void recordDistributionValues(String aspect, double[] values, double sampleRate, String... tags); 28 | 29 | 30 | /** 31 | * Records values for the specified named distribution. 32 | * 33 | *

The method doesn't take care of breaking down the values array if it is too large. It's up to the caller to 34 | * make sure the size is kept reasonable.

35 | * 36 | *

This method is a DataDog extension, and may not work with other servers.

37 | * 38 | *

This method is non-blocking and is guaranteed not to throw an exception.

39 | * 40 | * @param aspect the name of the distribution 41 | * @param values the values to be incorporated in the distribution 42 | * @param sampleRate percentage of time metric to be sent 43 | * @param tags array of tags to be added to the data 44 | */ 45 | void recordDistributionValues(String aspect, long[] values, double sampleRate, String... tags); 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/com/timgroup/statsd/Event.java: -------------------------------------------------------------------------------- 1 | package com.timgroup.statsd; 2 | 3 | import java.util.Date; 4 | 5 | /** 6 | * An event to send. 7 | * @see http://docs.datadoghq.com/guides/dogstatsd/#events 8 | */ 9 | public class Event { 10 | private String title; 11 | private String text; 12 | private long millisSinceEpoch = -1; 13 | private String hostname; 14 | private String aggregationKey; 15 | private String priority; 16 | private String sourceTypeName; 17 | private String alertType; 18 | 19 | public String getTitle() { 20 | return title; 21 | } 22 | 23 | public String getText() { 24 | return text; 25 | } 26 | 27 | /** 28 | * Get number of milliseconds since epoch started. 29 | * @return -1 if not set 30 | */ 31 | public long getMillisSinceEpoch() { 32 | return millisSinceEpoch; 33 | } 34 | 35 | public String getHostname() { 36 | return hostname; 37 | } 38 | 39 | public String getAggregationKey() { 40 | return aggregationKey; 41 | } 42 | 43 | public String getPriority() { 44 | return priority; 45 | } 46 | 47 | public String getSourceTypeName() { 48 | return sourceTypeName; 49 | } 50 | 51 | public String getAlertType() { 52 | return alertType; 53 | } 54 | 55 | public static Builder builder() { 56 | return new Builder(); 57 | } 58 | 59 | private Event(){} 60 | 61 | public enum Priority { 62 | LOW, NORMAL 63 | } 64 | 65 | public enum AlertType { 66 | ERROR, WARNING, INFO, SUCCESS 67 | } 68 | 69 | @SuppressWarnings({"AccessingNonPublicFieldOfAnotherObject", 70 | "PrivateMemberAccessBetweenOuterAndInnerClass", "ParameterHidesMemberVariable"}) 71 | public static class Builder { 72 | private final Event event = new Event(); 73 | 74 | private Builder() {} 75 | 76 | /** 77 | * Build factory method for the event. 78 | * @return Event built following specified options. 79 | */ 80 | public Event build() { 81 | if ((event.title == null) || event.title.isEmpty()) { 82 | throw new IllegalStateException("event title must be set"); 83 | } 84 | return event; 85 | } 86 | 87 | /** 88 | * Title for the event. 89 | * @param title 90 | * Event title ; mandatory 91 | * @return Builder object being used. 92 | */ 93 | public Builder withTitle(final String title) { 94 | event.title = title; 95 | return this; 96 | } 97 | 98 | /** 99 | * Text for the event. 100 | * @param text 101 | * Event text ; supports line breaks ; mandatory 102 | * @return Builder object being used. 103 | */ 104 | public Builder withText(final String text) { 105 | event.text = text; 106 | return this; 107 | } 108 | 109 | /** 110 | * Date for the event. 111 | * @param date 112 | * Assign a timestamp to the event ; Default: none (Default is the current Unix epoch timestamp when not sent) 113 | * @return Builder object being used. 114 | */ 115 | public Builder withDate(final Date date) { 116 | event.millisSinceEpoch = date.getTime(); 117 | return this; 118 | } 119 | 120 | /** 121 | * Date for the event. 122 | * @param millisSinceEpoch 123 | * Assign a timestamp to the event ; Default: none (Default is the current Unix epoch timestamp when not sent) 124 | * @return Builder object being used. 125 | */ 126 | public Builder withDate(final long millisSinceEpoch) { 127 | event.millisSinceEpoch = millisSinceEpoch; 128 | return this; 129 | } 130 | 131 | /** 132 | * Source hostname for the event. 133 | * @param hostname 134 | * Assign a hostname to the event ; Default: none 135 | * @return Builder object being used. 136 | */ 137 | public Builder withHostname(final String hostname) { 138 | event.hostname = hostname; 139 | return this; 140 | } 141 | 142 | /** 143 | * Aggregation key for the event. 144 | * @param aggregationKey 145 | * Assign an aggregation key to the event, to group it with some others ; Default: none 146 | * @return Builder object being used. 147 | */ 148 | public Builder withAggregationKey(final String aggregationKey) { 149 | event.aggregationKey = aggregationKey; 150 | return this; 151 | } 152 | 153 | /** 154 | * Priority for the event. 155 | * @param priority 156 | * Can be "normal" or "low" ; Default: "normal" 157 | * @return Builder object being used. 158 | */ 159 | public Builder withPriority(final Priority priority) { 160 | //noinspection StringToUpperCaseOrToLowerCaseWithoutLocale 161 | event.priority = priority.name().toLowerCase(); 162 | return this; 163 | } 164 | 165 | /** 166 | * Source Type name for the event. 167 | * @param sourceTypeName 168 | * Assign a source type to the event ; Default: none 169 | * @return Builder object being used. 170 | */ 171 | public Builder withSourceTypeName(final String sourceTypeName) { 172 | event.sourceTypeName = sourceTypeName; 173 | return this; 174 | } 175 | 176 | /** 177 | * Alert type for the event. 178 | * @param alertType 179 | * Can be "error", "warning", "info" or "success" ; Default: "info" 180 | * @return Builder object being used. 181 | */ 182 | public Builder withAlertType(final AlertType alertType) { 183 | //noinspection StringToUpperCaseOrToLowerCaseWithoutLocale 184 | event.alertType = alertType.name().toLowerCase(); 185 | return this; 186 | } 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /src/main/java/com/timgroup/statsd/InvalidMessageException.java: -------------------------------------------------------------------------------- 1 | package com.timgroup.statsd; 2 | 3 | /** 4 | * Signals that we've been passed a message that's invalid and won't be sent. 5 | * 6 | * @author Taylor Schilling 7 | */ 8 | 9 | public class InvalidMessageException extends RuntimeException { 10 | 11 | private final String invalidMessage; 12 | 13 | /** 14 | * Creates an InvalidMessageException with a specified detail message and the invalid message itself. 15 | * 16 | * @param detailMessage a message that details why the invalid message is considered so 17 | * @param invalidMessage the message deemed invalid 18 | */ 19 | public InvalidMessageException(final String detailMessage, final String invalidMessage) { 20 | super(detailMessage); 21 | this.invalidMessage = invalidMessage; 22 | } 23 | 24 | public String getInvalidMessage() { 25 | return invalidMessage; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/timgroup/statsd/MapUtils.java: -------------------------------------------------------------------------------- 1 | package com.timgroup.statsd; 2 | 3 | import java.lang.invoke.MethodHandle; 4 | import java.lang.invoke.MethodHandles; 5 | import java.lang.invoke.MethodType; 6 | import java.util.Map; 7 | 8 | /** 9 | * MethodHandle based bridge for using JDK8+ functionality at JDK7 language level. 10 | * Can be removed when support for JDK7 is dropped. 11 | */ 12 | public class MapUtils { 13 | 14 | private static final MethodHandle MAP_PUT_IF_ABSENT = buildMapPutIfAbsent(); 15 | 16 | /** 17 | * Emulates {@code Map.putIfAbsent} semantics. Replace when baselining at JDK8+. 18 | * @return the previous value associated with the message, or null if the value was not seen before 19 | */ 20 | static Message putIfAbsent(Map map, Message message) { 21 | if (MAP_PUT_IF_ABSENT != null) { 22 | try { 23 | return (Message) (Object) MAP_PUT_IF_ABSENT.invokeExact(map, (Object) message, (Object) message); 24 | } catch (Throwable ignore) { 25 | return putIfAbsentFallback(map, message); 26 | } 27 | } 28 | return putIfAbsentFallback(map, message); 29 | } 30 | 31 | /** 32 | * Emulates {@code Map.putIfAbsent} semantics. Replace when baselining at JDK8+. 33 | * @return the previous value associated with the message, or null if the value was not seen before 34 | */ 35 | private static Message putIfAbsentFallback(Map map, Message message) { 36 | if (map.containsKey(message)) { 37 | return map.get(message); 38 | } 39 | map.put(message, message); 40 | return null; 41 | } 42 | 43 | private static MethodHandle buildMapPutIfAbsent() { 44 | try { 45 | return MethodHandles.publicLookup() 46 | .findVirtual(Map.class, "putIfAbsent", 47 | MethodType.methodType(Object.class, Object.class, Object.class)); 48 | } catch (Throwable ignore) { 49 | return null; 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/com/timgroup/statsd/Message.java: -------------------------------------------------------------------------------- 1 | package com.timgroup.statsd; 2 | 3 | import java.util.Arrays; 4 | import java.util.EnumSet; 5 | import java.util.Objects; 6 | import java.util.Set; 7 | 8 | public abstract class Message implements Comparable { 9 | 10 | final String aspect; 11 | final Message.Type type; 12 | final String[] tags; 13 | protected boolean done; 14 | 15 | // borrowed from Array.hashCode implementation: 16 | // https://github.com/openjdk/jdk11/blob/master/src/java.base/share/classes/java/util/Arrays.java#L4454-L4465 17 | protected static final int HASH_MULTIPLIER = 31; 18 | 19 | public enum Type { 20 | GAUGE("g"), 21 | COUNT("c"), 22 | TIME("ms"), 23 | SET("s"), 24 | HISTOGRAM("h"), 25 | DISTRIBUTION("d"), 26 | EVENT("_e"), 27 | SERVICE_CHECK("_sc"); 28 | 29 | private final String type; 30 | 31 | Type(String type) { 32 | this.type = type; 33 | } 34 | 35 | public String toString() { 36 | return this.type; 37 | } 38 | } 39 | 40 | protected static final Set AGGREGATE_SET = EnumSet.of(Type.COUNT, Type.GAUGE, Type.SET); 41 | 42 | protected Message(Message.Type type) { 43 | this("", type, null); 44 | } 45 | 46 | protected Message(String aspect, Message.Type type, String[] tags) { 47 | this.aspect = aspect == null ? "" : aspect; 48 | this.type = type; 49 | this.done = false; 50 | this.tags = tags; 51 | } 52 | 53 | /** 54 | * Write this message to the provided {@link StringBuilder}. Will 55 | * be called from the sender threads. 56 | * 57 | * @param builder StringBuilder the text representation will be written to. 58 | * @param capacity The capacity of the send buffer. 59 | * @param containerID The container ID to be appended to the message. 60 | * @return boolean indicating whether the message was partially written to the builder. 61 | * If true, the method will be called again with the same arguments to continue writing. 62 | */ 63 | abstract boolean writeTo(StringBuilder builder, int capacity, String containerID); 64 | 65 | /** 66 | * Aggregate message. 67 | * 68 | * @param message 69 | * Message to aggregate. 70 | */ 71 | public abstract void aggregate(Message message); 72 | 73 | 74 | /** 75 | * Return the message aspect. 76 | * 77 | * @return returns the string representing the Message aspect 78 | */ 79 | public final String getAspect() { 80 | return this.aspect; 81 | } 82 | 83 | /** 84 | * Return the message type. 85 | * 86 | * @return returns the dogstatsd type for the Message 87 | */ 88 | public final Type getType() { 89 | return this.type; 90 | } 91 | 92 | /** 93 | * Return the array of tags for the message. 94 | * 95 | * @return returns the string array of tags for the Message 96 | */ 97 | public String[] getTags() { 98 | return this.tags; 99 | } 100 | 101 | /** 102 | * Return whether a message can be aggregated. 103 | * 104 | * @return boolean on whether or not this message type may be aggregated. 105 | */ 106 | public boolean canAggregate() { 107 | return AGGREGATE_SET.contains(type); 108 | } 109 | 110 | public void setDone(boolean done) { 111 | this.done = done; 112 | } 113 | 114 | public boolean getDone() { 115 | return this.done; 116 | } 117 | 118 | /** 119 | * Messages must implement hashCode. 120 | */ 121 | @Override 122 | public int hashCode() { 123 | return type.hashCode() * HASH_MULTIPLIER * HASH_MULTIPLIER 124 | + aspect.hashCode() * HASH_MULTIPLIER 125 | + Arrays.hashCode(this.tags); 126 | } 127 | 128 | /** 129 | * Messages must implement hashCode. 130 | */ 131 | @Override 132 | public boolean equals(Object object) { 133 | if (object == this) { 134 | return true; 135 | } 136 | if (object instanceof Message) { 137 | final Message msg = (Message)object; 138 | 139 | return (Objects.equals(this.getAspect(), msg.getAspect())) 140 | && (this.getType() == msg.getType()) 141 | && Arrays.equals(this.tags, msg.getTags()); 142 | } 143 | 144 | return false; 145 | } 146 | 147 | @Override 148 | public int compareTo(Message message) { 149 | int typeComparison = getType().compareTo(message.getType()); 150 | if (typeComparison == 0) { 151 | int aspectComparison = getAspect().compareTo(message.getAspect()); 152 | if (aspectComparison == 0) { 153 | if (tags == null && message.tags == null) { 154 | return 0; 155 | } else if (tags == null) { 156 | return 1; 157 | } else if (message.tags == null) { 158 | return -1; 159 | } 160 | if (tags.length == message.tags.length) { 161 | int comparison = 0; 162 | for (int i = 0; i < tags.length && comparison == 0; i++) { 163 | comparison = tags[i].compareTo(message.tags[i]); 164 | } 165 | return comparison; 166 | } 167 | return tags.length < message.tags.length ? 1 : -1; 168 | } 169 | return aspectComparison; 170 | } 171 | return typeComparison; 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /src/main/java/com/timgroup/statsd/NamedPipeClientChannel.java: -------------------------------------------------------------------------------- 1 | package com.timgroup.statsd; 2 | 3 | import java.io.FileNotFoundException; 4 | import java.io.IOException; 5 | import java.io.RandomAccessFile; 6 | import java.nio.ByteBuffer; 7 | import java.nio.channels.FileChannel; 8 | 9 | class NamedPipeClientChannel implements ClientChannel { 10 | private final RandomAccessFile randomAccessFile; 11 | private final FileChannel fileChannel; 12 | private final String pipe; 13 | 14 | /** 15 | * Creates a new NamedPipeClientChannel with the given address. 16 | * 17 | * @param address Location of named pipe 18 | * @throws FileNotFoundException if pipe does not exist 19 | */ 20 | NamedPipeClientChannel(NamedPipeSocketAddress address) throws FileNotFoundException { 21 | pipe = address.getPipe(); 22 | randomAccessFile = new RandomAccessFile(pipe, "rw"); 23 | fileChannel = randomAccessFile.getChannel(); 24 | } 25 | 26 | @Override 27 | public boolean isOpen() { 28 | return fileChannel.isOpen(); 29 | } 30 | 31 | @Override 32 | public int write(ByteBuffer src) throws IOException { 33 | return fileChannel.write(src); 34 | } 35 | 36 | @Override 37 | public void close() throws IOException { 38 | // closing the file also closes the channel 39 | randomAccessFile.close(); 40 | } 41 | 42 | @Override 43 | public String getTransportType() { 44 | return "namedpipe"; 45 | } 46 | 47 | @Override 48 | public String toString() { 49 | return pipe; 50 | } 51 | 52 | @Override 53 | public int getMaxPacketSizeBytes() { 54 | return NonBlockingStatsDClient.DEFAULT_UDS_MAX_PACKET_SIZE_BYTES; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/com/timgroup/statsd/NamedPipeSocketAddress.java: -------------------------------------------------------------------------------- 1 | package com.timgroup.statsd; 2 | 3 | import java.net.SocketAddress; 4 | 5 | public class NamedPipeSocketAddress extends SocketAddress { 6 | private static final String NAMED_PIPE_PREFIX = "\\\\.\\pipe\\"; 7 | private final String pipe; 8 | 9 | public NamedPipeSocketAddress(String pipeName) { 10 | this.pipe = normalizePipeName(pipeName); 11 | } 12 | 13 | public String getPipe() { 14 | return pipe; 15 | } 16 | 17 | /** 18 | * Return true if object is a NamedPipeSocketAddress referring to the same path. 19 | */ 20 | public boolean equals(Object object) { 21 | if (object instanceof NamedPipeSocketAddress) { 22 | return pipe.equals(((NamedPipeSocketAddress)object).pipe); 23 | } 24 | return false; 25 | } 26 | 27 | /** 28 | * A normalized version of the pipe name that includes the `\\.\pipe\` prefix 29 | */ 30 | static String normalizePipeName(String pipeName) { 31 | if (pipeName.startsWith(NAMED_PIPE_PREFIX)) { 32 | return pipeName; 33 | } else { 34 | return NAMED_PIPE_PREFIX + pipeName; 35 | } 36 | } 37 | 38 | static boolean isNamedPipe(String address) { 39 | return address.startsWith(NAMED_PIPE_PREFIX); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/timgroup/statsd/NoOpDirectStatsDClient.java: -------------------------------------------------------------------------------- 1 | package com.timgroup.statsd; 2 | 3 | /** 4 | * A No-Op {@link NonBlockingDirectStatsDClient}, which can be substituted in when metrics are not 5 | * required. 6 | */ 7 | public class NoOpDirectStatsDClient extends NoOpStatsDClient implements DirectStatsDClient { 8 | @Override public void recordDistributionValues(String aspect, double[] values, double sampleRate, String... tags) { } 9 | 10 | @Override public void recordDistributionValues(String aspect, long[] values, double sampleRate, String... tags) { } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/timgroup/statsd/NoOpStatsDClient.java: -------------------------------------------------------------------------------- 1 | package com.timgroup.statsd; 2 | 3 | /** 4 | * A No-Op StatsDClient, which can be substituted in when metrics are not 5 | * required. 6 | * 7 | * @author Tom Denley 8 | * 9 | */ 10 | public class NoOpStatsDClient implements StatsDClient { 11 | 12 | @Override public void stop() { } 13 | 14 | @Override public void close() { } 15 | 16 | @Override public void count(String aspect, long delta, String... tags) { } 17 | 18 | @Override public void count(String aspect, long delta, double sampleRate, String... tags) { } 19 | 20 | @Override public void count(String aspect, double delta, String... tags) { } 21 | 22 | @Override public void count(String aspect, double delta, double sampleRate, String... tags) { } 23 | 24 | @Override public void countWithTimestamp(String aspect, long delta, long timestamp, String... tags) { } 25 | 26 | @Override public void countWithTimestamp(String aspect, double delta, long timestamp, String... tags) { } 27 | 28 | @Override public void incrementCounter(String aspect, String... tags) { } 29 | 30 | @Override public void incrementCounter(String aspect, double sampleRate, String... tags) { } 31 | 32 | @Override public void increment(String aspect, String... tags) { } 33 | 34 | @Override public void increment(String aspect, double sampleRate, String...tags) { } 35 | 36 | @Override public void decrementCounter(String aspect, String... tags) { } 37 | 38 | @Override public void decrementCounter(String aspect, double sampleRate, String... tags) { } 39 | 40 | @Override public void decrement(String aspect, String... tags) { } 41 | 42 | @Override public void decrement(String aspect, double sampleRate, String... tags) { } 43 | 44 | @Override public void recordGaugeValue(String aspect, double value, String... tags) { } 45 | 46 | @Override public void recordGaugeValue(String aspect, double value, double sampleRate, String... tags) { } 47 | 48 | @Override public void recordGaugeValue(String aspect, long value, String... tags) { } 49 | 50 | @Override public void recordGaugeValue(String aspect, long value, double sampleRate, String... tags) { } 51 | 52 | @Override public void gauge(String aspect, double value, String... tags) { } 53 | 54 | @Override public void gauge(String aspect, double value, double sampleRate, String... tags) { } 55 | 56 | @Override public void gauge(String aspect, long value, String... tags) { } 57 | 58 | @Override public void gauge(String aspect, long value, double sampleRate, String... tags) { } 59 | 60 | @Override public void gaugeWithTimestamp(String aspect, double value, long timestamp, String... tags) { } 61 | 62 | @Override public void gaugeWithTimestamp(String aspect, long value, long timestamp, String... tags) { } 63 | 64 | @Override public void recordExecutionTime(String aspect, long timeInMs, String... tags) { } 65 | 66 | @Override public void recordExecutionTime(String aspect, long timeInMs, double sampleRate, String... tags) { } 67 | 68 | @Override public void time(String aspect, long value, String... tags) { } 69 | 70 | @Override public void time(String aspect, long value, double sampleRate, String... tags) { } 71 | 72 | @Override public void recordHistogramValue(String aspect, double value, String... tags) { } 73 | 74 | @Override public void recordHistogramValue(String aspect, double value, double sampleRate, String... tags) { } 75 | 76 | @Override public void recordHistogramValue(String aspect, long value, String... tags) { } 77 | 78 | @Override public void recordHistogramValue(String aspect, long value, double sampleRate, String... tags) { } 79 | 80 | @Override public void histogram(String aspect, double value, String... tags) { } 81 | 82 | @Override public void histogram(String aspect, double value, double sampleRate, String... tags) { } 83 | 84 | @Override public void histogram(String aspect, long value, String... tags) { } 85 | 86 | @Override public void histogram(String aspect, long value, double sampleRate, String... tags) { } 87 | 88 | @Override public void recordDistributionValue(String aspect, double value, String... tags) { } 89 | 90 | @Override public void recordDistributionValue(String aspect, double value, double sampleRate, String... tags) { } 91 | 92 | @Override public void recordDistributionValue(String aspect, long value, String... tags) { } 93 | 94 | @Override public void recordDistributionValue(String aspect, long value, double sampleRate, String... tags) { } 95 | 96 | @Override public void distribution(String aspect, double value, String... tags) { } 97 | 98 | @Override public void distribution(String aspect, double value, double sampleRate, String... tags) { } 99 | 100 | @Override public void distribution(String aspect, long value, String... tags) { } 101 | 102 | @Override public void distribution(String aspect, long value, double sampleRate, String... tags) { } 103 | 104 | @Override public void recordEvent(final Event event, final String... tags) { } 105 | 106 | @Override public void recordServiceCheckRun(ServiceCheck sc) { } 107 | 108 | @Override public void serviceCheck(ServiceCheck sc) { } 109 | 110 | @Override public void recordSetValue(String aspect, String value, String... tags) { } 111 | } 112 | -------------------------------------------------------------------------------- /src/main/java/com/timgroup/statsd/NonBlockingDirectStatsDClient.java: -------------------------------------------------------------------------------- 1 | package com.timgroup.statsd; 2 | 3 | class NonBlockingDirectStatsDClient extends NonBlockingStatsDClient implements DirectStatsDClient { 4 | 5 | public NonBlockingDirectStatsDClient(final NonBlockingStatsDClientBuilder builder) throws StatsDClientException { 6 | super(builder); 7 | } 8 | 9 | @Override 10 | public void recordDistributionValues(String aspect, double[] values, double sampleRate, String... tags) { 11 | if (values != null && values.length > 0) { 12 | sendMetric(new DoublesStatsDMessage(aspect, Message.Type.DISTRIBUTION, values, sampleRate, 0, tags)); 13 | } 14 | } 15 | 16 | @Override 17 | public void recordDistributionValues(String aspect, long[] values, double sampleRate, String... tags) { 18 | if (values != null && values.length > 0) { 19 | sendMetric(new LongsStatsDMessage(aspect, Message.Type.DISTRIBUTION, values, sampleRate, 0, tags)); 20 | } 21 | } 22 | 23 | abstract class MultiValuedStatsDMessage extends Message { 24 | private final double sampleRate; // NaN for none 25 | private final long timestamp; // zero for none 26 | private int metadataSize = -1; // Cache the size of the metadata, -1 means not calculated yet 27 | private int offset = 0; // The index of the first value that has not been written 28 | 29 | MultiValuedStatsDMessage(String aspect, Message.Type type, String[] tags, double sampleRate, long timestamp) { 30 | super(aspect, type, tags); 31 | this.sampleRate = sampleRate; 32 | this.timestamp = timestamp; 33 | } 34 | 35 | @Override 36 | public final boolean canAggregate() { 37 | return false; 38 | } 39 | 40 | @Override 41 | public final void aggregate(Message message) { 42 | } 43 | 44 | @Override 45 | public final boolean writeTo(StringBuilder builder, int capacity, String containerID) { 46 | int metadataSize = metadataSize(builder, containerID); 47 | writeHeadMetadata(builder); 48 | boolean partialWrite = writeValuesTo(builder, capacity - metadataSize); 49 | writeTailMetadata(builder, containerID); 50 | return partialWrite; 51 | 52 | } 53 | 54 | private int metadataSize(StringBuilder builder, String containerID) { 55 | if (metadataSize == -1) { 56 | final int previousLength = builder.length(); 57 | final int previousEncodedLength = Utf8.encodedLength(builder); 58 | writeHeadMetadata(builder); 59 | writeTailMetadata(builder, containerID); 60 | metadataSize = Utf8.encodedLength(builder) - previousEncodedLength; 61 | builder.setLength(previousLength); 62 | } 63 | return metadataSize; 64 | } 65 | 66 | private void writeHeadMetadata(StringBuilder builder) { 67 | builder.append(prefix).append(aspect); 68 | } 69 | 70 | private void writeTailMetadata(StringBuilder builder, String containerID) { 71 | builder.append('|').append(type); 72 | if (!Double.isNaN(sampleRate)) { 73 | builder.append('|').append('@').append(format(SAMPLE_RATE_FORMATTER, sampleRate)); 74 | } 75 | if (timestamp != 0) { 76 | builder.append("|T").append(timestamp); 77 | } 78 | tagString(tags, builder); 79 | if (containerID != null && !containerID.isEmpty()) { 80 | builder.append("|c:").append(containerID); 81 | } 82 | 83 | builder.append('\n'); 84 | } 85 | 86 | private boolean writeValuesTo(StringBuilder builder, int remainingCapacity) { 87 | if (offset >= lengthOfValues()) { 88 | return false; 89 | } 90 | 91 | int maxLength = builder.length() + remainingCapacity; 92 | 93 | // Add at least one value 94 | builder.append(':'); 95 | writeValueTo(builder, offset); 96 | int previousLength = builder.length(); 97 | 98 | // Add remaining values up to the max length 99 | for (int i = offset + 1; i < lengthOfValues(); i++) { 100 | builder.append(':'); 101 | writeValueTo(builder, i); 102 | if (builder.length() > maxLength) { 103 | builder.setLength(previousLength); 104 | offset = i; 105 | return true; 106 | } 107 | previousLength = builder.length(); 108 | } 109 | offset = lengthOfValues(); 110 | return false; 111 | } 112 | 113 | protected abstract int lengthOfValues(); 114 | 115 | protected abstract void writeValueTo(StringBuilder buffer, int index); 116 | } 117 | 118 | final class LongsStatsDMessage extends MultiValuedStatsDMessage { 119 | private final long[] values; 120 | 121 | LongsStatsDMessage(String aspect, Message.Type type, long[] values, double sampleRate, long timestamp, String[] tags) { 122 | super(aspect, type, tags, sampleRate, timestamp); 123 | this.values = values; 124 | } 125 | 126 | @Override 127 | protected int lengthOfValues() { 128 | return values.length; 129 | } 130 | 131 | @Override 132 | protected void writeValueTo(StringBuilder buffer, int index) { 133 | buffer.append(values[index]); 134 | } 135 | } 136 | 137 | final class DoublesStatsDMessage extends MultiValuedStatsDMessage { 138 | private final double[] values; 139 | 140 | DoublesStatsDMessage(String aspect, Message.Type type, double[] values, double sampleRate, long timestamp, 141 | String[] tags) { 142 | super(aspect, type, tags, sampleRate, timestamp); 143 | this.values = values; 144 | } 145 | 146 | @Override 147 | protected int lengthOfValues() { 148 | return values.length; 149 | } 150 | 151 | @Override 152 | protected void writeValueTo(StringBuilder buffer, int index) { 153 | buffer.append(values[index]); 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/main/java/com/timgroup/statsd/NumericMessage.java: -------------------------------------------------------------------------------- 1 | package com.timgroup.statsd; 2 | 3 | 4 | public abstract class NumericMessage extends Message { 5 | 6 | protected Number value; 7 | 8 | protected NumericMessage(Message.Type type) { 9 | super(type); 10 | } 11 | 12 | protected NumericMessage(String aspect, Message.Type type, T value, String[] tags) { 13 | super(aspect, type, tags); 14 | this.value = value; 15 | } 16 | 17 | 18 | /** 19 | * Aggregate message. 20 | * 21 | * @param message 22 | * Message to aggregate. 23 | */ 24 | @Override 25 | public void aggregate(Message message) { 26 | NumericMessage msg = (NumericMessage)message; 27 | Number value = msg.getValue(); 28 | switch (msg.getType()) { 29 | case GAUGE: 30 | setValue(value); 31 | break; 32 | default: 33 | if (value instanceof Double) { 34 | setValue(getValue().doubleValue() + value.doubleValue()); 35 | } else if (value instanceof Integer) { 36 | setValue(getValue().intValue() + value.intValue()); 37 | } else if (value instanceof Long) { 38 | setValue(getValue().longValue() + value.longValue()); 39 | } 40 | } 41 | 42 | return; 43 | } 44 | 45 | /** 46 | * Get underlying message value. 47 | * 48 | * @return returns the value for the Message 49 | */ 50 | public Number getValue() { 51 | return this.value; 52 | } 53 | 54 | /** 55 | * Set underlying message value. 56 | * 57 | * @param value the numeric value for the underlying message 58 | */ 59 | public void setValue(Number value) { 60 | this.value = value; 61 | } 62 | 63 | } 64 | 65 | -------------------------------------------------------------------------------- /src/main/java/com/timgroup/statsd/ServiceCheck.java: -------------------------------------------------------------------------------- 1 | package com.timgroup.statsd; 2 | 3 | /** 4 | * A service check model, which is used to format a service check message 5 | * sent to the datadog agent. 6 | */ 7 | public class ServiceCheck { 8 | 9 | public enum Status { 10 | OK(0), WARNING(1), CRITICAL(2), UNKNOWN(3); 11 | 12 | private final int val; 13 | Status(final int val) { 14 | this.val = val; 15 | } 16 | } 17 | 18 | private String name; 19 | private String hostname; 20 | private String message; 21 | 22 | private int checkRunId; 23 | private int timestamp; 24 | 25 | private Status status; 26 | 27 | private String[] tags; 28 | 29 | public static Builder builder() { 30 | return new Builder(); 31 | } 32 | 33 | public static class Builder { 34 | final ServiceCheck res = new ServiceCheck(); 35 | 36 | public Builder withName(final String name) { 37 | res.name = name; 38 | return this; 39 | } 40 | 41 | public Builder withHostname(final String hostname) { 42 | res.hostname = hostname; 43 | return this; 44 | } 45 | 46 | public Builder withMessage(final String message) { 47 | res.message = message; 48 | return this; 49 | } 50 | 51 | public Builder withCheckRunId(final int checkRunId) { 52 | res.checkRunId = checkRunId; 53 | return this; 54 | } 55 | 56 | public Builder withTimestamp(final int timestamp) { 57 | res.timestamp = timestamp; 58 | return this; 59 | } 60 | 61 | public Builder withStatus(final Status status) { 62 | res.status = status; 63 | return this; 64 | } 65 | 66 | public Builder withTags(final String[] tags) { 67 | res.tags = tags; 68 | return this; 69 | } 70 | 71 | public ServiceCheck build() { 72 | return res; 73 | } 74 | } 75 | 76 | private ServiceCheck() { 77 | } 78 | 79 | public String getName() { 80 | return name; 81 | } 82 | 83 | public int getStatus() { 84 | return status.val; 85 | } 86 | 87 | public String getMessage() { 88 | return message; 89 | } 90 | 91 | public String getEscapedMessage() { 92 | return message.replace("\n", "\\n").replace("m:", "m\\:"); 93 | } 94 | 95 | public String getHostname() { 96 | return hostname; 97 | } 98 | 99 | public int getTimestamp() { 100 | return timestamp; 101 | } 102 | 103 | public String[] getTags() { 104 | return tags; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/main/java/com/timgroup/statsd/StatsDAggregator.java: -------------------------------------------------------------------------------- 1 | package com.timgroup.statsd; 2 | 3 | import java.util.ArrayList; 4 | import java.util.HashMap; 5 | import java.util.Iterator; 6 | import java.util.Map; 7 | import java.util.Timer; 8 | import java.util.TimerTask; 9 | 10 | 11 | public class StatsDAggregator { 12 | public static int DEFAULT_FLUSH_INTERVAL = 2000; // 2s 13 | public static int DEFAULT_SHARDS = 4; // 4 partitions to reduce contention. 14 | 15 | protected final String AGGREGATOR_THREAD_NAME = "statsd-aggregator-thread"; 16 | protected final ArrayList> aggregateMetrics; 17 | 18 | protected final int shardGranularity; 19 | protected final long flushInterval; 20 | 21 | private final StatsDProcessor processor; 22 | 23 | protected Timer scheduler = null; 24 | 25 | private Telemetry telemetry; 26 | 27 | private class FlushTask extends TimerTask { 28 | @Override 29 | public void run() { 30 | flush(); 31 | } 32 | } 33 | 34 | /** 35 | * StatsDAggregtor constructor. 36 | * 37 | * @param processor the message processor, aggregated messages will be queued in the high priority queue. 38 | * @param shards number of shards for the aggregation map. 39 | * @param flushInterval flush interval in miliseconds, 0 disables message aggregation. 40 | * 41 | * */ 42 | public StatsDAggregator(final StatsDProcessor processor, final int shards, final long flushInterval) { 43 | this.processor = processor; 44 | this.flushInterval = flushInterval; 45 | this.shardGranularity = shards; 46 | this.aggregateMetrics = new ArrayList<>(shards); 47 | 48 | if (flushInterval > 0) { 49 | this.scheduler = new Timer(AGGREGATOR_THREAD_NAME, true); 50 | } 51 | 52 | for (int i = 0 ; i < this.shardGranularity ; i++) { 53 | this.aggregateMetrics.add(i, new HashMap()); 54 | } 55 | } 56 | 57 | /** 58 | * Start the aggregator flushing scheduler. 59 | * 60 | * */ 61 | public void start() { 62 | if (flushInterval > 0) { 63 | // snapshot of processor telemetry - avoid volatile reference to harness CPU cache 64 | // caller responsible of setting telemetry before starting 65 | telemetry = processor.getTelemetry(); 66 | scheduler.scheduleAtFixedRate(new FlushTask(), flushInterval, flushInterval); 67 | } 68 | } 69 | 70 | /** 71 | * Stop the aggregator flushing scheduler. 72 | * 73 | * */ 74 | public void stop() { 75 | if (flushInterval > 0) { 76 | scheduler.cancel(); 77 | } 78 | } 79 | 80 | /** 81 | * Aggregate a message if possible. 82 | * 83 | * @param message the dogstatsd Message we wish to aggregate. 84 | * @return a boolean reflecting if the message was aggregated. 85 | * 86 | * */ 87 | public boolean aggregateMessage(Message message) { 88 | if (flushInterval == 0 || !message.canAggregate() || message.getDone()) { 89 | return false; 90 | } 91 | 92 | 93 | int hash = message.hashCode(); 94 | int bucket = Math.abs(hash % this.shardGranularity); 95 | Map map = aggregateMetrics.get(bucket); 96 | 97 | synchronized (map) { 98 | // For now let's just put the message in the map 99 | Message msg = MapUtils.putIfAbsent(map, message); 100 | if (msg != null) { 101 | msg.aggregate(message); 102 | if (telemetry != null) { 103 | telemetry.incrAggregatedContexts(1); 104 | 105 | // developer metrics 106 | switch (message.getType()) { 107 | case GAUGE: 108 | telemetry.incrAggregatedGaugeContexts(1); 109 | break; 110 | case COUNT: 111 | telemetry.incrAggregatedCountContexts(1); 112 | break; 113 | case SET: 114 | telemetry.incrAggregatedSetContexts(1); 115 | break; 116 | default: 117 | break; 118 | } 119 | } 120 | } 121 | } 122 | 123 | return true; 124 | } 125 | 126 | public final long getFlushInterval() { 127 | return this.flushInterval; 128 | } 129 | 130 | public final int getShardGranularity() { 131 | return this.shardGranularity; 132 | } 133 | 134 | protected void flush() { 135 | for (int i = 0 ; i < shardGranularity ; i++) { 136 | Map map = aggregateMetrics.get(i); 137 | 138 | synchronized (map) { 139 | Iterator> iter = map.entrySet().iterator(); 140 | while (iter.hasNext()) { 141 | Message msg = iter.next().getValue(); 142 | msg.setDone(true); 143 | 144 | if (!processor.sendHighPrio(msg) && (telemetry != null)) { 145 | telemetry.incrPacketDroppedQueue(1); 146 | } 147 | 148 | iter.remove(); 149 | } 150 | } 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/main/java/com/timgroup/statsd/StatsDBlockingProcessor.java: -------------------------------------------------------------------------------- 1 | package com.timgroup.statsd; 2 | 3 | import com.timgroup.statsd.Message; 4 | 5 | import java.nio.BufferOverflowException; 6 | import java.nio.ByteBuffer; 7 | import java.util.concurrent.ArrayBlockingQueue; 8 | import java.util.concurrent.BlockingQueue; 9 | import java.util.concurrent.ThreadFactory; 10 | import java.util.concurrent.TimeUnit; 11 | 12 | 13 | public class StatsDBlockingProcessor extends StatsDProcessor { 14 | 15 | private final BlockingQueue messages; 16 | 17 | private class ProcessingTask extends StatsDProcessor.ProcessingTask { 18 | 19 | @Override 20 | protected Message getMessage() throws InterruptedException { 21 | return messages.poll(WAIT_SLEEP_MS, TimeUnit.MILLISECONDS); 22 | } 23 | 24 | @Override 25 | protected boolean haveMessages() { 26 | return !messages.isEmpty(); 27 | } 28 | } 29 | 30 | StatsDBlockingProcessor(final int queueSize, final StatsDClientErrorHandler handler, 31 | final int maxPacketSizeBytes, final int poolSize, final int workers, 32 | final int aggregatorFlushInterval, final int aggregatorShards, 33 | final ThreadFactory threadFactory, final String containerID) throws Exception { 34 | 35 | super(queueSize, handler, maxPacketSizeBytes, poolSize, workers, 36 | aggregatorFlushInterval, aggregatorShards, threadFactory, containerID); 37 | this.messages = new ArrayBlockingQueue<>(queueSize); 38 | } 39 | 40 | @Override 41 | protected ProcessingTask createProcessingTask() { 42 | return new ProcessingTask(); 43 | } 44 | 45 | @Override 46 | protected boolean send(final Message message) { 47 | try { 48 | if (!shutdown) { 49 | messages.put(message); 50 | return true; 51 | } 52 | } catch (InterruptedException e) { 53 | // NOTHING 54 | } 55 | 56 | return false; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/com/timgroup/statsd/StatsDClientErrorHandler.java: -------------------------------------------------------------------------------- 1 | package com.timgroup.statsd; 2 | 3 | /** 4 | * Describes a handler capable of processing exceptions that occur during StatsD client operations. 5 | * 6 | * @author Tom Denley 7 | * 8 | */ 9 | public interface StatsDClientErrorHandler { 10 | 11 | /** 12 | * Handle the given exception, which occurred during a StatsD client operation. 13 | * 14 | *

Should normally be implemented as a synchronized method.

15 | * 16 | * @param exception 17 | * the {@link Exception} that occurred 18 | */ 19 | void handle(Exception exception); 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/timgroup/statsd/StatsDClientException.java: -------------------------------------------------------------------------------- 1 | package com.timgroup.statsd; 2 | 3 | 4 | /** 5 | * Signals that an exception has occurred when trying to start the 6 | * StatsD client. 7 | * 8 | * @author Tom Denley 9 | * 10 | */ 11 | public final class StatsDClientException extends RuntimeException { 12 | 13 | private static final long serialVersionUID = 3186887620964773839L; 14 | 15 | public StatsDClientException() { 16 | super(); 17 | } 18 | 19 | public StatsDClientException(String message, Throwable cause) { 20 | super(message, cause); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/timgroup/statsd/StatsDNonBlockingProcessor.java: -------------------------------------------------------------------------------- 1 | package com.timgroup.statsd; 2 | 3 | import com.timgroup.statsd.Message; 4 | 5 | import java.nio.BufferOverflowException; 6 | import java.nio.ByteBuffer; 7 | import java.util.Queue; 8 | import java.util.concurrent.ConcurrentLinkedQueue; 9 | import java.util.concurrent.ThreadFactory; 10 | import java.util.concurrent.atomic.AtomicInteger; 11 | 12 | 13 | public class StatsDNonBlockingProcessor extends StatsDProcessor { 14 | 15 | private final Queue messages; 16 | private final AtomicInteger qsize; // qSize will not reflect actual size, but a close estimate. 17 | 18 | private class ProcessingTask extends StatsDProcessor.ProcessingTask { 19 | @Override 20 | protected Message getMessage() throws InterruptedException { 21 | final Message message = messages.poll(); 22 | if (message != null) { 23 | qsize.decrementAndGet(); 24 | return message; 25 | } 26 | 27 | Thread.sleep(WAIT_SLEEP_MS); 28 | 29 | return null; 30 | } 31 | 32 | @Override 33 | protected boolean haveMessages() { 34 | return !messages.isEmpty(); 35 | } 36 | } 37 | 38 | StatsDNonBlockingProcessor(final int queueSize, final StatsDClientErrorHandler handler, 39 | final int maxPacketSizeBytes, final int poolSize, final int workers, 40 | final int aggregatorFlushInterval, final int aggregatorShards, 41 | final ThreadFactory threadFactory, final String containerID) throws Exception { 42 | 43 | super(queueSize, handler, maxPacketSizeBytes, poolSize, workers, 44 | aggregatorFlushInterval, aggregatorShards, threadFactory, containerID); 45 | this.qsize = new AtomicInteger(0); 46 | this.messages = new ConcurrentLinkedQueue<>(); 47 | } 48 | 49 | @Override 50 | protected ProcessingTask createProcessingTask() { 51 | return new ProcessingTask(); 52 | } 53 | 54 | @Override 55 | protected boolean send(final Message message) { 56 | if (!shutdown) { 57 | if (qsize.get() < qcapacity) { 58 | messages.offer(message); 59 | qsize.incrementAndGet(); 60 | return true; 61 | } 62 | } 63 | 64 | return false; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/com/timgroup/statsd/StatsDProcessor.java: -------------------------------------------------------------------------------- 1 | package com.timgroup.statsd; 2 | 3 | import com.timgroup.statsd.Message; 4 | 5 | import java.nio.BufferOverflowException; 6 | import java.nio.ByteBuffer; 7 | 8 | import java.util.Queue; 9 | import java.util.concurrent.ArrayBlockingQueue; 10 | import java.util.concurrent.BlockingQueue; 11 | import java.util.concurrent.ConcurrentLinkedQueue; 12 | import java.util.concurrent.CountDownLatch; 13 | import java.util.concurrent.ThreadFactory; 14 | import java.util.concurrent.TimeUnit; 15 | import java.util.concurrent.atomic.AtomicInteger; 16 | 17 | public abstract class StatsDProcessor { 18 | protected static final String MESSAGE_TOO_LONG = "Message longer than size of sendBuffer"; 19 | protected static final int WAIT_SLEEP_MS = 10; // 10 ms would be a 100HZ slice 20 | 21 | protected final StatsDClientErrorHandler handler; 22 | 23 | final int maxPacketSizeBytes; 24 | protected final BufferPool bufferPool; 25 | protected final Queue highPrioMessages; // FIFO queue for high priority messages 26 | protected final BlockingQueue outboundQueue; // FIFO queue with outbound buffers 27 | protected final CountDownLatch endSignal; 28 | final CountDownLatch closeSignal; 29 | 30 | protected final ThreadFactory threadFactory; 31 | protected final Thread[] workers; 32 | protected final int qcapacity; 33 | 34 | protected StatsDAggregator aggregator; 35 | protected volatile Telemetry telemetry; 36 | 37 | protected volatile boolean shutdown; 38 | volatile boolean shutdownAgg; 39 | 40 | String containerID; 41 | 42 | protected abstract class ProcessingTask implements Runnable { 43 | protected StringBuilder builder = new StringBuilder(); 44 | char[] charBuffer = new char[maxPacketSizeBytes]; 45 | // + 4 so that we can check for buffer overflow without computing encoded length first 46 | final byte[] byteBuffer = new byte[maxPacketSizeBytes + 4]; 47 | 48 | public final void run() { 49 | try { 50 | processLoop(); 51 | } finally { 52 | endSignal.countDown(); 53 | } 54 | } 55 | 56 | protected void processLoop() { 57 | ByteBuffer sendBuffer; 58 | 59 | try { 60 | sendBuffer = bufferPool.borrow(); 61 | } catch (final InterruptedException e) { 62 | return; 63 | } 64 | 65 | boolean clientClosed = false; 66 | while (!Thread.interrupted()) { 67 | try { 68 | // Read flags before polling for messages. We can continue shutdown only when 69 | // shutdown flag is true *before* we get empty result from the queue. Shadow 70 | // the names, so we can't check the non-local copy by accident. 71 | boolean shutdown = StatsDProcessor.this.shutdown; 72 | boolean shutdownAgg = StatsDProcessor.this.shutdownAgg; 73 | 74 | Message message = highPrioMessages.poll(); 75 | if (message == null && shutdownAgg) { 76 | break; 77 | } 78 | if (message == null && !clientClosed) { 79 | message = getMessage(); 80 | } 81 | if (message == null) { 82 | if (shutdown && !clientClosed) { 83 | closeSignal.countDown(); 84 | clientClosed = true; 85 | } 86 | if (clientClosed) { 87 | // We are draining highPrioMessages, which is a non-blocking 88 | // queue. Avoid a busy loop if the queue is empty while the aggregator 89 | // is flushing. 90 | Thread.sleep(WAIT_SLEEP_MS); 91 | } 92 | continue; 93 | } 94 | 95 | if (aggregator.aggregateMessage(message)) { 96 | continue; 97 | } 98 | 99 | boolean partialWrite; 100 | do { 101 | builder.setLength(0); 102 | partialWrite = message.writeTo(builder, sendBuffer.capacity(), containerID); 103 | int lowerBoundSize = builder.length(); 104 | 105 | if (sendBuffer.capacity() < lowerBoundSize) { 106 | throw new InvalidMessageException(MESSAGE_TOO_LONG, builder.toString()); 107 | } 108 | 109 | if (sendBuffer.remaining() < (lowerBoundSize + 1)) { 110 | outboundQueue.put(sendBuffer); 111 | sendBuffer = bufferPool.borrow(); 112 | } 113 | 114 | try { 115 | writeBuilderToSendBuffer(sendBuffer); 116 | } catch (BufferOverflowException boe) { 117 | outboundQueue.put(sendBuffer); 118 | sendBuffer = bufferPool.borrow(); 119 | writeBuilderToSendBuffer(sendBuffer); 120 | } 121 | } 122 | while (partialWrite); 123 | 124 | if (!haveMessages()) { 125 | outboundQueue.put(sendBuffer); 126 | sendBuffer = bufferPool.borrow(); 127 | } 128 | } catch (final InterruptedException e) { 129 | break; 130 | } catch (final Exception e) { 131 | handler.handle(e); 132 | } 133 | } 134 | 135 | builder.setLength(0); 136 | builder.trimToSize(); 137 | } 138 | 139 | abstract boolean haveMessages(); 140 | 141 | abstract Message getMessage() throws InterruptedException; 142 | 143 | protected void writeBuilderToSendBuffer(ByteBuffer sendBuffer) { 144 | int length = builder.length(); 145 | if (length > charBuffer.length) { 146 | charBuffer = new char[length]; 147 | } 148 | 149 | // We trust this returns valid UTF-16. 150 | builder.getChars(0, length, charBuffer, 0); 151 | 152 | int blen = 0; 153 | for (int i = 0; i < length; i++) { 154 | char ch = charBuffer[i]; 155 | // https://en.wikipedia.org/wiki/UTF-8#Description 156 | // https://en.wikipedia.org/wiki/UTF-16#Description 157 | if (ch < 0x80) { 158 | byteBuffer[blen++] = (byte)ch; 159 | } else if (ch < 0x800) { 160 | byteBuffer[blen++] = (byte)(192 | (ch >> 6)); 161 | byteBuffer[blen++] = (byte)(128 | (ch & 63)); 162 | } else if (ch < 0xd800 || ch >= 0xe000) { 163 | byteBuffer[blen++] = (byte)(224 | (ch >> 12)); 164 | byteBuffer[blen++] = (byte)(128 | ((ch >> 6) & 63)); 165 | byteBuffer[blen++] = (byte)(128 | (ch & 63)); 166 | } else { 167 | // surrogate pair 168 | int decoded = ((ch & 0x3ff) << 10) | (charBuffer[++i] & 0x3ff) | 0x10000; 169 | byteBuffer[blen++] = (byte)(240 | (decoded >> 18)); 170 | byteBuffer[blen++] = (byte)(128 | ((decoded >> 12) & 63)); 171 | byteBuffer[blen++] = (byte)(128 | ((decoded >> 6) & 63)); 172 | byteBuffer[blen++] = (byte)(128 | (decoded & 63)); 173 | } 174 | 175 | if (blen >= maxPacketSizeBytes) { 176 | throw new BufferOverflowException(); 177 | } 178 | } 179 | 180 | sendBuffer.mark(); 181 | sendBuffer.put(byteBuffer, 0, blen); 182 | } 183 | } 184 | 185 | StatsDProcessor(final int queueSize, final StatsDClientErrorHandler handler, 186 | final int maxPacketSizeBytes, final int poolSize, final int workers, 187 | final int aggregatorFlushInterval, final int aggregatorShards, 188 | final ThreadFactory threadFactory, final String containerID) throws Exception { 189 | 190 | this.handler = handler; 191 | this.threadFactory = threadFactory; 192 | this.workers = new Thread[workers]; 193 | this.qcapacity = queueSize; 194 | 195 | this.maxPacketSizeBytes = maxPacketSizeBytes; 196 | this.bufferPool = new BufferPool(poolSize, maxPacketSizeBytes, true); 197 | this.highPrioMessages = new ConcurrentLinkedQueue<>(); 198 | this.outboundQueue = new ArrayBlockingQueue(poolSize); 199 | this.endSignal = new CountDownLatch(workers); 200 | this.closeSignal = new CountDownLatch(workers); 201 | this.aggregator = new StatsDAggregator(this, aggregatorShards, aggregatorFlushInterval); 202 | 203 | this.containerID = containerID; 204 | } 205 | 206 | protected abstract ProcessingTask createProcessingTask(); 207 | 208 | protected abstract boolean send(final Message message); 209 | 210 | protected boolean sendHighPrio(final Message message) { 211 | highPrioMessages.offer(message); 212 | return true; 213 | } 214 | 215 | public BufferPool getBufferPool() { 216 | return this.bufferPool; 217 | } 218 | 219 | public BlockingQueue getOutboundQueue() { 220 | return this.outboundQueue; 221 | } 222 | 223 | public int getQcapacity() { 224 | return this.qcapacity; 225 | } 226 | 227 | void startWorkers(final String namePrefix) { 228 | aggregator.start(); 229 | // each task is a busy loop taking up one thread, so keep it simple and use an array of threads 230 | for (int i = 0 ; i < workers.length ; i++) { 231 | workers[i] = threadFactory.newThread(createProcessingTask()); 232 | workers[i].setName(namePrefix + (i + 1)); 233 | workers[i].start(); 234 | } 235 | } 236 | 237 | public StatsDAggregator getAggregator() { 238 | return this.aggregator; 239 | } 240 | 241 | public void setTelemetry(final Telemetry telemetry) { 242 | this.telemetry = telemetry; 243 | } 244 | 245 | public Telemetry getTelemetry() { 246 | return telemetry; 247 | } 248 | 249 | void shutdown(boolean blocking) throws InterruptedException { 250 | shutdown = true; 251 | aggregator.stop(); 252 | 253 | if (blocking) { 254 | // Wait for messages to pass through the queues and the aggregator. Shutdown logic for 255 | // each queue follows the same pattern: 256 | // 257 | // if queue returns no messages after a shutdown flag is set, assume no new messages 258 | // will arrive and count down the latch. 259 | // 260 | // This may drop messages if send is called concurrently with shutdown (even in 261 | // blocking mode); but we will (attempt to) deliver everything that was sent before 262 | // this point. 263 | closeSignal.await(); 264 | aggregator.flush(); 265 | shutdownAgg = true; 266 | endSignal.await(); 267 | } else { 268 | // Stop all workers immediately. 269 | for (int i = 0 ; i < workers.length ; i++) { 270 | workers[i].interrupt(); 271 | } 272 | } 273 | } 274 | } 275 | -------------------------------------------------------------------------------- /src/main/java/com/timgroup/statsd/StatsDSender.java: -------------------------------------------------------------------------------- 1 | package com.timgroup.statsd; 2 | 3 | import java.io.IOException; 4 | import java.net.SocketAddress; 5 | import java.nio.ByteBuffer; 6 | import java.nio.channels.DatagramChannel; 7 | import java.nio.channels.WritableByteChannel; 8 | import java.util.concurrent.BlockingQueue; 9 | import java.util.concurrent.Callable; 10 | import java.util.concurrent.CountDownLatch; 11 | import java.util.concurrent.ThreadFactory; 12 | import java.util.concurrent.TimeUnit; 13 | import java.util.concurrent.atomic.AtomicInteger; 14 | 15 | public class StatsDSender { 16 | private final WritableByteChannel clientChannel; 17 | private final StatsDClientErrorHandler handler; 18 | 19 | private final BufferPool pool; 20 | private final BlockingQueue buffers; 21 | private static final int WAIT_SLEEP_MS = 10; // 10 ms would be a 100HZ slice 22 | 23 | protected final ThreadFactory threadFactory; 24 | protected final Thread[] workers; 25 | 26 | private final CountDownLatch endSignal; 27 | private volatile boolean shutdown; 28 | 29 | private volatile Telemetry telemetry; 30 | 31 | 32 | StatsDSender(final WritableByteChannel clientChannel, 33 | final StatsDClientErrorHandler handler, BufferPool pool, BlockingQueue buffers, 34 | final int workers, final ThreadFactory threadFactory) { 35 | 36 | this.pool = pool; 37 | this.buffers = buffers; 38 | this.handler = handler; 39 | this.threadFactory = threadFactory; 40 | this.workers = new Thread[workers]; 41 | 42 | this.clientChannel = clientChannel; 43 | 44 | this.endSignal = new CountDownLatch(workers); 45 | } 46 | 47 | public void setTelemetry(final Telemetry telemetry) { 48 | this.telemetry = telemetry; 49 | } 50 | 51 | public Telemetry getTelemetry() { 52 | return telemetry; 53 | } 54 | 55 | void startWorkers(final String namePrefix) { 56 | // each task is a busy loop taking up one thread, so keep it simple and use an array of threads 57 | for (int i = 0 ; i < workers.length ; i++) { 58 | workers[i] = threadFactory.newThread(new Runnable() { 59 | public void run() { 60 | try { 61 | sendLoop(); 62 | } finally { 63 | endSignal.countDown(); 64 | } 65 | } 66 | }); 67 | workers[i].setName(namePrefix + (i + 1)); 68 | workers[i].start(); 69 | } 70 | } 71 | 72 | void sendLoop() { 73 | ByteBuffer buffer = null; 74 | Telemetry telemetry = getTelemetry(); // attribute snapshot to harness CPU cache 75 | 76 | while (!(buffers.isEmpty() && shutdown)) { 77 | int sizeOfBuffer = 0; 78 | try { 79 | 80 | if (buffer != null) { 81 | buffer.clear(); 82 | pool.put(buffer); 83 | } 84 | 85 | buffer = buffers.poll(WAIT_SLEEP_MS, TimeUnit.MILLISECONDS); 86 | if (buffer == null) { 87 | continue; 88 | } 89 | 90 | sizeOfBuffer = buffer.position(); 91 | 92 | buffer.flip(); 93 | final int sentBytes = clientChannel.write(buffer); 94 | 95 | if (sizeOfBuffer != sentBytes) { 96 | throw new IOException( 97 | String.format("Could not send stat %s entirely to %s. Only sent %d out of %d bytes", 98 | buffer, 99 | clientChannel, 100 | sentBytes, 101 | sizeOfBuffer)); 102 | } 103 | 104 | if (telemetry != null) { 105 | telemetry.incrBytesSent(sizeOfBuffer); 106 | telemetry.incrPacketSent(1); 107 | } 108 | 109 | } catch (final InterruptedException e) { 110 | if (shutdown) { 111 | break; 112 | } 113 | } catch (final Exception e) { 114 | if (telemetry != null) { 115 | telemetry.incrBytesDropped(sizeOfBuffer); 116 | telemetry.incrPacketDropped(1); 117 | } 118 | handler.handle(e); 119 | } 120 | } 121 | } 122 | 123 | void shutdown(boolean blocking) throws InterruptedException { 124 | shutdown = true; 125 | if (blocking) { 126 | endSignal.await(); 127 | } else { 128 | for (int i = 0 ; i < workers.length ; i++) { 129 | workers[i].interrupt(); 130 | } 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/main/java/com/timgroup/statsd/StatsDThreadFactory.java: -------------------------------------------------------------------------------- 1 | package com.timgroup.statsd; 2 | 3 | import java.util.concurrent.Executors; 4 | import java.util.concurrent.ThreadFactory; 5 | 6 | final class StatsDThreadFactory implements ThreadFactory { 7 | private final ThreadFactory delegate = Executors.defaultThreadFactory(); 8 | 9 | @Override 10 | public Thread newThread(final Runnable runnable) { 11 | final Thread result = delegate.newThread(runnable); 12 | result.setName("StatsD-" + result.getName()); 13 | result.setDaemon(true); 14 | return result; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/timgroup/statsd/UnixDatagramClientChannel.java: -------------------------------------------------------------------------------- 1 | package com.timgroup.statsd; 2 | 3 | import jnr.unixsocket.UnixDatagramChannel; 4 | import jnr.unixsocket.UnixSocketOptions; 5 | 6 | import java.io.IOException; 7 | import java.net.SocketAddress; 8 | 9 | class UnixDatagramClientChannel extends DatagramClientChannel { 10 | /** 11 | * Creates a new UnixDatagramClientChannel. 12 | * 13 | * @param address Address to connect the channel to 14 | * @param timeout Send timeout 15 | * @param bufferSize Buffer size 16 | * @throws IOException if socket options cannot be set 17 | */ 18 | UnixDatagramClientChannel(SocketAddress address, int timeout, int bufferSize) throws IOException { 19 | super(UnixDatagramChannel.open(), address); 20 | // Set send timeout, to handle the case where the transmission buffer is full 21 | // If no timeout is set, the send becomes blocking 22 | if (timeout > 0) { 23 | delegate.setOption(UnixSocketOptions.SO_SNDTIMEO, timeout); 24 | } 25 | if (bufferSize > 0) { 26 | delegate.setOption(UnixSocketOptions.SO_SNDBUF, bufferSize); 27 | } 28 | } 29 | 30 | @Override 31 | public String getTransportType() { 32 | return "uds"; 33 | } 34 | 35 | @Override 36 | public int getMaxPacketSizeBytes() { 37 | return NonBlockingStatsDClient.DEFAULT_UDS_MAX_PACKET_SIZE_BYTES; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/timgroup/statsd/UnixSocketAddressWithTransport.java: -------------------------------------------------------------------------------- 1 | package com.timgroup.statsd; 2 | 3 | import java.net.SocketAddress; 4 | import java.util.Objects; 5 | 6 | public class UnixSocketAddressWithTransport extends SocketAddress { 7 | 8 | private final SocketAddress address; 9 | private final TransportType transportType; 10 | 11 | public enum TransportType { 12 | UDS_STREAM("uds-stream"), 13 | UDS_DATAGRAM("uds-datagram"), 14 | UDS("uds"); 15 | 16 | private final String transportType; 17 | 18 | TransportType(String transportType) { 19 | this.transportType = transportType; 20 | } 21 | 22 | String getTransportType() { 23 | return transportType; 24 | } 25 | 26 | static TransportType fromScheme(String scheme) { 27 | switch (scheme) { 28 | case "unixstream": 29 | return UDS_STREAM; 30 | case "unixgram": 31 | return UDS_DATAGRAM; 32 | case "unix": 33 | return UDS; 34 | default: 35 | break; 36 | } 37 | throw new IllegalArgumentException("Unknown scheme: " + scheme); 38 | } 39 | } 40 | 41 | public UnixSocketAddressWithTransport(final SocketAddress address, final TransportType transportType) { 42 | this.address = address; 43 | this.transportType = transportType; 44 | } 45 | 46 | @Override 47 | public boolean equals(Object other) { 48 | if (this == other) { 49 | return true; 50 | } 51 | if (other == null || getClass() != other.getClass()) { 52 | return false; 53 | } 54 | UnixSocketAddressWithTransport that = (UnixSocketAddressWithTransport) other; 55 | return Objects.equals(address, that.address) && transportType == that.transportType; 56 | } 57 | 58 | @Override 59 | public int hashCode() { 60 | return Objects.hash(address, transportType); 61 | } 62 | 63 | SocketAddress getAddress() { 64 | return address; 65 | } 66 | 67 | TransportType getTransportType() { 68 | return transportType; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/main/java/com/timgroup/statsd/UnixStreamClientChannel.java: -------------------------------------------------------------------------------- 1 | package com.timgroup.statsd; 2 | 3 | import jnr.unixsocket.UnixSocketAddress; 4 | import jnr.unixsocket.UnixSocketChannel; 5 | import jnr.unixsocket.UnixSocketOptions; 6 | 7 | import java.io.IOException; 8 | import java.net.SocketAddress; 9 | import java.nio.ByteBuffer; 10 | import java.nio.ByteOrder; 11 | import java.nio.channels.SocketChannel; 12 | 13 | /** 14 | * A ClientChannel for Unix domain sockets. 15 | */ 16 | public class UnixStreamClientChannel implements ClientChannel { 17 | private final UnixSocketAddress address; 18 | private final int timeout; 19 | private final int connectionTimeout; 20 | private final int bufferSize; 21 | 22 | 23 | private SocketChannel delegate; 24 | private final ByteBuffer delimiterBuffer = ByteBuffer.allocateDirect(Integer.SIZE / Byte.SIZE).order(ByteOrder.LITTLE_ENDIAN); 25 | 26 | /** 27 | * Creates a new NamedPipeClientChannel with the given address. 28 | * 29 | * @param address Location of named pipe 30 | */ 31 | UnixStreamClientChannel(SocketAddress address, int timeout, int connectionTimeout, int bufferSize) throws IOException { 32 | this.delegate = null; 33 | this.address = (UnixSocketAddress) address; 34 | this.timeout = timeout; 35 | this.connectionTimeout = connectionTimeout; 36 | this.bufferSize = bufferSize; 37 | } 38 | 39 | @Override 40 | public boolean isOpen() { 41 | return delegate.isConnected(); 42 | } 43 | 44 | @Override 45 | public synchronized int write(ByteBuffer src) throws IOException { 46 | connectIfNeeded(); 47 | 48 | int size = src.remaining(); 49 | int written = 0; 50 | if (size == 0) { 51 | return 0; 52 | } 53 | delimiterBuffer.clear(); 54 | delimiterBuffer.putInt(size); 55 | delimiterBuffer.flip(); 56 | 57 | try { 58 | long deadline = System.nanoTime() + timeout * 1_000_000L; 59 | written = writeAll(delimiterBuffer, true, deadline); 60 | if (written > 0) { 61 | written += writeAll(src, false, deadline); 62 | } 63 | } catch (IOException e) { 64 | // If we get an exception, it's unrecoverable, we close the channel and try to reconnect 65 | disconnect(); 66 | throw e; 67 | } 68 | 69 | // If we haven't written anything, we have a timeout 70 | if (written == 0) { 71 | throw new IOException("Write timed out"); 72 | } 73 | 74 | return size; 75 | } 76 | 77 | /** 78 | * Writes all bytes from the given buffer to the channel. 79 | * @param bb buffer to write 80 | * @param canReturnOnTimeout if true, we return if the channel is blocking and we haven't written anything yet 81 | * @param deadline deadline for the write 82 | * @return number of bytes written 83 | * @throws IOException if the channel is closed or an error occurs 84 | */ 85 | public int writeAll(ByteBuffer bb, boolean canReturnOnTimeout, long deadline) throws IOException { 86 | int remaining = bb.remaining(); 87 | int written = 0; 88 | while (remaining > 0) { 89 | int read = delegate.write(bb); 90 | 91 | // If we haven't written anything yet, we can still return 92 | if (read == 0 && canReturnOnTimeout && written == 0) { 93 | return written; 94 | } 95 | 96 | remaining -= read; 97 | written += read; 98 | 99 | if (deadline > 0 && System.nanoTime() > deadline) { 100 | throw new IOException("Write timed out"); 101 | } 102 | } 103 | return written; 104 | } 105 | 106 | private void connectIfNeeded() throws IOException { 107 | if (delegate == null) { 108 | connect(); 109 | } 110 | } 111 | 112 | private void disconnect() throws IOException { 113 | if (delegate != null) { 114 | delegate.close(); 115 | delegate = null; 116 | } 117 | } 118 | 119 | private void connect() throws IOException { 120 | if (this.delegate != null) { 121 | try { 122 | disconnect(); 123 | } catch (IOException e) { 124 | // ignore to be sure we don't stay with a broken delegate forever. 125 | } 126 | } 127 | 128 | UnixSocketChannel delegate = UnixSocketChannel.create(); 129 | 130 | long deadline = System.nanoTime() + connectionTimeout * 1_000_000L; 131 | if (connectionTimeout > 0) { 132 | // Set connect timeout, this should work at least on linux 133 | // https://elixir.bootlin.com/linux/v5.7.4/source/net/unix/af_unix.c#L1696 134 | // We'd have better timeout support if we used Java 16's native Unix domain socket support (JEP 380) 135 | delegate.setOption(UnixSocketOptions.SO_SNDTIMEO, connectionTimeout); 136 | } 137 | try { 138 | if (!delegate.connect(address)) { 139 | if (connectionTimeout > 0 && System.nanoTime() > deadline) { 140 | throw new IOException("Connection timed out"); 141 | } 142 | if (!delegate.finishConnect()) { 143 | throw new IOException("Connection failed"); 144 | } 145 | } 146 | 147 | delegate.setOption(UnixSocketOptions.SO_SNDTIMEO, Math.max(timeout, 0)); 148 | if (bufferSize > 0) { 149 | delegate.setOption(UnixSocketOptions.SO_SNDBUF, bufferSize); 150 | } 151 | } catch (Exception e) { 152 | try { 153 | delegate.close(); 154 | } catch (IOException __) { 155 | // ignore 156 | } 157 | throw e; 158 | } 159 | 160 | 161 | this.delegate = delegate; 162 | } 163 | 164 | @Override 165 | public void close() throws IOException { 166 | disconnect(); 167 | } 168 | 169 | @Override 170 | public String getTransportType() { 171 | return "uds-stream"; 172 | } 173 | 174 | @Override 175 | public String toString() { 176 | return "[" + getTransportType() + "] " + address; 177 | } 178 | 179 | @Override 180 | public int getMaxPacketSizeBytes() { 181 | return NonBlockingStatsDClient.DEFAULT_UDS_MAX_PACKET_SIZE_BYTES; 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /src/main/java/com/timgroup/statsd/Utf8.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2013 The Guava Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | 15 | package com.timgroup.statsd; 16 | 17 | import static java.lang.Character.MAX_SURROGATE; 18 | import static java.lang.Character.MIN_SURROGATE; 19 | 20 | import java.nio.charset.StandardCharsets; 21 | 22 | /** 23 | * This class is a partial copy of the {@code com.google.common.base.Utf8} 24 | * class 25 | * from the Guava library. 26 | * It is copied here to avoid a dependency on Guava. 27 | */ 28 | final class Utf8 { 29 | 30 | private static final int UTF8_REPLACEMENT_LENGTH = StandardCharsets.UTF_8.newEncoder().replacement().length; 31 | 32 | private Utf8() { 33 | } 34 | 35 | /** 36 | * Returns the number of bytes in the UTF-8-encoded form of {@code sequence}. For a string, this 37 | * method is equivalent to {@code string.getBytes(UTF_8).length}, but is more efficient in both 38 | * time and space. 39 | * 40 | * @throws IllegalArgumentException if {@code sequence} contains ill-formed UTF-16 (unpaired 41 | * surrogates) 42 | */ 43 | public static int encodedLength(CharSequence sequence) { 44 | // Warning to maintainers: this implementation is highly optimized. 45 | int utf16Length = sequence.length(); 46 | int utf8Length = utf16Length; 47 | int index = 0; 48 | 49 | // This loop optimizes for pure ASCII. 50 | while (index < utf16Length && sequence.charAt(index) < 0x80) { 51 | index++; 52 | } 53 | 54 | // This loop optimizes for chars less than 0x800. 55 | for (; index < utf16Length; index++) { 56 | char character = sequence.charAt(index); 57 | if (character < 0x800) { 58 | utf8Length += ((0x7f - character) >>> 31); // branch free! 59 | } else { 60 | utf8Length += encodedLengthGeneral(sequence, index); 61 | break; 62 | } 63 | } 64 | 65 | if (utf8Length < utf16Length) { 66 | // Necessary and sufficient condition for overflow because of maximum 3x expansion 67 | throw new IllegalArgumentException( 68 | "UTF-8 length does not fit in int: " + (utf8Length + (1L << 32))); 69 | } 70 | return utf8Length; 71 | } 72 | 73 | private static int encodedLengthGeneral(CharSequence sequence, int start) { 74 | int utf16Length = sequence.length(); 75 | int utf8Length = 0; 76 | for (int index = start; index < utf16Length; index++) { 77 | char character = sequence.charAt(index); 78 | if (character < 0x800) { 79 | utf8Length += (0x7f - character) >>> 31; // branch free! 80 | } else { 81 | utf8Length += 2; 82 | // jdk7+: if (Character.isSurrogate(character)) { 83 | if (MIN_SURROGATE <= character && character <= MAX_SURROGATE) { 84 | // Check that we have a well-formed surrogate pair. 85 | if (Character.codePointAt(sequence, index) == character) { 86 | // Bad input so deduct char length and account for the replacement characters 87 | utf8Length += -2 + UTF8_REPLACEMENT_LENGTH - 1; 88 | } else { 89 | index++; 90 | } 91 | } 92 | } 93 | } 94 | return utf8Length; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/main/resources/dogstatsd/version.properties: -------------------------------------------------------------------------------- 1 | dogstatsd_client_version=${project.version} 2 | -------------------------------------------------------------------------------- /src/test/java/com/timgroup/statsd/BuilderAddressTest.java: -------------------------------------------------------------------------------- 1 | package com.timgroup.statsd; 2 | 3 | import java.net.SocketAddress; 4 | import java.net.InetSocketAddress; 5 | 6 | import java.util.ArrayList; 7 | import java.util.Arrays; 8 | import java.util.Collection; 9 | 10 | import jnr.unixsocket.UnixSocketAddress; 11 | 12 | import org.junit.Before; 13 | import org.junit.Test; 14 | import org.junit.Rule; 15 | import org.junit.contrib.java.lang.system.EnvironmentVariables; 16 | 17 | import org.junit.runner.RunWith; 18 | import org.junit.runners.Parameterized; 19 | import org.junit.runners.Parameterized.Parameters; 20 | 21 | import static org.junit.Assert.assertEquals; 22 | 23 | @RunWith(Parameterized.class) 24 | public class BuilderAddressTest { 25 | @Rule 26 | public final EnvironmentVariables environmentVariables = new EnvironmentVariables(); 27 | 28 | final String url; 29 | final String host; 30 | final String port; 31 | final String pipe; 32 | final SocketAddress expected; 33 | 34 | public BuilderAddressTest(String url, String host, String port, String pipe, SocketAddress expected) { 35 | this.url = url; 36 | this.host = host; 37 | this.port = port; 38 | this.pipe = pipe; 39 | this.expected = expected; 40 | } 41 | 42 | static final private int defaultPort = NonBlockingStatsDClient.DEFAULT_DOGSTATSD_PORT; 43 | 44 | @Parameters 45 | public static Collection parameters() { 46 | ArrayList params = new ArrayList(); 47 | 48 | params.addAll(Arrays.asList(new Object[][]{ 49 | // DD_DOGSTATSD_URL 50 | { "udp://1.1.1.1", null, null, null, new InetSocketAddress("1.1.1.1", defaultPort) }, 51 | { "udp://1.1.1.1:9999", null, null, null, new InetSocketAddress("1.1.1.1", 9999) }, 52 | { "\\\\.\\pipe\\foo", null, null, null, new NamedPipeSocketAddress("\\\\.\\pipe\\foo") }, 53 | 54 | // DD_AGENT_HOST 55 | { null, "1.1.1.1", null, null, new InetSocketAddress("1.1.1.1", defaultPort) }, 56 | 57 | // DD_AGENT_HOST, DD_DOGSTATSD_PORT 58 | { null, "1.1.1.1", "9999", null, new InetSocketAddress("1.1.1.1", 9999) }, 59 | 60 | { null, null, null, "foo", new NamedPipeSocketAddress("\\\\.\\pipe\\foo") }, 61 | 62 | // DD_DOGSTATSD_URL overrides other env vars. 63 | { "udp://1.1.1.1", null, null, "foo", new InetSocketAddress("1.1.1.1", defaultPort) }, 64 | { "udp://1.1.1.1:9999", null, null, "foo", new InetSocketAddress("1.1.1.1", 9999) }, 65 | { "\\\\.\\pipe\\foo", null, null, "bar", new NamedPipeSocketAddress("\\\\.\\pipe\\foo") }, 66 | { "\\\\.\\pipe\\foo", "1.1.1.1", null, null, new NamedPipeSocketAddress("\\\\.\\pipe\\foo") }, 67 | { "\\\\.\\pipe\\foo", "1.1.1.1", "9999", null, new NamedPipeSocketAddress("\\\\.\\pipe\\foo") }, 68 | 69 | // DD_DOGSTATSD_NAMED_PIPE overrides DD_AGENT_HOST. 70 | { null, "1.1.1.1", null, "foo", new NamedPipeSocketAddress("\\\\.\\pipe\\foo") }, 71 | { null, "1.1.1.1", "9999", "foo", new NamedPipeSocketAddress("\\\\.\\pipe\\foo") }, 72 | })); 73 | 74 | if (TestHelpers.isJnrAvailable()) { 75 | // Here we use FakeUnixSocketAddress instead of UnixSocketAddress to make sure we can always run the tests without jnr-unixsock. 76 | 77 | UnixSocketAddressWithTransport unixDsd = new UnixSocketAddressWithTransport(new FakeUnixSocketAddress("/dsd.sock"), UnixSocketAddressWithTransport.TransportType.UDS); 78 | UnixSocketAddressWithTransport unixDgramDsd = new UnixSocketAddressWithTransport(new FakeUnixSocketAddress("/dsd.sock"), UnixSocketAddressWithTransport.TransportType.UDS_DATAGRAM); 79 | UnixSocketAddressWithTransport unixStreamDsd = new UnixSocketAddressWithTransport(new FakeUnixSocketAddress("/dsd.sock"), UnixSocketAddressWithTransport.TransportType.UDS_STREAM); 80 | 81 | params.addAll(Arrays.asList(new Object[][]{ 82 | { "unix:///dsd.sock", null, null, null, unixDsd }, 83 | { "unix://unused/dsd.sock", null, null, null, unixDsd }, 84 | { "unix://unused:9999/dsd.sock", null, null, null, unixDsd}, 85 | { null, "/dsd.sock", "0", null, unixDsd }, 86 | { "unix:///dsd.sock", "1.1.1.1", "9999", null, unixDsd }, 87 | { "unixgram:///dsd.sock", null, null, null, unixDgramDsd }, 88 | { "unixstream:///dsd.sock", null, null, null, unixStreamDsd }, 89 | })); 90 | } 91 | 92 | return params; 93 | } 94 | 95 | static class FakeUnixSocketAddress extends SocketAddress { 96 | final String path; 97 | public FakeUnixSocketAddress(String path) { 98 | this.path = path; 99 | } 100 | 101 | public String getPath() { 102 | return path; 103 | } 104 | } 105 | 106 | @Before 107 | public void set() { 108 | set(NonBlockingStatsDClient.DD_DOGSTATSD_URL_ENV_VAR, url); 109 | set(NonBlockingStatsDClient.DD_AGENT_HOST_ENV_VAR, host); 110 | set(NonBlockingStatsDClient.DD_DOGSTATSD_PORT_ENV_VAR, port); 111 | set(NonBlockingStatsDClient.DD_NAMED_PIPE_ENV_VAR, pipe); 112 | } 113 | 114 | void set(String name, String val) { 115 | if (val != null) { 116 | environmentVariables.set(name, val); 117 | } else { 118 | environmentVariables.clear(name); 119 | } 120 | } 121 | 122 | @Test(timeout = 5000L) 123 | public void address_resolution() throws Exception { 124 | NonBlockingStatsDClientBuilder b; 125 | 126 | // Default configuration matches env vars 127 | b = new NonBlockingStatsDClientBuilder().resolve(); 128 | SocketAddress actual = b.addressLookup.call(); 129 | 130 | // Make it possible to run this code even if we don't have jnr-unixsocket. 131 | if (expected instanceof UnixSocketAddressWithTransport) { 132 | UnixSocketAddressWithTransport a = (UnixSocketAddressWithTransport)actual; 133 | UnixSocketAddressWithTransport e = (UnixSocketAddressWithTransport)expected; 134 | assertEquals(((FakeUnixSocketAddress)e.getAddress()).getPath(), ((UnixSocketAddress)a.getAddress()).path()); 135 | assertEquals(e.getTransportType(), a.getTransportType()); 136 | } else { 137 | assertEquals(expected, actual); 138 | } 139 | 140 | // Explicit configuration is used regardless of environment variables. 141 | b = new NonBlockingStatsDClientBuilder().hostname("2.2.2.2").resolve(); 142 | assertEquals(new InetSocketAddress("2.2.2.2", defaultPort), b.addressLookup.call()); 143 | 144 | b = new NonBlockingStatsDClientBuilder().hostname("2.2.2.2").port(2222).resolve(); 145 | assertEquals(new InetSocketAddress("2.2.2.2", 2222), b.addressLookup.call()); 146 | 147 | b = new NonBlockingStatsDClientBuilder().namedPipe("ook").resolve(); 148 | assertEquals(new NamedPipeSocketAddress("ook"), b.addressLookup.call()); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/test/java/com/timgroup/statsd/DummyLowMemStatsDServer.java: -------------------------------------------------------------------------------- 1 | 2 | package com.timgroup.statsd; 3 | 4 | import java.io.IOException; 5 | import java.util.concurrent.atomic.AtomicInteger; 6 | 7 | 8 | class DummyLowMemStatsDServer extends UDPDummyStatsDServer { 9 | private final AtomicInteger messageCount = new AtomicInteger(0); 10 | 11 | public DummyLowMemStatsDServer(int port) throws IOException { 12 | super(port); 13 | } 14 | 15 | @Override 16 | public void clear() { 17 | messageCount.set(0); 18 | super.clear(); 19 | } 20 | 21 | public int getMessageCount() { 22 | return messageCount.get(); 23 | } 24 | 25 | @Override 26 | protected void addMessage(String msg) { 27 | messageCount.incrementAndGet(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/test/java/com/timgroup/statsd/DummyStatsDServer.java: -------------------------------------------------------------------------------- 1 | 2 | package com.timgroup.statsd; 3 | 4 | import java.io.Closeable; 5 | import java.io.IOException; 6 | import java.nio.Buffer; 7 | import java.nio.ByteBuffer; 8 | import java.nio.charset.StandardCharsets; 9 | import java.util.ArrayList; 10 | import java.util.List; 11 | import java.util.concurrent.atomic.AtomicInteger; 12 | 13 | import static com.timgroup.statsd.NonBlockingStatsDClient.DEFAULT_UDS_MAX_PACKET_SIZE_BYTES; 14 | 15 | abstract class DummyStatsDServer implements Closeable { 16 | private final List messagesReceived = new ArrayList(); 17 | private AtomicInteger packetsReceived = new AtomicInteger(0); 18 | 19 | protected volatile Boolean freeze = false; 20 | Thread thread; 21 | 22 | protected void listen() { 23 | thread = new Thread(new Runnable() { 24 | @Override 25 | public void run() { 26 | final ByteBuffer packet = ByteBuffer.allocate(DEFAULT_UDS_MAX_PACKET_SIZE_BYTES); 27 | 28 | while(isOpen() && !Thread.interrupted()) { 29 | if (freeze) { 30 | try { 31 | Thread.sleep(10); 32 | } catch (InterruptedException e) { 33 | } 34 | } else { 35 | try { 36 | ((Buffer)packet).clear(); // Cast necessary to handle Java9 covariant return types 37 | // see: https://jira.mongodb.org/browse/JAVA-2559 for ref. 38 | receive(packet); 39 | handlePacket(packet); 40 | } catch (IOException e) { 41 | } 42 | } 43 | } 44 | } 45 | }); 46 | thread.setDaemon(true); 47 | thread.start(); 48 | } 49 | 50 | protected boolean sleepIfFrozen() { 51 | if (freeze) { 52 | try { 53 | Thread.sleep(10); 54 | } catch (InterruptedException e) { 55 | } 56 | } 57 | return freeze; 58 | } 59 | 60 | protected void handlePacket(ByteBuffer packet) { 61 | packetsReceived.addAndGet(1); 62 | 63 | packet.flip(); 64 | for (String msg : StandardCharsets.UTF_8.decode(packet).toString().split("\n")) { 65 | addMessage(msg); 66 | } 67 | } 68 | 69 | public void waitForMessage() { 70 | waitForMessage(null); 71 | } 72 | 73 | public void waitForMessage(String prefix) { 74 | boolean done = false; 75 | 76 | while (!done) { 77 | try { 78 | synchronized(messagesReceived) { 79 | done = !messagesReceived.isEmpty(); 80 | } 81 | 82 | if (done && prefix != null && !prefix.isEmpty()) { 83 | done = false; 84 | List messages = this.messagesReceived(); 85 | for (String message : messages) { 86 | if(message.contains(prefix)) { 87 | return; 88 | } 89 | } 90 | } 91 | Thread.sleep(100L); 92 | } catch (InterruptedException e) { 93 | } 94 | } 95 | } 96 | 97 | public List messagesReceived() { 98 | synchronized(messagesReceived) { 99 | return new ArrayList(messagesReceived); 100 | } 101 | } 102 | 103 | public int getPacketsReceived() { 104 | return packetsReceived.get(); 105 | } 106 | 107 | public void freeze() { 108 | freeze = true; 109 | } 110 | 111 | public void unfreeze() { 112 | freeze = false; 113 | } 114 | 115 | public void clear() { 116 | packetsReceived.set(0); 117 | messagesReceived.clear(); 118 | } 119 | 120 | protected abstract boolean isOpen(); 121 | 122 | protected abstract void receive(ByteBuffer packet) throws IOException; 123 | 124 | protected void addMessage(String msg) { 125 | synchronized(messagesReceived) { 126 | String trimmed = msg.trim(); 127 | if (!trimmed.isEmpty()) { 128 | messagesReceived.add(msg.trim()); 129 | } 130 | } 131 | } 132 | 133 | } 134 | -------------------------------------------------------------------------------- /src/test/java/com/timgroup/statsd/EventTest.java: -------------------------------------------------------------------------------- 1 | package com.timgroup.statsd; 2 | 3 | import org.junit.Test; 4 | 5 | import java.util.Date; 6 | 7 | import static org.junit.Assert.assertEquals; 8 | 9 | public class EventTest { 10 | @Test 11 | public void builds() { 12 | final Event event = Event.builder() 13 | .withTitle("title1") 14 | .withText("text1") 15 | .withDate(1234) 16 | .withHostname("host1") 17 | .withPriority(Event.Priority.LOW) 18 | .withAggregationKey("key1") 19 | .withAlertType(Event.AlertType.ERROR) 20 | .withSourceTypeName("sourceType1") 21 | .build(); 22 | 23 | assertEquals("title1", event.getTitle()); 24 | assertEquals("text1", event.getText()); 25 | assertEquals(1234, event.getMillisSinceEpoch()); 26 | assertEquals("host1", event.getHostname()); 27 | assertEquals("low", event.getPriority()); 28 | assertEquals("key1", event.getAggregationKey()); 29 | assertEquals("error", event.getAlertType()); 30 | assertEquals("sourceType1", event.getSourceTypeName()); 31 | } 32 | 33 | @Test 34 | public void builds_with_defaults() { 35 | final Event event = Event.builder() 36 | .withTitle("title1") 37 | .withText("text1") 38 | .build(); 39 | 40 | assertEquals("title1", event.getTitle()); 41 | assertEquals("text1", event.getText()); 42 | assertEquals(-1, event.getMillisSinceEpoch()); 43 | assertEquals(null, event.getHostname()); 44 | assertEquals(null, event.getPriority()); 45 | assertEquals(null, event.getAggregationKey()); 46 | assertEquals(null, event.getAlertType()); 47 | assertEquals(null, event.getSourceTypeName()); 48 | } 49 | 50 | @Test (expected = IllegalStateException.class) 51 | public void fails_without_title() { 52 | Event.builder().withText("text1") 53 | .withDate(1234) 54 | .withHostname("host1") 55 | .withPriority(Event.Priority.LOW) 56 | .withAggregationKey("key1") 57 | .withAlertType(Event.AlertType.ERROR) 58 | .withSourceTypeName("sourceType1") 59 | .build(); 60 | } 61 | 62 | @Test 63 | public void builds_with_date() { 64 | final long expectedMillis = 1234567000; 65 | final Date date = new Date(expectedMillis); 66 | final Event event = Event.builder() 67 | .withTitle("title1") 68 | .withText("text1") 69 | .withDate(date) 70 | .build(); 71 | 72 | assertEquals("title1", event.getTitle()); 73 | assertEquals("text1", event.getText()); 74 | assertEquals(expectedMillis, event.getMillisSinceEpoch()); 75 | } 76 | 77 | @Test 78 | public void builds_without_text() { 79 | final long expectedMillis = 1234567000; 80 | final Date date = new Date(expectedMillis); 81 | final Event event = Event.builder() 82 | .withTitle("title1") 83 | .withDate(date) 84 | .build(); 85 | 86 | assertEquals("title1", event.getTitle()); 87 | assertEquals(null, event.getText()); 88 | assertEquals(expectedMillis, event.getMillisSinceEpoch()); 89 | } 90 | 91 | } 92 | -------------------------------------------------------------------------------- /src/test/java/com/timgroup/statsd/NamedPipeDummyStatsDServer.java: -------------------------------------------------------------------------------- 1 | package com.timgroup.statsd; 2 | 3 | import com.sun.jna.platform.win32.Kernel32; 4 | import com.sun.jna.platform.win32.WinBase; 5 | import com.sun.jna.platform.win32.WinError; 6 | import com.sun.jna.platform.win32.WinNT.HANDLE; 7 | import com.sun.jna.ptr.IntByReference; 8 | import java.io.IOException; 9 | import java.nio.ByteBuffer; 10 | import java.util.logging.Logger; 11 | 12 | // Template from https://github.com/java-native-access/jna/blob/master/contrib/platform/test/com/sun/jna/platform/win32/Kernel32NamedPipeTest.java 13 | // And https://docs.microsoft.com/en-us/windows/win32/ipc/multithreaded-pipe-server 14 | public class NamedPipeDummyStatsDServer extends DummyStatsDServer { 15 | private static final Logger log = Logger.getLogger("NamedPipeDummyStatsDServer"); 16 | private final HANDLE hNamedPipe; 17 | private volatile boolean clientConnected = false; 18 | private volatile boolean isOpen = true; 19 | 20 | public NamedPipeDummyStatsDServer(String pipeName) { 21 | String normalizedPipeName = NamedPipeSocketAddress.normalizePipeName(pipeName); 22 | 23 | hNamedPipe= Kernel32.INSTANCE.CreateNamedPipe(normalizedPipeName, 24 | WinBase.PIPE_ACCESS_DUPLEX, // dwOpenMode 25 | WinBase.PIPE_TYPE_BYTE | WinBase.PIPE_READMODE_BYTE | WinBase.PIPE_WAIT, // dwPipeMode 26 | 1, // nMaxInstances, 27 | Byte.MAX_VALUE, // nOutBufferSize, 28 | Byte.MAX_VALUE, // nInBufferSize, 29 | 1000, // nDefaultTimeOut, 30 | null); // lpSecurityAttributes 31 | 32 | if (WinBase.INVALID_HANDLE_VALUE.equals(hNamedPipe)) { 33 | throw new RuntimeException("Unable to create named pipe"); 34 | } 35 | 36 | listen(); 37 | } 38 | @Override 39 | protected boolean isOpen() { 40 | return isOpen; 41 | } 42 | 43 | @Override 44 | protected void receive(ByteBuffer packet) throws IOException { 45 | if (!isOpen) { 46 | throw new IOException("Server closed"); 47 | } 48 | if (!clientConnected) { 49 | boolean connected = Kernel32.INSTANCE.ConnectNamedPipe(hNamedPipe, null); 50 | // ERROR_PIPE_CONNECTED means the client connected before the server 51 | // The connection is established 52 | int lastError = Kernel32.INSTANCE.GetLastError(); 53 | connected = connected || lastError == WinError.ERROR_PIPE_CONNECTED; 54 | if (connected) { 55 | clientConnected = true; 56 | } else { 57 | log.info("Failed to connect. Last error: " + lastError); 58 | close(); 59 | return; 60 | } 61 | } 62 | 63 | IntByReference bytesRead = new IntByReference(); 64 | boolean success = Kernel32.INSTANCE.ReadFile( 65 | hNamedPipe, // handle to pipe 66 | packet.array(), // buffer to receive data 67 | packet.remaining(), // size of buffer 68 | bytesRead, // number of bytes read 69 | null); // not overlapped I/O 70 | 71 | log.info("Read bytes. Result: " + success + ". Bytes read: " + bytesRead.getValue()); 72 | packet.position(bytesRead.getValue()); 73 | } 74 | 75 | @Override 76 | public void close() throws IOException { 77 | isOpen = false; 78 | Kernel32.INSTANCE.CloseHandle(hNamedPipe); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/test/java/com/timgroup/statsd/NamedPipeTest.java: -------------------------------------------------------------------------------- 1 | package com.timgroup.statsd; 2 | 3 | import static org.hamcrest.MatcherAssert.assertThat; 4 | import static org.hamcrest.Matchers.contains; 5 | import static org.hamcrest.Matchers.nullValue; 6 | import static org.hamcrest.Matchers.instanceOf; 7 | 8 | 9 | import java.io.IOException; 10 | import java.net.InetSocketAddress; 11 | import java.util.Random; 12 | import java.util.logging.Logger; 13 | 14 | import org.junit.After; 15 | import org.junit.Assume; 16 | import org.junit.Before; 17 | import org.junit.BeforeClass; 18 | import org.junit.Test; 19 | import org.junit.Rule; 20 | import org.junit.contrib.java.lang.system.EnvironmentVariables; 21 | import static org.junit.Assert.assertEquals; 22 | 23 | public class NamedPipeTest implements StatsDClientErrorHandler { 24 | private static final Logger log = Logger.getLogger("NamedPipeTest"); 25 | 26 | private static final Random random = new Random(); 27 | private NonBlockingStatsDClient client; 28 | private DummyStatsDServer server; 29 | private volatile Exception lastException = new Exception(); 30 | 31 | @Rule 32 | public final EnvironmentVariables environmentVariables = new EnvironmentVariables(); 33 | 34 | public synchronized void handle(Exception exception) { 35 | log.info("Got exception: " + exception.getMessage()); 36 | lastException = exception; 37 | } 38 | 39 | @BeforeClass 40 | public static void supportedOnly() { 41 | Assume.assumeTrue(System.getProperty("os.name").toLowerCase().contains("windows")); 42 | } 43 | 44 | @Before 45 | public void start() { 46 | String pipeName = "testPipe-" + random.nextInt(10000); 47 | 48 | server = new NamedPipeDummyStatsDServer(pipeName); 49 | client = new NonBlockingStatsDClientBuilder().prefix("my.prefix") 50 | .namedPipe(pipeName) 51 | .queueSize(1) 52 | .enableAggregation(false) 53 | .errorHandler(this) 54 | .build(); 55 | } 56 | 57 | @After 58 | public void stop() throws IOException { 59 | if (client != null) { 60 | client.stop(); 61 | } 62 | if (server != null) { 63 | server.close(); 64 | } 65 | } 66 | 67 | @Test(timeout = 5000L) 68 | public void sends_to_statsd() { 69 | for(long i = 0; i < 5 ; i++) { 70 | client.gauge("mycount", i); 71 | server.waitForMessage(); 72 | String expected = String.format("my.prefix.mycount:%d|g", i); 73 | assertThat(server.messagesReceived(), contains(expected)); 74 | server.clear(); 75 | } 76 | assertThat(lastException.getMessage(), nullValue()); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/test/java/com/timgroup/statsd/NonBlockingDirectStatsDClientTest.java: -------------------------------------------------------------------------------- 1 | package com.timgroup.statsd; 2 | 3 | import org.junit.After; 4 | import org.junit.AfterClass; 5 | import org.junit.BeforeClass; 6 | import org.junit.FixMethodOrder; 7 | import org.junit.Rule; 8 | import org.junit.Test; 9 | import org.junit.contrib.java.lang.system.EnvironmentVariables; 10 | import org.junit.runners.MethodSorters; 11 | 12 | import java.io.IOException; 13 | 14 | import static org.hamcrest.MatcherAssert.assertThat; 15 | import static org.hamcrest.Matchers.comparesEqualTo; 16 | import static org.hamcrest.Matchers.hasItem; 17 | 18 | @FixMethodOrder(MethodSorters.NAME_ASCENDING) 19 | public class NonBlockingDirectStatsDClientTest { 20 | 21 | private static final int STATSD_SERVER_PORT = 17256; 22 | private static final int MAX_PACKET_SIZE = 64; 23 | private static DirectStatsDClient client; 24 | private static DummyStatsDServer server; 25 | 26 | @Rule 27 | public final EnvironmentVariables environmentVariables = new EnvironmentVariables(); 28 | 29 | @BeforeClass 30 | public static void start() throws IOException { 31 | server = new UDPDummyStatsDServer(STATSD_SERVER_PORT); 32 | client = new NonBlockingStatsDClientBuilder() 33 | .prefix("my.prefix") 34 | .hostname("localhost") 35 | .port(STATSD_SERVER_PORT) 36 | .enableTelemetry(false) 37 | .originDetectionEnabled(false) 38 | .maxPacketSizeBytes(MAX_PACKET_SIZE) 39 | .buildDirectStatsDClient(); 40 | } 41 | 42 | @AfterClass 43 | public static void stop() { 44 | try { 45 | client.stop(); 46 | server.close(); 47 | } catch (java.io.IOException ignored) { 48 | } 49 | } 50 | 51 | @After 52 | public void clear() { 53 | server.clear(); 54 | } 55 | 56 | 57 | @Test(timeout = 5000L) 58 | public void sends_multivalued_distribution_to_statsd() { 59 | client.recordDistributionValues("mydistribution", new long[] { 423L, 234L }, Double.NaN); 60 | server.waitForMessage("my.prefix"); 61 | 62 | assertThat(server.messagesReceived(), hasItem(comparesEqualTo("my.prefix.mydistribution:423:234|d"))); 63 | } 64 | 65 | @Test(timeout = 5000L) 66 | public void sends_double_multivalued_distribution_to_statsd() { 67 | client.recordDistributionValues("mydistribution", new double[] { 0.423D, 0.234D }, Double.NaN); 68 | server.waitForMessage("my.prefix"); 69 | 70 | assertThat(server.messagesReceived(), hasItem(comparesEqualTo("my.prefix.mydistribution:0.423:0.234|d"))); 71 | } 72 | 73 | @Test(timeout = 5000L) 74 | public void sends_multivalued_distribution_to_statsd_with_tags() { 75 | client.recordDistributionValues("mydistribution", new long[] { 423L, 234L }, Double.NaN, "foo:bar", "baz"); 76 | server.waitForMessage("my.prefix"); 77 | 78 | assertThat(server.messagesReceived(), hasItem(comparesEqualTo("my.prefix.mydistribution:423:234|d|#baz,foo:bar"))); 79 | } 80 | 81 | @Test(timeout = 5000L) 82 | public void sends_multivalued_distribution_to_statsd_with_sampling_rate() { 83 | client.recordDistributionValues("mydistribution", new long[] { 423L, 234L }, 1); 84 | server.waitForMessage("my.prefix"); 85 | 86 | assertThat(server.messagesReceived(), hasItem(comparesEqualTo("my.prefix.mydistribution:423:234|d|@1.000000"))); 87 | } 88 | 89 | @Test(timeout = 5000L) 90 | public void sends_multivalued_distribution_to_statsd_with_non_1_sampling_rate() { 91 | client.recordDistributionValues("mydistribution", new long[] { 423L, 234L }, 0.1); 92 | server.waitForMessage("my.prefix"); 93 | 94 | assertThat(server.messagesReceived(), hasItem(comparesEqualTo("my.prefix.mydistribution:423:234|d|@0.100000"))); 95 | } 96 | 97 | @Test(timeout = 5000L) 98 | public void sends_multivalued_distribution_to_statsd_with_tags_and_sampling_rate() { 99 | client.recordDistributionValues("mydistribution", new long[] { 423L, 234L }, 1, "foo:bar", "baz"); 100 | server.waitForMessage("my.prefix"); 101 | 102 | assertThat(server.messagesReceived(), hasItem(comparesEqualTo("my.prefix.mydistribution:423:234|d|@1.000000|#baz,foo:bar"))); 103 | } 104 | 105 | @Test(timeout = 5000L) 106 | public void sends_too_long_multivalued_distribution_to_statsd() { 107 | long[] values = {423L, 234L, 456L, 512L, 345L, 898L, 959876543123L, 667L}; 108 | client.recordDistributionValues("mydistribution", values, 0.4, "foo:bar", "baz"); 109 | 110 | server.waitForMessage("my.prefix"); 111 | assertThat(server.messagesReceived(), hasItem(comparesEqualTo("my.prefix.mydistribution:423:234:456|d|@0.400000|#baz,foo:bar"))); 112 | 113 | server.waitForMessage("my.prefix"); 114 | assertThat(server.messagesReceived(), hasItem(comparesEqualTo("my.prefix.mydistribution:512:345:898|d|@0.400000|#baz,foo:bar"))); 115 | 116 | server.waitForMessage("my.prefix"); 117 | assertThat(server.messagesReceived(), hasItem(comparesEqualTo("my.prefix.mydistribution:959876543123|d|@0.400000|#baz,foo:bar"))); 118 | 119 | server.waitForMessage("my.prefix"); 120 | assertThat(server.messagesReceived(), hasItem(comparesEqualTo("my.prefix.mydistribution:667|d|@0.400000|#baz,foo:bar"))); 121 | } 122 | 123 | } 124 | -------------------------------------------------------------------------------- /src/test/java/com/timgroup/statsd/NonBlockingStatsDClientMaxPerfTest.java: -------------------------------------------------------------------------------- 1 | package com.timgroup.statsd; 2 | 3 | import java.io.IOException; 4 | import java.net.SocketException; 5 | 6 | import java.util.Arrays; 7 | import java.util.Collection; 8 | import java.util.Random; 9 | import java.util.logging.Logger; 10 | import java.util.concurrent.ExecutorService; 11 | import java.util.concurrent.Executors; 12 | import java.util.concurrent.TimeUnit; 13 | import java.util.concurrent.atomic.AtomicBoolean; 14 | 15 | import org.junit.After; 16 | import org.junit.Assume; 17 | import org.junit.Before; 18 | import org.junit.Test; 19 | import org.junit.runner.RunWith; 20 | import org.junit.runners.Parameterized; 21 | import org.junit.runners.Parameterized.Parameters; 22 | 23 | import static org.junit.Assert.assertNotEquals; 24 | 25 | @RunWith(Parameterized.class) 26 | public final class NonBlockingStatsDClientMaxPerfTest { 27 | 28 | private static final int testWorkers = 4; 29 | private static final int port = 17255; 30 | private final int processorWorkers; 31 | private final int senderWorkers; 32 | private final int duration; // Duration in secs 33 | private final int qSize; // Queue length (number of elements) 34 | 35 | private NonBlockingStatsDClient client; 36 | private DummyLowMemStatsDServer server; 37 | 38 | private AtomicBoolean running; 39 | private final ExecutorService executor; 40 | 41 | private static Logger log = Logger.getLogger("NonBlockingStatsDClientMaxPerfTest"); 42 | 43 | @Parameters 44 | public static Collection data() { 45 | return Arrays.asList(new Object[][] { 46 | { 30, false, 256, 1, 1 }, // 30 seconds, non-blocking, 256 qSize, 1 worker 47 | { 30, false, 512, 1, 1 }, // 30 seconds, non-blocking, 512 qSize, 1 worker 48 | { 30, false, 1024, 1, 1 }, // 30 seconds, non-blocking, 1024 qSize, 1 worker 49 | { 30, false, 2048, 1, 1 }, // 30 seconds, non-blocking, 2048 qSize, 1 worker 50 | { 30, false, 4096, 1, 1 }, // 30 seconds, non-blocking, 4096 qSize, 1 worker 51 | // { 30, 17260, Integer.MAX_VALUE, 1 }, // 30 seconds, non-blocking, MAX_VALUE qSize, 1 worker 52 | { 30, false, 256, 2, 1 }, // 30 seconds, non-blocking, 256 qSize, 2 workers 53 | { 30, false, 512, 2, 1 }, // 30 seconds, non-blocking, 512 qSize, 2 workers 54 | { 30, false, 1024, 2, 1 }, // 30 seconds, non-blocking, 1024 qSize, 2 workers 55 | { 30, false, 2048, 2, 1 }, // 30 seconds, non-blocking, 2048 qSize, 2 workers 56 | { 30, false, 4096, 2, 1 }, // 30 seconds, non-blocking, 4096 qSize, 2 workers 57 | // // { 30, false, Integer.MAX_VALUE, 2 } // 30 seconds, non-blocking, MAX_VALUE qSize, 2 workers 58 | { 30, false, 256, 1, 2}, // 30 seconds, non-blocking, 256 qSize, 1 sender worker, 2 processor workers 59 | { 30, false, 512, 1, 2 }, // 30 seconds, non-blocking, 512 qSize, 1 sender worker, 2 processor workers 60 | { 30, false, 1024, 1, 2 }, // 30 seconds, non-blocking, 1024 qSize, 1 sender worker, 2 processor workers 61 | { 30, false, 2048, 1, 2 }, // 30 seconds, non-blocking, 2048 qSize, 1 sender worke, 2 processor workers 62 | { 30, false, 4096, 1, 2 }, // 30 seconds, non-blocking, 4096 qSize, 1 sender worke, 2 processor workers 63 | // // { 30, false, Integer.MAX_VALUE, 1, 2 }, // 30 seconds, non-blocking, MAX_VALUE qSize, 1 worker 64 | { 30, false, 256, 2, 2 }, // 30 seconds, non-blocking, 256 qSize, 2 sender workers, 2 processor workers 65 | { 30, false, 512, 2, 2 }, // 30 seconds, non-blocking, 512 qSize, 2 sender workers, 2 processor workers 66 | { 30, false, 1024, 2, 2 }, // 30 seconds, non-blocking, 1024 qSize, 2 sender workers, 2 processor workers 67 | { 30, false, 2048, 2, 2 }, // 30 seconds, non-blocking, 2048 qSize, 2 sender workers, 2 processor workers 68 | { 30, false, 4096, 2, 2 }, // 30 seconds, non-blocking, 4096 qSize, 2 sender workers, 2 processor workers 69 | // // { 30, false, Integer.MAX_VALUE, 2 } // 30 seconds, non-blocking, MAX_VALUE qSize, 2 sender workers 70 | { 30, true, 256, 1, 1 }, // 30 seconds, non-blocking, 256 qSize, 1 worker 71 | { 30, true, 512, 1, 1 }, // 30 seconds, non-blocking, 512 qSize, 1 worker 72 | { 30, true, 1024, 1, 1 }, // 30 seconds, non-blocking, 1024 qSize, 1 worker 73 | { 30, true, 2048, 1, 1 }, // 30 seconds, non-blocking, 2048 qSize, 1 worker 74 | { 30, true, 4096, 1, 1 }, // 30 seconds, non-blocking, 4096 qSize, 1 worker 75 | // { 30, 17260, Integer.MAX_VALUE, 1 }, // 30 seconds, non-blocking, MAX_VALUE qSize, 1 worker 76 | { 30, true, 256, 2, 1 }, // 30 seconds, non-blocking, 256 qSize, 2 workers 77 | { 30, true, 512, 2, 1 }, // 30 seconds, non-blocking, 512 qSize, 2 workers 78 | { 30, true, 1024, 2, 1 }, // 30 seconds, non-blocking, 1024 qSize, 2 workers 79 | { 30, true, 2048, 2, 1 }, // 30 seconds, non-blocking, 2048 qSize, 2 workers 80 | { 30, true, 4096, 2, 1 }, // 30 seconds, non-blocking, 4096 qSize, 2 workers 81 | // // { 30, true, Integer.MAX_VALUE, 2 } // 30 seconds, non-blocking, MAX_VALUE qSize, 2 workers 82 | { 30, true, 256, 1, 2}, // 30 seconds, non-blocking, 256 qSize, 1 sender worker, 2 processor workers 83 | { 30, true, 512, 1, 2 }, // 30 seconds, non-blocking, 512 qSize, 1 sender worker, 2 processor workers 84 | { 30, true, 1024, 1, 2 }, // 30 seconds, non-blocking, 1024 qSize, 1 sender worker, 2 processor workers 85 | { 30, true, 2048, 1, 2 }, // 30 seconds, non-blocking, 2048 qSize, 1 sender worke, 2 processor workers 86 | { 30, true, 4096, 1, 2 }, // 30 seconds, non-blocking, 4096 qSize, 1 sender worke, 2 processor workers 87 | // // { 30, true, Integer.MAX_VALUE, 1, 2 }, // 30 seconds, non-blocking, MAX_VALUE qSize, 1 worker 88 | { 30, true, 256, 2, 2 }, // 30 seconds, non-blocking, 256 qSize, 2 sender workers, 2 processor workers 89 | { 30, true, 512, 2, 2 }, // 30 seconds, non-blocking, 512 qSize, 2 sender workers, 2 processor workers 90 | { 30, true, 1024, 2, 2 }, // 30 seconds, non-blocking, 1024 qSize, 2 sender workers, 2 processor workers 91 | { 30, true, 2048, 2, 2 }, // 30 seconds, non-blocking, 2048 qSize, 2 sender workers, 2 processor workers 92 | { 30, true, 4096, 2, 2 }, // 30 seconds, non-blocking, 4096 qSize, 2 sender workers, 2 processor workers 93 | // // { 30, true, Integer.MAX_VALUE, 2 } // 30 seconds, non-blocking, MAX_VALUE qSize, 2 sender workers 94 | }); 95 | } 96 | 97 | public NonBlockingStatsDClientMaxPerfTest(int duration, boolean blocking, int qSize, 98 | int processorWorkers, int senderWorkers) throws IOException { 99 | this.duration = duration; 100 | this.qSize = qSize; 101 | this.processorWorkers = processorWorkers; 102 | this.senderWorkers = senderWorkers; 103 | this.client = new NonBlockingStatsDClientBuilder().prefix("my.prefix") 104 | .hostname("localhost") 105 | .port(port) 106 | .blocking(blocking) 107 | .queueSize(qSize) 108 | .senderWorkers(senderWorkers) 109 | .processorWorkers(processorWorkers) 110 | .enableAggregation(false) 111 | .build(); 112 | this.server = new DummyLowMemStatsDServer(port); 113 | 114 | this.executor = Executors.newFixedThreadPool(senderWorkers); 115 | this.running = new AtomicBoolean(true); 116 | 117 | } 118 | 119 | /** 120 | * Run with -Dtest_perf=true if you wish the performance tests defined here to run 121 | */ 122 | @Before 123 | public void shouldRun() { 124 | boolean run = false; 125 | try { 126 | run = (System.getProperty("test_perf").compareToIgnoreCase("true") == 0); 127 | } catch (Exception ex) { 128 | // NADA 129 | } 130 | Assume.assumeTrue(run); 131 | } 132 | 133 | @After 134 | public void stop() throws Exception { 135 | client.stop(); 136 | server.close(); 137 | } 138 | 139 | @Test 140 | public void perfTest() throws Exception { 141 | 142 | for(int i=0 ; i < this.testWorkers ; i++) { 143 | executor.submit(new Runnable() { 144 | public void run() { 145 | while (running.get()) { 146 | client.count("mycount", 1); 147 | } 148 | } 149 | }); 150 | } 151 | 152 | Thread.sleep(TimeUnit.SECONDS.toMillis(this.duration)); 153 | running.set(false); 154 | 155 | executor.shutdown(); 156 | executor.awaitTermination(1, TimeUnit.SECONDS); 157 | 158 | assertNotEquals(0, server.getMessageCount()); 159 | log.info("Messages at server: " + server.getMessageCount() + " packets: " + server.getPacketsReceived()); 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/test/java/com/timgroup/statsd/NonBlockingStatsDClientPerfTest.java: -------------------------------------------------------------------------------- 1 | package com.timgroup.statsd; 2 | 3 | 4 | import java.io.IOException; 5 | import java.net.SocketException; 6 | import java.util.concurrent.ExecutorService; 7 | import java.util.concurrent.Executors; 8 | import java.util.concurrent.TimeUnit; 9 | import java.util.logging.Logger; 10 | import java.util.Random; 11 | import org.junit.After; 12 | import org.junit.AfterClass; 13 | import org.junit.Before; 14 | import org.junit.BeforeClass; 15 | import org.junit.Test; 16 | 17 | import static org.junit.Assert.assertEquals; 18 | 19 | public final class NonBlockingStatsDClientPerfTest { 20 | 21 | 22 | private static final int STATSD_SERVER_PORT = 17255; 23 | private static final Random RAND = new Random(); 24 | private static final NonBlockingStatsDClient client = new NonBlockingStatsDClientBuilder().prefix("my.prefix") 25 | .hostname("localhost") 26 | .port(STATSD_SERVER_PORT) 27 | .blocking(true) // non-blocking processors will drop messages if the queue fills up 28 | .enableTelemetry(false) 29 | .enableAggregation(false) 30 | .originDetectionEnabled(false) 31 | .build(); 32 | 33 | private static final NonBlockingStatsDClient clientAggr = new NonBlockingStatsDClientBuilder().prefix("my.prefix.aggregated") 34 | .hostname("localhost") 35 | .port(STATSD_SERVER_PORT) 36 | .blocking(true) // non-blocking processors will drop messages if the queue fills up 37 | .enableTelemetry(false) 38 | .originDetectionEnabled(false) 39 | .build(); 40 | 41 | private ExecutorService executor; 42 | private static DummyStatsDServer server; 43 | 44 | private static Logger log = Logger.getLogger("NonBlockingStatsDClientPerfTest"); 45 | 46 | @BeforeClass 47 | public static void start() throws IOException { 48 | server = new UDPDummyStatsDServer(STATSD_SERVER_PORT); 49 | } 50 | 51 | @AfterClass 52 | public static void stop() throws Exception { 53 | 54 | client.stop(); 55 | clientAggr.stop(); 56 | server.close(); 57 | } 58 | 59 | @Before 60 | public void setup() throws Exception { 61 | executor = Executors.newFixedThreadPool(10); 62 | } 63 | 64 | @After 65 | public void clear() throws Exception { 66 | executor.shutdown(); 67 | executor.awaitTermination(20, TimeUnit.SECONDS); 68 | server.clear(); 69 | } 70 | 71 | @Test(timeout=30000) 72 | public void perfTest() throws Exception { 73 | 74 | int testSize = 10000; 75 | for(int i = 0; i < testSize; ++i) { 76 | executor.submit(new Runnable() { 77 | public void run() { 78 | client.count("mycount", 1); 79 | } 80 | }); 81 | 82 | } 83 | 84 | while(server.messagesReceived().size() < testSize) { 85 | try { 86 | Thread.sleep(50); 87 | } catch (InterruptedException ex) {} 88 | } 89 | 90 | assertEquals(testSize, server.messagesReceived().size()); 91 | } 92 | 93 | @Test(timeout=30000) 94 | public void perfAggregatedTest() throws Exception { 95 | 96 | int expectedSize = 1; 97 | long start = System.currentTimeMillis(); 98 | 99 | while(System.currentTimeMillis() - start < clientAggr.statsDProcessor.getAggregator().getFlushInterval() - 1) { 100 | clientAggr.count("myaggrcount", 1); 101 | Thread.sleep(50); 102 | } 103 | 104 | while(server.messagesReceived().size() < expectedSize) { 105 | try { 106 | Thread.sleep(50); 107 | } catch (InterruptedException ex) {} 108 | } 109 | 110 | assertEquals(expectedSize, server.messagesReceived().size()); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/test/java/com/timgroup/statsd/RecordingErrorHandler.java: -------------------------------------------------------------------------------- 1 | package com.timgroup.statsd; 2 | 3 | import java.util.ArrayList; 4 | import java.util.concurrent.ConcurrentLinkedQueue; 5 | import java.util.List; 6 | import java.util.Queue; 7 | 8 | 9 | /** 10 | * @author Taylor Schilling 11 | */ 12 | public class RecordingErrorHandler implements StatsDClientErrorHandler { 13 | private final Queue exceptions = new ConcurrentLinkedQueue<>(); 14 | 15 | @Override 16 | public void handle(final Exception exception) { 17 | exceptions.add(exception); 18 | } 19 | 20 | public List getExceptions() { 21 | return new ArrayList(exceptions); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/test/java/com/timgroup/statsd/StatsDAggregatorTest.java: -------------------------------------------------------------------------------- 1 | package com.timgroup.statsd; 2 | 3 | import org.junit.After; 4 | import org.junit.AfterClass; 5 | import org.junit.Assume; 6 | import org.junit.BeforeClass; 7 | import org.junit.FixMethodOrder; 8 | import org.junit.Rule; 9 | import org.junit.Test; 10 | import org.junit.contrib.java.lang.system.EnvironmentVariables; 11 | import org.junit.runners.MethodSorters; 12 | 13 | import java.util.Iterator; 14 | import java.util.Map; 15 | import java.util.Queue; 16 | import java.util.concurrent.ConcurrentLinkedQueue; 17 | import java.util.concurrent.ExecutorService; 18 | import java.util.concurrent.Executors; 19 | import java.util.concurrent.ThreadFactory; 20 | import java.util.concurrent.atomic.AtomicInteger; 21 | import java.util.logging.Logger; 22 | 23 | import static org.junit.Assert.assertEquals; 24 | import static org.junit.Assert.assertTrue; 25 | 26 | @FixMethodOrder(MethodSorters.NAME_ASCENDING) 27 | public class StatsDAggregatorTest { 28 | 29 | private static final String TEST_NAME = "StatsDAggregatorTest"; 30 | private static final int STATSD_SERVER_PORT = 17254; 31 | private static DummyStatsDServer server; 32 | private static FakeProcessor fakeProcessor; 33 | 34 | private static Logger log = Logger.getLogger(TEST_NAME); 35 | 36 | private static final ExecutorService executor = Executors.newFixedThreadPool(2, new ThreadFactory() { 37 | final ThreadFactory delegate = Executors.defaultThreadFactory(); 38 | @Override public Thread newThread(final Runnable runnable) { 39 | final Thread result = delegate.newThread(runnable); 40 | result.setName(TEST_NAME + result.getName()); 41 | result.setDaemon(true); 42 | return result; 43 | } 44 | }); 45 | 46 | @Rule 47 | public final EnvironmentVariables environmentVariables = new EnvironmentVariables(); 48 | 49 | private static final StatsDClientErrorHandler NO_OP_HANDLER = new StatsDClientErrorHandler() { 50 | @Override public void handle(final Exception ex) { /* No-op */ } 51 | }; 52 | 53 | public static class FakeMessage extends NumericMessage { 54 | protected FakeMessage(String aspect, Message.Type type, T value) { 55 | super(aspect, type, value, null); 56 | } 57 | 58 | @Override 59 | protected boolean writeTo(StringBuilder builder, int capacity, String containerID) { 60 | return false; 61 | } 62 | } 63 | 64 | public static class FakeAlphaMessage extends AlphaNumericMessage { 65 | protected FakeAlphaMessage(String aspect, Message.Type type, String value) { 66 | super(aspect, type, value, null); 67 | } 68 | 69 | @Override 70 | protected boolean writeTo(StringBuilder builder, int capacity, String containerID) { 71 | return false; 72 | } 73 | } 74 | 75 | 76 | // fakeProcessor store messages from the telemetry only 77 | public static class FakeProcessor extends StatsDProcessor { 78 | 79 | private final Queue messages; 80 | private final AtomicInteger messageSent = new AtomicInteger(0); 81 | private final AtomicInteger messageAggregated = new AtomicInteger(0); 82 | 83 | FakeProcessor(final StatsDClientErrorHandler handler) throws Exception { 84 | super(0, handler, 0, 1, 1, 0, 0, new StatsDThreadFactory(), null); 85 | this.messages = new ConcurrentLinkedQueue<>(); 86 | } 87 | 88 | 89 | private class FakeProcessingTask extends StatsDProcessor.ProcessingTask { 90 | @Override 91 | protected void processLoop() { 92 | 93 | while (!shutdown) { 94 | final Message message = messages.poll(); 95 | if (message == null) { 96 | 97 | try{ 98 | Thread.sleep(50L); 99 | } catch (InterruptedException e) {} 100 | 101 | continue; 102 | } 103 | 104 | if (aggregator.aggregateMessage(message)) { 105 | messageAggregated.incrementAndGet(); 106 | continue; 107 | } 108 | 109 | // otherwise just "send" it 110 | messageSent.incrementAndGet(); 111 | } 112 | } 113 | 114 | @Override 115 | Message getMessage() { return null; } 116 | 117 | @Override 118 | boolean haveMessages() { return false; } 119 | } 120 | 121 | @Override 122 | protected StatsDProcessor.ProcessingTask createProcessingTask() { 123 | return new FakeProcessingTask(); 124 | } 125 | 126 | @Override 127 | public boolean send(final Message msg) { 128 | messages.offer(msg); 129 | return true; 130 | } 131 | 132 | public Queue getMessages() { 133 | return messages; 134 | } 135 | 136 | public void clear() { 137 | try { 138 | messages.clear(); 139 | highPrioMessages.clear(); 140 | } catch (Exception e) {} 141 | } 142 | } 143 | 144 | @BeforeClass 145 | public static void start() throws Exception { 146 | fakeProcessor = new FakeProcessor(NO_OP_HANDLER); 147 | 148 | // set telemetry 149 | Telemetry telemetry = new Telemetry.Builder() 150 | .processor(fakeProcessor) 151 | .build(); 152 | fakeProcessor.setTelemetry(telemetry); 153 | 154 | // 15s flush period should be enough for all tests to be done - flushes will be manual 155 | StatsDAggregator aggregator = new StatsDAggregator(fakeProcessor, StatsDAggregator.DEFAULT_SHARDS, 3000L); 156 | fakeProcessor.aggregator = aggregator; 157 | fakeProcessor.startWorkers("StatsD-Test-"); 158 | } 159 | 160 | @AfterClass 161 | public static void stop() { 162 | try { 163 | fakeProcessor.shutdown(false); 164 | } catch (InterruptedException e) { 165 | return; 166 | } 167 | } 168 | 169 | @After 170 | public void clear() { 171 | // we should probably clear all queues 172 | fakeProcessor.clear(); 173 | } 174 | 175 | public void waitForQueueSize(Queue queue, int size) { 176 | 177 | // Wait for the flush to happen 178 | while (queue.size() != size) { 179 | try { 180 | Thread.sleep(1000L); 181 | } catch (InterruptedException e) {} 182 | } 183 | } 184 | 185 | @Test(timeout = 2000L) 186 | public void aggregate_messages() throws Exception { 187 | 188 | for(int i=0 ; i<10 ; i++) { 189 | fakeProcessor.send(new FakeMessage("some.gauge", Message.Type.GAUGE, 1)); 190 | fakeProcessor.send(new FakeMessage("some.count", Message.Type.COUNT, 1)); 191 | fakeProcessor.send(new FakeMessage("some.histogram", Message.Type.HISTOGRAM, 1)); 192 | fakeProcessor.send(new FakeMessage("some.distribution", Message.Type.DISTRIBUTION, 1)); 193 | fakeProcessor.send(new FakeAlphaMessage("some.set", Message.Type.SET, "value")); 194 | } 195 | 196 | waitForQueueSize(fakeProcessor.messages, 0); 197 | 198 | // 10 gauges, 10 counts, 10 sets 199 | assertEquals(30, fakeProcessor.messageAggregated.get()); 200 | // 10 histogram, 10 distribution 201 | assertEquals(20, fakeProcessor.messageSent.get()); 202 | 203 | // wait for aggregator flush... 204 | fakeProcessor.aggregator.flush(); 205 | 206 | // 2 metrics (gauge, count) + 1 set, so 3 aggregates 207 | assertEquals(3, fakeProcessor.highPrioMessages.size()); 208 | 209 | } 210 | 211 | @Test(timeout = 2000L) 212 | public void aggregation_sharding() throws Exception { 213 | final int iterations = 10; 214 | 215 | for(int i=0 ; i gauge = new FakeMessage("some.gauge", Message.Type.GAUGE, 1) { 218 | @Override 219 | public int hashCode() { 220 | return hash; 221 | } 222 | }; 223 | fakeProcessor.send(gauge); 224 | } 225 | 226 | waitForQueueSize(fakeProcessor.messages, 0); 227 | 228 | for (int i=0 ; i map = fakeProcessor.aggregator.aggregateMetrics.get(i); 230 | synchronized (map) { 231 | Iterator> iter = map.entrySet().iterator(); 232 | int count = 0; 233 | while (iter.hasNext()) { 234 | count++; 235 | iter.next(); 236 | } 237 | 238 | // sharding should be balanced 239 | assertEquals(iterations, count); 240 | } 241 | } 242 | } 243 | 244 | @Test(timeout = 5000L) 245 | public void aggregation_flushing() throws Exception { 246 | 247 | for(int i=0 ; i<10 ; i++) { 248 | fakeProcessor.send(new FakeMessage<>("some.gauge", Message.Type.GAUGE, i)); 249 | } 250 | 251 | // processor should auto-flush within 2s 252 | waitForQueueSize(fakeProcessor.highPrioMessages, 1); 253 | 254 | //aggregated message should take last value - 10 255 | NumericMessage message = (NumericMessage)fakeProcessor.highPrioMessages.element(); 256 | assertEquals(9, message.getValue()); 257 | 258 | } 259 | 260 | @Test(timeout = 5000L) 261 | public void test_aggregation_degradation_to_treenodes() { 262 | fakeProcessor.aggregator.flush(); 263 | fakeProcessor.clear(); 264 | // these counts have been determined to trigger treeification of the message aggregates 265 | int numMessages = 10000; 266 | int numTags = 100; 267 | Assume.assumeTrue("assertions depend on divisibility",numMessages % numTags == 0); 268 | String[][] tags = new String[numTags][]; 269 | for (int i = 0; i < numTags; i++) { 270 | tags[i] = new String[] {String.valueOf(i)}; 271 | } 272 | for (int i = 0; i < numMessages; i++) { 273 | fakeProcessor.send(new NumericMessage("some.counter", Message.Type.COUNT, 1, tags[i % numTags]) { 274 | @Override 275 | boolean writeTo(StringBuilder builder, int capacity, String containerID) { 276 | return false; 277 | } 278 | 279 | @Override 280 | public int hashCode() { 281 | // provoke collisions 282 | return 0; 283 | } 284 | }); 285 | } 286 | waitForQueueSize(fakeProcessor.messages, 0); 287 | fakeProcessor.aggregator.flush(); 288 | assertEquals(numTags, fakeProcessor.highPrioMessages.size()); 289 | for (int i = 0; i < numTags; i++) { 290 | Message message = fakeProcessor.highPrioMessages.poll(); 291 | assertTrue(message instanceof NumericMessage); 292 | assertEquals(numMessages / numTags, ((NumericMessage) message).value.intValue()); 293 | } 294 | } 295 | } 296 | -------------------------------------------------------------------------------- /src/test/java/com/timgroup/statsd/StatsDTestMessage.java: -------------------------------------------------------------------------------- 1 | package com.timgroup.statsd; 2 | 3 | class StatsDTestMessage extends NumericMessage { 4 | final double sampleRate; // NaN for none 5 | 6 | protected StatsDTestMessage(String aspect, Message.Type type, T value, double sampleRate, String[] tags) { 7 | super(aspect, type, value, tags); 8 | this.sampleRate = sampleRate; 9 | } 10 | 11 | @Override 12 | public final boolean writeTo(StringBuilder builder, int capacity, String containerID) { 13 | builder.append("test.").append(aspect).append(':'); 14 | writeValue(builder); 15 | builder.append('|').append(type); 16 | if (!Double.isNaN(sampleRate)) { 17 | builder.append('|').append('@').append(NonBlockingStatsDClient.format(NonBlockingStatsDClient.SAMPLE_RATE_FORMATTER, sampleRate)); 18 | } 19 | NonBlockingStatsDClient.tagString(this.tags, "", builder); 20 | if (containerID != null && !containerID.isEmpty()) { 21 | builder.append("|c:").append(containerID); 22 | } 23 | 24 | builder.append('\n'); 25 | return false; 26 | } 27 | 28 | protected void writeValue(StringBuilder builder) { 29 | builder.append(NonBlockingStatsDClient.format(NonBlockingStatsDClient.NUMBER_FORMATTER, this.value)); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/test/java/com/timgroup/statsd/TestHelpers.java: -------------------------------------------------------------------------------- 1 | package com.timgroup.statsd; 2 | 3 | public class TestHelpers 4 | { 5 | static boolean isLinux() { 6 | return System.getProperty("os.name").toLowerCase().contains("linux"); 7 | } 8 | 9 | static boolean isMac() { 10 | return System.getProperty("os.name").toLowerCase().contains("mac"); 11 | } 12 | 13 | // Check if jnr.unixsocket is on the classpath. 14 | static boolean isJnrAvailable() { 15 | try { 16 | Class.forName("jnr.unixsocket.UnixDatagramChannel"); 17 | return true; 18 | } catch (ClassNotFoundException e) { 19 | return false; 20 | } 21 | } 22 | 23 | static boolean isUdsAvailable() { 24 | return (isLinux() || isMac()) && isJnrAvailable(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/test/java/com/timgroup/statsd/UDPDummyStatsDServer.java: -------------------------------------------------------------------------------- 1 | package com.timgroup.statsd; 2 | 3 | import java.io.IOException; 4 | import java.net.InetSocketAddress; 5 | import java.nio.ByteBuffer; 6 | import java.nio.channels.DatagramChannel; 7 | 8 | public class UDPDummyStatsDServer extends DummyStatsDServer { 9 | private final DatagramChannel server; 10 | 11 | public UDPDummyStatsDServer(int port) throws IOException { 12 | server = DatagramChannel.open(); 13 | server.bind(new InetSocketAddress(port)); 14 | this.listen(); 15 | } 16 | 17 | @Override 18 | protected boolean isOpen() { 19 | return server.isOpen(); 20 | } 21 | 22 | @Override 23 | protected void receive(ByteBuffer packet) throws IOException { 24 | server.receive(packet); 25 | } 26 | 27 | public void close() throws IOException { 28 | try { 29 | server.close(); 30 | } catch (Exception e) { 31 | //ignore 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/test/java/com/timgroup/statsd/UnixDatagramSocketDummyStatsDServer.java: -------------------------------------------------------------------------------- 1 | package com.timgroup.statsd; 2 | 3 | import java.io.IOException; 4 | import java.nio.ByteBuffer; 5 | import java.nio.channels.DatagramChannel; 6 | import jnr.unixsocket.UnixDatagramChannel; 7 | import jnr.unixsocket.UnixSocketAddress; 8 | 9 | import static com.timgroup.statsd.NonBlockingStatsDClient.DEFAULT_UDS_MAX_PACKET_SIZE_BYTES; 10 | 11 | public class UnixDatagramSocketDummyStatsDServer extends DummyStatsDServer { 12 | private final DatagramChannel server; 13 | UnixSocketAddress addr; 14 | 15 | public UnixDatagramSocketDummyStatsDServer(String socketPath) throws IOException { 16 | server = UnixDatagramChannel.open(); 17 | addr = new UnixSocketAddress(socketPath); 18 | server.bind(addr); 19 | this.listen(); 20 | } 21 | 22 | @Override 23 | protected boolean isOpen() { 24 | return server.isOpen(); 25 | } 26 | 27 | protected void receive(ByteBuffer packet) throws IOException { 28 | server.receive(packet); 29 | } 30 | 31 | public void close() throws IOException { 32 | if (!server.isOpen()) { 33 | return; 34 | } 35 | thread.interrupt(); 36 | // JNR doesn't interrupt syscalls when a thread is interrupted, so we send a dummy message 37 | // to wake the thread up. 38 | int sent = server.send(ByteBuffer.wrap(new byte[]{1}), addr); 39 | if (sent == 0) { 40 | throw new IOException("failed to send wake up call to the server thread"); 41 | } 42 | server.close(); 43 | try { 44 | thread.join(); 45 | } catch (Exception e) { 46 | throw new IOException(e); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/test/java/com/timgroup/statsd/UnixSocketTest.java: -------------------------------------------------------------------------------- 1 | package com.timgroup.statsd; 2 | 3 | import java.util.logging.Logger; 4 | import org.junit.After; 5 | import org.junit.Assume; 6 | import org.junit.Before; 7 | import org.junit.BeforeClass; 8 | import org.junit.Test; 9 | import java.io.IOException; 10 | import java.io.File; 11 | import java.nio.file.Files; 12 | 13 | import static org.hamcrest.MatcherAssert.assertThat; 14 | import static org.hamcrest.Matchers.contains; 15 | import static org.hamcrest.Matchers.nullValue; 16 | import static org.hamcrest.Matchers.containsString; 17 | import static org.hamcrest.Matchers.hasItem; 18 | import static org.junit.Assert.assertEquals; 19 | 20 | public class UnixSocketTest implements StatsDClientErrorHandler { 21 | private static File tmpFolder; 22 | private static NonBlockingStatsDClient client; 23 | private static NonBlockingStatsDClient clientAggregate; 24 | private static DummyStatsDServer server; 25 | private static File socketFile; 26 | 27 | private volatile Exception lastException = new Exception(); 28 | 29 | private static Logger log = Logger.getLogger(StatsDClientErrorHandler.class.getName()); 30 | 31 | public synchronized void handle(Exception exception) { 32 | log.info("Got exception: " + exception); 33 | lastException = exception; 34 | } 35 | 36 | @BeforeClass 37 | public static void supportedOnly() throws IOException { 38 | Assume.assumeTrue(TestHelpers.isUdsAvailable()); 39 | } 40 | 41 | @Before 42 | public void start() throws IOException { 43 | tmpFolder = Files.createTempDirectory(System.getProperty("java-dsd-test")).toFile(); 44 | tmpFolder.deleteOnExit(); 45 | socketFile = new File(tmpFolder, "socket.sock"); 46 | socketFile.deleteOnExit(); 47 | 48 | server = new UnixDatagramSocketDummyStatsDServer(socketFile.toString()); 49 | 50 | client = new NonBlockingStatsDClientBuilder().prefix("my.prefix") 51 | .hostname(socketFile.toString()) 52 | .port(0) 53 | .queueSize(1) 54 | .timeout(1) // non-zero timeout to ensure exception triggered if socket buffer full. 55 | .socketBufferSize(1024 * 1024) 56 | .enableAggregation(false) 57 | .errorHandler(this) 58 | .originDetectionEnabled(false) 59 | .build(); 60 | 61 | clientAggregate = new NonBlockingStatsDClientBuilder().prefix("my.prefix") 62 | .hostname(socketFile.toString()) 63 | .port(0) 64 | .queueSize(1) 65 | .timeout(1) // non-zero timeout to ensure exception triggered if socket buffer full. 66 | .socketBufferSize(1024 * 1024) 67 | .enableAggregation(false) 68 | .errorHandler(this) 69 | .originDetectionEnabled(false) 70 | .build(); 71 | } 72 | 73 | @After 74 | public void stop() throws Exception { 75 | client.stop(); 76 | clientAggregate.stop(); 77 | server.close(); 78 | } 79 | 80 | @Test 81 | public void assert_default_uds_size() throws Exception { 82 | assertEquals(client.statsDProcessor.bufferPool.getBufferSize(), NonBlockingStatsDClient.DEFAULT_UDS_MAX_PACKET_SIZE_BYTES); 83 | } 84 | 85 | @Test(timeout = 5000L) 86 | public void sends_to_statsd() throws Exception { 87 | for(long i = 0; i < 5 ; i++) { 88 | client.gauge("mycount", i); 89 | server.waitForMessage(); 90 | String expected = String.format("my.prefix.mycount:%d|g", i); 91 | assertThat(server.messagesReceived(), contains(expected)); 92 | server.clear(); 93 | } 94 | assertThat(lastException.getMessage(), nullValue()); 95 | } 96 | 97 | @Test(timeout = 10000L) 98 | public void resist_dsd_restart() throws Exception { 99 | // Send one metric, check that it works. 100 | client.gauge("mycount", 10); 101 | server.waitForMessage(); 102 | assertThat(server.messagesReceived(), contains("my.prefix.mycount:10|g")); 103 | server.clear(); 104 | assertThat(lastException.getMessage(), nullValue()); 105 | 106 | // Close the server, client should throw an IOException 107 | server.close(); 108 | 109 | client.gauge("mycount", 20); 110 | while(lastException.getMessage() == null) { 111 | Thread.sleep(10); 112 | } 113 | assertThat(lastException.getMessage(), containsString("Connection refused")); 114 | 115 | // Delete the socket file, client should throw an IOException 116 | socketFile.delete(); 117 | lastException = new Exception(); 118 | 119 | client.gauge("mycount", 21); 120 | while(lastException.getMessage() == null) { 121 | Thread.sleep(10); 122 | } 123 | assertThat(lastException.getMessage(), containsString("No such file or directory")); 124 | 125 | // Re-open the server, next send should work OK 126 | lastException = new Exception(); 127 | DummyStatsDServer server2 = new UnixDatagramSocketDummyStatsDServer(socketFile.toString()); 128 | 129 | client.gauge("mycount", 30); 130 | 131 | server2.waitForMessage(); 132 | assertThat(server2.messagesReceived(), hasItem("my.prefix.mycount:30|g")); 133 | 134 | server2.clear(); 135 | assertThat(lastException.getMessage(), nullValue()); 136 | server2.close(); 137 | } 138 | 139 | @Test(timeout = 10000L) 140 | public void resist_dsd_timeout() throws Exception { 141 | client.gauge("mycount", 10); 142 | server.waitForMessage(); 143 | assertThat(server.messagesReceived(), contains("my.prefix.mycount:10|g")); 144 | server.clear(); 145 | assertThat(lastException.getMessage(), nullValue()); 146 | 147 | // Freeze the server to simulate dsd being overwhelmed 148 | server.freeze(); 149 | while (lastException.getMessage() == null) { 150 | client.gauge("mycount", 20); 151 | Thread.sleep(10); // We need to fill the buffer, setting a shorter sleep 152 | } 153 | String excMessage = TestHelpers.isLinux() ? "Resource temporarily unavailable" : "No buffer space available"; 154 | assertThat(lastException.getMessage(), containsString(excMessage)); 155 | 156 | // Make sure we recover after we resume listening 157 | server.clear(); 158 | server.unfreeze(); 159 | 160 | // Now make sure we can receive gauges with 30 161 | while (!server.messagesReceived().contains("my.prefix.mycount:30|g")) { 162 | server.clear(); 163 | client.gauge("mycount", 30); 164 | server.waitForMessage(); 165 | } 166 | assertThat(server.messagesReceived(), hasItem("my.prefix.mycount:30|g")); 167 | server.clear(); 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /src/test/java/com/timgroup/statsd/UnixStreamSocketDummyStatsDServer.java: -------------------------------------------------------------------------------- 1 | package com.timgroup.statsd; 2 | 3 | import java.io.IOException; 4 | import java.nio.Buffer; 5 | import java.nio.ByteBuffer; 6 | import java.nio.ByteOrder; 7 | import java.nio.channels.SocketChannel; 8 | import java.util.concurrent.ConcurrentLinkedQueue; 9 | import java.util.logging.Logger; 10 | import jnr.unixsocket.UnixServerSocketChannel; 11 | import jnr.unixsocket.UnixSocketAddress; 12 | import jnr.unixsocket.UnixSocketChannel; 13 | 14 | import static com.timgroup.statsd.NonBlockingStatsDClient.DEFAULT_UDS_MAX_PACKET_SIZE_BYTES; 15 | 16 | public class UnixStreamSocketDummyStatsDServer extends DummyStatsDServer { 17 | private final UnixServerSocketChannel server; 18 | private final ConcurrentLinkedQueue channels = new ConcurrentLinkedQueue<>(); 19 | 20 | private final Logger logger = Logger.getLogger(UnixStreamSocketDummyStatsDServer.class.getName()); 21 | 22 | public UnixStreamSocketDummyStatsDServer(String socketPath) throws IOException { 23 | server = UnixServerSocketChannel.open(); 24 | server.configureBlocking(true); 25 | server.socket().bind(new UnixSocketAddress(socketPath)); 26 | this.listen(); 27 | } 28 | 29 | @Override 30 | protected boolean isOpen() { 31 | return server.isOpen(); 32 | } 33 | 34 | @Override 35 | protected void receive(ByteBuffer packet) throws IOException { 36 | // This is unused because we re-implement listen() to fit our needs 37 | } 38 | 39 | @Override 40 | protected void listen() { 41 | logger.info("Listening on " + server.getLocalSocketAddress()); 42 | Thread thread = new Thread(new Runnable() { 43 | @Override 44 | public void run() { 45 | while(isOpen()) { 46 | if (sleepIfFrozen()) { 47 | continue; 48 | } 49 | try { 50 | logger.info("Waiting for connection"); 51 | UnixSocketChannel clientChannel = server.accept(); 52 | if (clientChannel != null) { 53 | clientChannel.configureBlocking(true); 54 | try { 55 | logger.info("Accepted connection from " + clientChannel.getRemoteSocketAddress()); 56 | } catch (Exception e) { 57 | logger.warning("Failed to get remote socket address"); 58 | } 59 | channels.add(clientChannel); 60 | readChannel(clientChannel); 61 | } 62 | } catch (IOException e) { 63 | } 64 | } 65 | } 66 | }); 67 | thread.setDaemon(true); 68 | thread.start(); 69 | } 70 | 71 | public void readChannel(final UnixSocketChannel clientChannel) { 72 | logger.info("Reading from " + clientChannel); 73 | Thread thread = new Thread(new Runnable() { 74 | @Override 75 | public void run() { 76 | final ByteBuffer packet = ByteBuffer.allocate(DEFAULT_UDS_MAX_PACKET_SIZE_BYTES); 77 | 78 | while(clientChannel.isOpen()) { 79 | if (sleepIfFrozen()) { 80 | continue; 81 | } 82 | ((Buffer)packet).clear(); // Cast necessary to handle Java9 covariant return types 83 | // see: https://jira.mongodb.org/browse/JAVA-2559 for ref. 84 | if (readPacket(clientChannel, packet)) { 85 | handlePacket(packet); 86 | } else { 87 | try { 88 | clientChannel.close(); 89 | } catch (IOException e) { 90 | logger.warning("Failed to close channel: " + e); 91 | } 92 | } 93 | 94 | } 95 | logger.info("Disconnected from " + clientChannel); 96 | } 97 | }); 98 | thread.setDaemon(true); 99 | thread.start(); 100 | } 101 | 102 | private boolean readPacket(SocketChannel channel, ByteBuffer packet) { 103 | try { 104 | ByteBuffer delimiterBuffer = ByteBuffer.allocate(Integer.SIZE / Byte.SIZE).order(ByteOrder.LITTLE_ENDIAN); 105 | 106 | int read = channel.read(delimiterBuffer); 107 | 108 | delimiterBuffer.flip(); 109 | if (read <= 0) { 110 | // There was nothing to read 111 | return false; 112 | } 113 | 114 | int packetSize = delimiterBuffer.getInt(); 115 | if (packetSize > packet.capacity()) { 116 | throw new IOException("Packet size too large"); 117 | } 118 | 119 | packet.limit(packetSize); 120 | while (packet.hasRemaining() && channel.isConnected()) { 121 | channel.read(packet); 122 | } 123 | return true; 124 | } catch (IOException e) { 125 | return false; 126 | } 127 | } 128 | 129 | public void close() throws IOException { 130 | try { 131 | server.close(); 132 | for (UnixSocketChannel channel : channels) { 133 | channel.close(); 134 | } 135 | } catch (Exception e) { 136 | //ignore 137 | } 138 | } 139 | 140 | } 141 | -------------------------------------------------------------------------------- /src/test/java/com/timgroup/statsd/UnixStreamSocketTest.java: -------------------------------------------------------------------------------- 1 | package com.timgroup.statsd; 2 | 3 | import java.util.logging.Logger; 4 | import org.junit.After; 5 | import org.junit.Assume; 6 | import org.junit.Before; 7 | import org.junit.BeforeClass; 8 | import org.junit.Test; 9 | import java.io.IOException; 10 | import java.io.File; 11 | import java.nio.file.Files; 12 | 13 | import static org.hamcrest.CoreMatchers.anyOf; 14 | import static org.hamcrest.MatcherAssert.assertThat; 15 | import static org.hamcrest.Matchers.contains; 16 | import static org.hamcrest.Matchers.nullValue; 17 | import static org.hamcrest.Matchers.containsString; 18 | import static org.hamcrest.Matchers.hasItem; 19 | import static org.junit.Assert.assertEquals; 20 | 21 | public class UnixStreamSocketTest implements StatsDClientErrorHandler { 22 | private static File tmpFolder; 23 | private static NonBlockingStatsDClient client; 24 | private static NonBlockingStatsDClient clientAggregate; 25 | private static DummyStatsDServer server; 26 | private static File socketFile; 27 | 28 | private volatile Exception lastException = new Exception(); 29 | 30 | private static Logger log = Logger.getLogger(StatsDClientErrorHandler.class.getName()); 31 | 32 | public synchronized void handle(Exception exception) { 33 | log.info("Got exception: " + exception); 34 | lastException = exception; 35 | } 36 | 37 | @BeforeClass 38 | public static void supportedOnly() throws IOException { 39 | Assume.assumeTrue(TestHelpers.isUdsAvailable()); 40 | } 41 | 42 | @Before 43 | public void start() throws IOException { 44 | tmpFolder = Files.createTempDirectory(System.getProperty("java-dsd-test")).toFile(); 45 | tmpFolder.deleteOnExit(); 46 | socketFile = new File(tmpFolder, "socket.sock"); 47 | socketFile.deleteOnExit(); 48 | 49 | server = new UnixStreamSocketDummyStatsDServer(socketFile.toString()); 50 | 51 | client = new NonBlockingStatsDClientBuilder().prefix("my.prefix") 52 | .address("unixstream://" + socketFile.getPath()) 53 | .port(0) 54 | .queueSize(1) 55 | .timeout(500) // non-zero timeout to ensure exception triggered if socket buffer full. 56 | .connectionTimeout(500) 57 | .socketBufferSize(1024 * 1024) 58 | .enableAggregation(false) 59 | .errorHandler(this) 60 | .originDetectionEnabled(false) 61 | .build(); 62 | 63 | clientAggregate = new NonBlockingStatsDClientBuilder().prefix("my.prefix") 64 | .address("unixstream://" + socketFile.getPath()) 65 | .port(0) 66 | .queueSize(1) 67 | .timeout(500) // non-zero timeout to ensure exception triggered if socket buffer full. 68 | .connectionTimeout(500) 69 | .socketBufferSize(1024 * 1024) 70 | .enableAggregation(false) 71 | .errorHandler(this) 72 | .originDetectionEnabled(false) 73 | .build(); 74 | } 75 | 76 | @After 77 | public void stop() throws Exception { 78 | client.stop(); 79 | clientAggregate.stop(); 80 | server.close(); 81 | } 82 | 83 | @Test 84 | public void assert_default_uds_size() throws Exception { 85 | assertEquals(client.statsDProcessor.bufferPool.getBufferSize(), NonBlockingStatsDClient.DEFAULT_UDS_MAX_PACKET_SIZE_BYTES); 86 | } 87 | 88 | @Test(timeout = 5000L) 89 | public void sends_to_statsd() throws Exception { 90 | for(long i = 0; i < 5 ; i++) { 91 | client.gauge("mycount", i); 92 | server.waitForMessage(); 93 | String expected = String.format("my.prefix.mycount:%d|g", i); 94 | assertThat(server.messagesReceived(), contains(expected)); 95 | server.clear(); 96 | } 97 | assertThat(lastException.getMessage(), nullValue()); 98 | } 99 | 100 | @Test(timeout = 10000L) 101 | public void resist_dsd_restart() throws Exception { 102 | // Send one metric, check that it works. 103 | client.gauge("mycount", 10); 104 | server.waitForMessage(); 105 | assertThat(server.messagesReceived(), contains("my.prefix.mycount:10|g")); 106 | server.clear(); 107 | assertThat(lastException.getMessage(), nullValue()); 108 | 109 | // Close the server, client should throw an IOException 110 | server.close(); 111 | 112 | client.gauge("mycount", 20); 113 | while(lastException.getMessage() == null) { 114 | Thread.sleep(10); 115 | } 116 | // Depending on the state of the client at that point we might get different messages. 117 | assertThat(lastException.getMessage(), anyOf(containsString("Connection refused"), containsString("Broken pipe"))); 118 | 119 | // Delete the socket file, client should throw an IOException 120 | lastException = new Exception(); 121 | socketFile.delete(); 122 | 123 | client.gauge("mycount", 21); 124 | while(lastException.getMessage() == null) { 125 | Thread.sleep(10); 126 | } 127 | assertThat(lastException.getMessage(), containsString("No such file or directory")); 128 | 129 | // Re-open the server, next send should work OK 130 | DummyStatsDServer server2; 131 | server2 = new UnixStreamSocketDummyStatsDServer(socketFile.toString()); 132 | 133 | lastException = new Exception(); 134 | 135 | client.gauge("mycount", 30); 136 | server2.waitForMessage(); 137 | assertThat(server2.messagesReceived(), hasItem("my.prefix.mycount:30|g")); 138 | 139 | server2.clear(); 140 | assertThat(lastException.getMessage(), nullValue()); 141 | server2.close(); 142 | } 143 | 144 | @Test(timeout = 10000L) 145 | public void resist_dsd_timeout() throws Exception { 146 | client.gauge("mycount", 10); 147 | server.waitForMessage(); 148 | assertThat(server.messagesReceived(), contains("my.prefix.mycount:10|g")); 149 | server.clear(); 150 | assertThat(lastException.getMessage(), nullValue()); 151 | 152 | // Freeze the server to simulate dsd being overwhelmed 153 | server.freeze(); 154 | 155 | while (lastException.getMessage() == null) { 156 | client.gauge("mycount", 20); 157 | 158 | } 159 | String excMessage = "Write timed out"; 160 | assertThat(lastException.getMessage(), containsString(excMessage)); 161 | 162 | // Make sure we recover after we resume listening 163 | server.clear(); 164 | server.unfreeze(); 165 | 166 | // Now make sure we can receive gauges with 30 167 | while (!server.messagesReceived().contains("my.prefix.mycount:30|g")) { 168 | server.clear(); 169 | client.gauge("mycount", 30); 170 | server.waitForMessage(); 171 | } 172 | assertThat(server.messagesReceived(), hasItem("my.prefix.mycount:30|g")); 173 | server.clear(); 174 | } 175 | 176 | @Test(timeout = 5000L) 177 | public void stream_uds_has_uds_buffer_size() throws Exception { 178 | final NonBlockingStatsDClient client = new NonBlockingStatsDClientBuilder() 179 | .prefix("my.prefix") 180 | .address("unixstream:///foo") 181 | .containerID("fake-container-id") 182 | .build(); 183 | 184 | assertEquals(client.statsDProcessor.bufferPool.getBufferSize(), NonBlockingStatsDClient.DEFAULT_UDS_MAX_PACKET_SIZE_BYTES); 185 | } 186 | 187 | @Test(timeout = 5000L) 188 | public void max_packet_size_override() throws Exception { 189 | final NonBlockingStatsDClient client = new NonBlockingStatsDClientBuilder() 190 | .prefix("my.prefix") 191 | .address("unixstream:///foo") 192 | .containerID("fake-container-id") 193 | .maxPacketSizeBytes(576) 194 | .build(); 195 | 196 | assertEquals(client.statsDProcessor.bufferPool.getBufferSize(), 576); 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /src/test/java/com/timgroup/statsd/Utf8Test.java: -------------------------------------------------------------------------------- 1 | package com.timgroup.statsd; 2 | 3 | import org.junit.Test; 4 | 5 | import java.nio.ByteBuffer; 6 | import java.nio.CharBuffer; 7 | import java.nio.charset.CharacterCodingException; 8 | import java.nio.charset.CharsetEncoder; 9 | import java.nio.charset.CodingErrorAction; 10 | import java.nio.charset.StandardCharsets; 11 | 12 | import static java.lang.Character.MIN_SURROGATE; 13 | import static org.hamcrest.MatcherAssert.assertThat; 14 | import static org.hamcrest.Matchers.equalTo; 15 | 16 | public class Utf8Test { 17 | 18 | @Test 19 | public void should_handle_malformed_inputs() throws CharacterCodingException { 20 | shouldHandleMalformedInput("foo" + MIN_SURROGATE + "bar"); 21 | shouldHandleMalformedInput("🍻☀️😎🏖️" + MIN_SURROGATE + "🍻☀️😎🏖️"); 22 | } 23 | 24 | private static void shouldHandleMalformedInput(String malformedInput) throws CharacterCodingException { 25 | CharsetEncoder utf8Encoder = StandardCharsets.UTF_8.newEncoder() 26 | .onMalformedInput(CodingErrorAction.REPLACE) 27 | .onUnmappableCharacter(CodingErrorAction.REPLACE); 28 | ByteBuffer encoded = utf8Encoder.encode(CharBuffer.wrap(malformedInput)); 29 | 30 | assertThat(Utf8.encodedLength(malformedInput), equalTo(encoded.limit())); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /style.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 76 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 101 | 102 | 103 | 105 | 106 | 107 | 108 | 110 | 111 | 112 | 113 | 115 | 116 | 117 | 118 | 119 | 120 | 122 | 123 | 124 | 125 | 127 | 128 | 129 | 130 | 132 | 133 | 134 | 135 | 137 | 139 | 141 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | -------------------------------------------------------------------------------- /vendor/buildlib/hamcrest-core-1.3.0RC2.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DataDog/java-dogstatsd-client/ae5c0f79127e57a0a0c0d9fcd7b371d5cf862afa/vendor/buildlib/hamcrest-core-1.3.0RC2.jar -------------------------------------------------------------------------------- /vendor/buildlib/hamcrest-library-1.3.0RC2.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DataDog/java-dogstatsd-client/ae5c0f79127e57a0a0c0d9fcd7b371d5cf862afa/vendor/buildlib/hamcrest-library-1.3.0RC2.jar -------------------------------------------------------------------------------- /vendor/buildlib/junit-dep-4.10.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DataDog/java-dogstatsd-client/ae5c0f79127e57a0a0c0d9fcd7b371d5cf862afa/vendor/buildlib/junit-dep-4.10.jar -------------------------------------------------------------------------------- /vendor/src/junit-dep-4.10-sources.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DataDog/java-dogstatsd-client/ae5c0f79127e57a0a0c0d9fcd7b371d5cf862afa/vendor/src/junit-dep-4.10-sources.jar --------------------------------------------------------------------------------