diff --git a/README.md b/README.md index a2993a73c..1af53fd22 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,104 @@ -spectator -========= -tbd +# Spectator + +Simple library for instrumenting code to record dimensional time series. + +## Requirements + +* Java 7 or higher. + +## Dependencies + +To instrument your code you need to depend on the api library. This provides the minimal interfaces +for you to code against and build test cases. The only dependency is slf4j. + +``` +com.netflix.spectator:spectator-api:latest.release +``` + +If running at Netflix with the standard platform, use the spectator-nflx library to automatically +setup and initalize with the correct bindings to report data to internal tools like Atlas and +Chronos. + +``` +com.netflix.spectator:spectator-nflx:latest.release +``` + +## Instrumenting Code + +Suppose we have a server and we want to keep track of: + +* Number of requests received with dimensions for breaking down by status code, country, and + the exception type if the request fails in an unexpected way. +* Latency for handling requests. +* Summary of the response sizes. +* Current number of active connections on the server. + +Here is some sample code that does that: + +```java +// The Spectator class provides a static lookup for the default registry +Server s = new Server(Spectator.registry()); + +public class Server { + private final ExtendedRegistry registry; + private final Id requestCountId; + private final Timer requestLatency; + private final DistributionSummary responseSizes; + + // We can pass in the registry to make unit testing easier + public Server(ExtendedRegistry registry) { + this.registry = registry; + + // Create a base id for the request count. The id will get refined with additional dimensions + // when we receive a request. + requestCountId = registry.createId("server.requestCount"); + + // Create a timer for tracking the latency. The reference can be held onto to avoid + // additional lookup cost in critical paths. + requestLatency = registry.timer("server.requestLatency"); + + // Create a distribution summary meter for tracking the response sizes. + responseSizes = registry.distributionSummary("server.responseSizes"); + + // Gauge type that can be sampled. In this case it will invoke the specified method via + // reflection to get the value. The registry will keep a weak reference to the object passed + // in so that registration will not prevent garbage collection of the server object. + registry.methodValue("server.numConnections", this, "getNumConnections"); + } + + public Response handle(Request req) { + final long s = System.nanoTime(); + try { + Response res = doSomething(req); + + // Update the counter id with dimensions based on the request. The counter will then + // be looked up in the registry which should be fairly cheap, such as lookup of id object + // in a ConcurrentHashMap, it is more expensive than having a local variable set to the + // counter. + final Id cntId = requestCountId + .withTag("country", req.country()) + .withTag("status", res.status()); + registry.counter(cntId).increment(); + + responseSizes.record(res.body().size()); + + return res; + } catch (Exception e) { + final Id cntId = requestCountId + .withTag("country", req.country()) + .withTag("status", "exception") + .withTag("error", e.getClass().getSimpleName()); + registry.counter(cntId).increment(); + throw e; + } finally { + // Update the latency timer. This should typically be done in a finally block. + requestLatency.record(System.nanoTime() - s, TimeUnit.NANOSECONDS); + } + } + + public int getNumConnections() { + // however we determine the current number of connections on the server + } +} +``` diff --git a/build.gradle b/build.gradle new file mode 100755 index 000000000..ff9ee0467 --- /dev/null +++ b/build.gradle @@ -0,0 +1,139 @@ + +// Establish version and status +ext.githubProjectName = 'spectator' + +buildscript { + repositories { + mavenLocal() + mavenCentral() + } + apply from: file('gradle/buildscript.gradle'), to: buildscript +} + +allprojects { + apply plugin: 'java' + apply plugin: 'jacoco' + apply plugin: 'idea' + + repositories { + mavenLocal() + mavenCentral() + } + + jacoco { + toolVersion = "0.7.1.201405082137" + } +} + +apply from: file('gradle/convention.gradle') +apply from: file('gradle/maven.gradle') +apply from: file('gradle/check.gradle') +apply from: file('gradle/license.gradle') +apply from: file('gradle/release.gradle') + +subprojects { + group = "com.netflix.${githubProjectName}" + + sourceCompatibility = 1.7 + targetCompatibility = 1.7 + + tasks.withType(Compile) { + options.compilerArgs << "-Xlint:unchecked" << "-Xlint:deprecation" + } + + javadoc { + options { + links = ['http://docs.oracle.com/javase/7/docs/api/'] + } + } + + dependencies { + compile 'org.slf4j:slf4j-api:1.7.6' + testCompile 'junit:junit:4.10' + testCompile 'nl.jqno.equalsverifier:equalsverifier:1.4.1' + } + + jacocoTestReport { + additionalSourceDirs = files(sourceSets.main.allJava.srcDirs) + reports { + xml.enabled false + csv.enabled false + html.destination "${buildDir}/reports/jacoco" + } + } + + task copyDepsToBuild << { + ['compile', 'runtime', 'testCompile', 'testRuntime'].each { conf -> + delete "${buildDir}/dependencies/${conf}" + copy { + from configurations[conf] + into "${buildDir}/dependencies/${conf}" + } + } + } +} + +project(':spectator-api') { + + javadoc { + options.memberLevel = org.gradle.external.javadoc.JavadocMemberLevel.PROTECTED + title "Spectator" + } + + task(checkCompatibility, dependsOn: 'jar', type: JavaExec) { + // There is probably a better way, but we remove "-SNAPSHOT" from the name if the archive + // doesn't exist because release plugin changes the name. + main = 'com.googlecode.japi.checker.cli.Main' + classpath = files("$projectDir/../codequality/japi-checker-cli-0.2.0-SNAPSHOT.jar") + args = [ + "$projectDir/../codequality/spectator-api-BASELINE.jar", + (jar.archivePath.exists()) + ? jar.archivePath.path + : jar.archivePath.path.replace("-SNAPSHOT", "") + ] + } + + build { + it.dependsOn checkCompatibility + } +} + +project(':spectator-nflx') { + dependencies { + compile project(':spectator-api') + compile project(':spectator-ext-gc') + compile project(':spectator-reg-servo') + compile 'com.netflix.archaius:archaius-core:latest.release' + compile 'com.netflix.governator:governator:latest.release' + compile 'com.netflix.ribbon:ribbon-core:0.3.+' + compile 'com.netflix.ribbon:ribbon-eureka:0.3.+' + compile 'com.netflix.ribbon:ribbon-httpclient:0.3.+' + } +} + +project(':spectator-ext-gc') { + dependencies { + compile project(':spectator-api') + } +} + +project(':spectator-reg-servo') { + dependencies { + compile project(':spectator-api') + compile 'com.netflix.servo:servo-core:latest.release' + } +} + +project(':spectator-reg-metrics2') { + dependencies { + compile project(':spectator-api') + compile 'com.yammer.metrics:metrics-core:2.2.0' + } +} + +project(':spectator-reg-metrics3') { + dependencies { + compile project(':spectator-api') + compile 'com.codahale.metrics:metrics-core:3.0.2' + } +} diff --git a/codequality/HEADER b/codequality/HEADER new file mode 100644 index 000000000..3102e4b44 --- /dev/null +++ b/codequality/HEADER @@ -0,0 +1,13 @@ +Copyright ${year} Netflix, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/codequality/checkstyle.xml b/codequality/checkstyle.xml new file mode 100644 index 000000000..6b0ec7011 --- /dev/null +++ b/codequality/checkstyle.xml @@ -0,0 +1,191 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/codequality/japi-checker-cli-0.2.0-SNAPSHOT.jar b/codequality/japi-checker-cli-0.2.0-SNAPSHOT.jar new file mode 100644 index 000000000..e335bf7ed Binary files /dev/null and b/codequality/japi-checker-cli-0.2.0-SNAPSHOT.jar differ diff --git a/codequality/spectator-api-BASELINE.jar b/codequality/spectator-api-BASELINE.jar new file mode 100644 index 000000000..872bce985 Binary files /dev/null and b/codequality/spectator-api-BASELINE.jar differ diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 000000000..eee94cf57 --- /dev/null +++ b/gradle.properties @@ -0,0 +1 @@ +version=0.8-SNAPSHOT diff --git a/gradle/buildscript.gradle b/gradle/buildscript.gradle new file mode 100644 index 000000000..0b6da7ce8 --- /dev/null +++ b/gradle/buildscript.gradle @@ -0,0 +1,11 @@ +// Executed in context of buildscript +repositories { + // Repo in addition to maven central + repositories { maven { url 'http://dl.bintray.com/content/netflixoss/external-gradle-plugins/' } } // For gradle-release +} +dependencies { + classpath 'nl.javadude.gradle.plugins:license-gradle-plugin:0.6.1' + classpath 'com.mapvine:gradle-cobertura-plugin:0.1' + classpath 'gradle-release:gradle-release:1.1.5' + classpath 'org.ajoberstar:gradle-git:0.5.0' +} diff --git a/gradle/check.gradle b/gradle/check.gradle new file mode 100644 index 000000000..151e098a6 --- /dev/null +++ b/gradle/check.gradle @@ -0,0 +1,34 @@ +subprojects { + apply plugin: 'build-dashboard' + + // Checkstyle + apply plugin: 'checkstyle' + checkstyle { + ignoreFailures = false + configFile = rootProject.file('codequality/checkstyle.xml') + sourceSets = [sourceSets.main] + } + + // FindBugs + apply plugin: 'findbugs' + findbugs { + ignoreFailures = false + sourceSets = [sourceSets.main] + } + tasks.withType(FindBugs) { + findbugs.toolVersion "3.0.0" + reports { + xml.enabled = false + html.enabled = true + } + } + + // PMD + apply plugin: 'pmd' + pmd { + ignoreFailures = false + sourceSets = [sourceSets.main] + } + //tasks.withType(Pmd) { reports.html.enabled true } + +} diff --git a/gradle/convention.gradle b/gradle/convention.gradle new file mode 100644 index 000000000..d972d58cb --- /dev/null +++ b/gradle/convention.gradle @@ -0,0 +1,101 @@ +// GRADLE-2087 workaround, perform after java plugin +status = project.hasProperty('preferredStatus')?project.preferredStatus:(version.contains('SNAPSHOT')?'snapshot':'release') + +subprojects { project -> + apply plugin: 'java' // Plugin as major conventions + + sourceCompatibility = 1.6 + + // Restore status after Java plugin + status = rootProject.status + + task sourcesJar(type: Jar, dependsOn:classes) { + from sourceSets.main.allSource + classifier 'sources' + extension 'jar' + } + + task javadocJar(type: Jar, dependsOn:javadoc) { + from javadoc.destinationDir + classifier 'javadoc' + extension 'jar' + } + + configurations.add('sources') + configurations.add('javadoc') + configurations.archives { + extendsFrom configurations.sources + extendsFrom configurations.javadoc + } + + // When outputting to an Ivy repo, we want to use the proper type field + gradle.taskGraph.whenReady { + def isNotMaven = !it.hasTask(project.uploadMavenCentral) + if (isNotMaven) { + def artifacts = project.configurations.sources.artifacts + def sourceArtifact = artifacts.iterator().next() + sourceArtifact.type = 'sources' + } + } + + artifacts { + sources(sourcesJar) { + // Weird Gradle quirk where type will be used for the extension, but only for sources + type 'jar' + } + javadoc(javadocJar) { + type 'javadoc' + } + } + + configurations { + provided { + description = 'much like compile, but indicates you expect the JDK or a container to provide it. It is only available on the compilation classpath, and is not transitive.' + transitive = true + visible = true + } + } + + project.sourceSets { + main.compileClasspath += project.configurations.provided + main.runtimeClasspath -= project.configurations.provided + test.compileClasspath += project.configurations.provided + test.runtimeClasspath += project.configurations.provided + } +} + +apply plugin: 'github-pages' // Used to create publishGhPages task + +def docTasks = [:] +[Javadoc,ScalaDoc,Groovydoc].each{ Class docClass -> + def allSources = allprojects.tasks*.withType(docClass).flatten()*.source + if (allSources) { + def shortName = docClass.simpleName.toLowerCase() + def docTask = task "aggregate${shortName.capitalize()}"(type: docClass, description: "Aggregate subproject ${shortName}s") { + source = allSources + destinationDir = file("${project.buildDir}/docs/${shortName}") + doFirst { + def classpaths = allprojects.findAll { it.plugins.hasPlugin(JavaPlugin) }.collect { it.sourceSets.main.compileClasspath } + classpath = files(classpaths) + } + } + docTasks[shortName] = docTask + processGhPages.dependsOn(docTask) + } +} + +githubPages { + repoUri = "git@github.com:Netflix/${rootProject.githubProjectName}.git" + pages { + docTasks.each { shortName, docTask -> + from(docTask.outputs.files) { + into "docs/${shortName}" + } + } + } +} + +// Generate wrapper, which is distributed as part of source to alleviate the need of installing gradle +task createWrapper(type: Wrapper) { + gradleVersion = '1.5' +} diff --git a/gradle/license.gradle b/gradle/license.gradle new file mode 100644 index 000000000..abd2e2c0e --- /dev/null +++ b/gradle/license.gradle @@ -0,0 +1,10 @@ +// Dependency for plugin was set in buildscript.gradle + +subprojects { +apply plugin: 'license' //nl.javadude.gradle.plugins.license.LicensePlugin +license { + header rootProject.file('codequality/HEADER') + ext.year = Calendar.getInstance().get(Calendar.YEAR) + skipExistingHeaders true +} +} diff --git a/gradle/maven.gradle b/gradle/maven.gradle new file mode 100644 index 000000000..817846d77 --- /dev/null +++ b/gradle/maven.gradle @@ -0,0 +1,70 @@ +// Maven side of things +subprojects { + apply plugin: 'maven' // Java plugin has to have been already applied for the conf2scope mappings to work + apply plugin: 'signing' + + signing { + required { gradle.taskGraph.hasTask(uploadMavenCentral) } + sign configurations.archives + } + +/** + * Publishing to Maven Central example provided from http://jedicoder.blogspot.com/2011/11/automated-gradle-project-deployment-to.html + * artifactory will execute uploadArchives to force generation of ivy.xml, and we don't want that to trigger an upload to maven + * central, so using custom upload task. + */ +task uploadMavenCentral(type:Upload, dependsOn: signArchives) { + configuration = configurations.archives + onlyIf { ['release', 'snapshot'].contains(project.status) } + repositories.mavenDeployer { + beforeDeployment { signing.signPom(it) } + + // To test deployment locally, use the following instead of oss.sonatype.org + //repository(url: "file://localhost/${rootProject.rootDir}/repo") + + def sonatypeUsername = rootProject.hasProperty('sonatypeUsername')?rootProject.sonatypeUsername:'' + def sonatypePassword = rootProject.hasProperty('sonatypePassword')?rootProject.sonatypePassword:'' + + repository(url: 'https://oss.sonatype.org/service/local/staging/deploy/maven2') { + authentication(userName: sonatypeUsername, password: sonatypePassword) + } + + snapshotRepository(url: 'https://oss.sonatype.org/content/repositories/snapshots/') { + authentication(userName: sonatypeUsername, password: sonatypePassword) + } + + // Prevent datastamp from being appending to artifacts during deployment + uniqueVersion = false + + // Closure to configure all the POM with extra info, common to all projects + pom.project { + name "${project.name}" + description "${project.name} developed by Netflix" + developers { + developer { + id 'netflixgithub' + name 'Netflix Open Source Development' + email 'talent@netflix.com' + } + } + licenses { + license { + name 'The Apache Software License, Version 2.0' + url 'http://www.apache.org/licenses/LICENSE-2.0.txt' + distribution 'repo' + } + } + url "https://github.com/Netflix/${rootProject.githubProjectName}" + scm { + connection "scm:git:git@github.com:Netflix/${rootProject.githubProjectName}.git" + url "scm:git:git@github.com:Netflix/${rootProject.githubProjectName}.git" + developerConnection "scm:git:git@github.com:Netflix/${rootProject.githubProjectName}.git" + } + issueManagement { + system 'github' + url "https://github.com/Netflix/${rootProject.githubProjectName}/issues" + } + } + } + } +} diff --git a/gradle/netflix-oss.gradle b/gradle/netflix-oss.gradle new file mode 100644 index 000000000..a87bc54ef --- /dev/null +++ b/gradle/netflix-oss.gradle @@ -0,0 +1 @@ +apply from: 'http://artifacts.netflix.com/gradle-netflix-local/artifactory.gradle' diff --git a/gradle/release.gradle b/gradle/release.gradle new file mode 100644 index 000000000..69027db83 --- /dev/null +++ b/gradle/release.gradle @@ -0,0 +1,61 @@ +apply plugin: 'release' + +[ uploadIvyLocal: 'uploadLocal', uploadArtifactory: 'artifactoryPublish', buildWithArtifactory: 'build' ].each { key, value -> + // Call out to compile against internal repository + task "${key}"(type: GradleBuild) { + startParameter = project.gradle.startParameter.newInstance() + doFirst { + startParameter.projectProperties = [status: project.status, preferredStatus: project.status] + } + startParameter.addInitScript( file('gradle/netflix-oss.gradle') ) + startParameter.getExcludedTaskNames().add('check') + tasks = [ 'build', value ] + } +} + +// Marker task for following code to key in on +task releaseCandidate(dependsOn: release) +task forceCandidate { + onlyIf { gradle.taskGraph.hasTask(releaseCandidate) } + doFirst { project.status = 'candidate' } +} +task forceRelease { + onlyIf { !gradle.taskGraph.hasTask(releaseCandidate) } + doFirst { project.status = 'release' } +} +release.dependsOn([forceCandidate, forceRelease]) + +task uploadMavenCentral(dependsOn: subprojects.tasks.uploadMavenCentral) +task releaseSnapshot(dependsOn: [uploadArtifactory, uploadMavenCentral]) + +// Ensure our versions look like the project status before publishing +task verifyStatus << { + def hasSnapshot = version.contains('-SNAPSHOT') + if (project.status == 'snapshot' && !hasSnapshot) { + throw new GradleException("Version (${version}) needs -SNAPSHOT if publishing snapshot") + } +} +uploadArtifactory.dependsOn(verifyStatus) +uploadMavenCentral.dependsOn(verifyStatus) + +// Ensure upload happens before tagging, hence upload failures will leave repo in a revertable state +preTagCommit.dependsOn([uploadArtifactory, uploadMavenCentral]) + + +gradle.taskGraph.whenReady { taskGraph -> + def hasRelease = taskGraph.hasTask('commitNewVersion') + def indexOf = { return taskGraph.allTasks.indexOf(it) } + + if (hasRelease) { + assert indexOf(build) < indexOf(unSnapshotVersion), 'build target has to be after unSnapshotVersion' + assert indexOf(uploadMavenCentral) < indexOf(preTagCommit), 'preTagCommit has to be after uploadMavenCentral' + assert indexOf(uploadArtifactory) < indexOf(preTagCommit), 'preTagCommit has to be after uploadArtifactory' + } +} + +// Prevent plugin from asking for a version number interactively +ext.'gradle.release.useAutomaticVersion' = "true" + +release { + git.requireBranch = null +} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 000000000..faa569a9a Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..d7036d56e --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Tue Apr 02 11:45:56 PDT 2013 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=http\://services.gradle.org/distributions/gradle-1.12-bin.zip diff --git a/gradlew b/gradlew new file mode 100755 index 000000000..91a7e269e --- /dev/null +++ b/gradlew @@ -0,0 +1,164 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# For Cygwin, ensure paths are in UNIX format before anything is touched. +if $cygwin ; then + [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` +fi + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >&- +APP_HOME="`pwd -P`" +cd "$SAVED" >&- + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/settings.gradle b/settings.gradle new file mode 100755 index 000000000..5ede2af1b --- /dev/null +++ b/settings.gradle @@ -0,0 +1,6 @@ +include 'spectator-api', + 'spectator-nflx', + 'spectator-ext-gc', + 'spectator-reg-metrics2', + 'spectator-reg-metrics3', + 'spectator-reg-servo' diff --git a/spectator-api/src/main/java/com/netflix/spectator/api/AbstractMeter.java b/spectator-api/src/main/java/com/netflix/spectator/api/AbstractMeter.java new file mode 100644 index 000000000..60f7bc893 --- /dev/null +++ b/spectator-api/src/main/java/com/netflix/spectator/api/AbstractMeter.java @@ -0,0 +1,63 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.netflix.spectator.api; + +import java.lang.ref.WeakReference; + +/** + * Helper base class for meters that maintains a weak reference to the object being measured. + * Meters that are based on polling rather than activity need some other way to define the + * lifespan and in this case that is if the reference still exists. Basing this off a weak + * reference means there doesn't need to be an explicit de-registration and registering to + * collect insight will not prevent garbage collection of the underlying object. + */ +public abstract class AbstractMeter implements Meter { + /** Clock to use for getting measurement timestamps. */ + protected final Clock clock; + + /** Identifier for the meter. */ + protected final Id id; + + /** Reference to the underlying object used to compute the measurements. */ + protected final WeakReference ref; + + /** + * Create a new instance. + * + * @param clock + * Clock to use for getting measurement timestamps. Typically should be the clock used by + * the registry (see {@link Registry#clock()}). + * @param id + * Identifier for the meter. + * @param obj + * Reference to the underlying object used to compute the measurements. + */ + public AbstractMeter(Clock clock, Id id, T obj) { + this.clock = clock; + this.id = id; + this.ref = new WeakReference<>(obj); + } + + /** {@inheritDoc} */ + @Override public Id id() { + return this.id; + } + + /** {@inheritDoc} */ + @Override public boolean hasExpired() { + return this.ref.get() == null; + } +} diff --git a/spectator-api/src/main/java/com/netflix/spectator/api/AbstractRegistry.java b/spectator-api/src/main/java/com/netflix/spectator/api/AbstractRegistry.java new file mode 100644 index 000000000..01af6788c --- /dev/null +++ b/spectator-api/src/main/java/com/netflix/spectator/api/AbstractRegistry.java @@ -0,0 +1,219 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.netflix.spectator.api; + +import java.util.Iterator; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Base class to make it easier to implement a simple registry that only needs to customise the + * types returned for Counter, DistributionSummary, and Timer calls. + */ +public abstract class AbstractRegistry implements Registry { + + private final Clock clock; + + private final ConcurrentHashMap listeners; + private final ConcurrentHashMap meters; + + /** + * Create a new instance. + * + * @param clock + * Clock used for performing all timing measurements. + */ + public AbstractRegistry(Clock clock) { + this.clock = clock; + listeners = new ConcurrentHashMap<>(); + meters = new ConcurrentHashMap<>(); + } + + /** + * Create a new counter instance for a given id. + * + * @param id + * Identifier used to lookup this meter in the registry. + * @return + * New counter instance. + */ + protected abstract Counter newCounter(Id id); + + /** + * Create a new distribution summary instance for a given id. + * + * @param id + * Identifier used to lookup this meter in the registry. + * @return + * New counter instance. + */ + protected abstract DistributionSummary newDistributionSummary(Id id); + + /** + * Create a new timer instance for a given id. + * + * @param id + * Identifier used to lookup this meter in the registry. + * @return + * New counter instance. + */ + protected abstract Timer newTimer(Id id); + + /** {@inheritDoc} */ + @Override + public final Clock clock() { + return clock; + } + + /** {@inheritDoc} */ + @Override + public final Id createId(String name) { + return new DefaultId(name, TagList.EMPTY); + } + + /** {@inheritDoc} */ + @Override + public final Id createId(String name, Iterable tags) { + return new DefaultId(name, TagList.create(tags)); + } + + private void logTypeError(Id id, Class desired, Class found) { + final String dtype = desired.getName(); + final String ftype = found.getName(); + final String msg = String.format("cannot access '%s' as a %s, it already exists as a %s", + id, dtype, ftype); + Throwables.propagate(new IllegalStateException(msg)); + } + + private void addToAggr(Meter aggr, Meter meter) { + if (aggr instanceof AggrMeter) { + ((AggrMeter) aggr).add(meter); + } else { + logTypeError(meter.id(), meter.getClass(), aggr.getClass()); + } + } + + private Meter putIfAbsent(Id id, Meter m, Meter fallback) { + return (meters.size() >= Config.maxNumberOfMeters()) ? fallback : meters.putIfAbsent(id, m); + } + + /** {@inheritDoc} */ + @Override + public final void register(Meter meter) { + Meter m = meters.get(meter.id()); + if (m == null) { + if (meters.size() >= Config.maxNumberOfMeters()) { + return; + } + AggrMeter aggr = new AggrMeter(meter.id()); + m = meters.putIfAbsent(meter.id(), aggr); + if (m == null) { + aggr.add(meter); + } else { + addToAggr(m, meter); + } + } else { + addToAggr(m, meter); + } + notifyOfAdd(meter); + } + + /** {@inheritDoc} */ + @Override + public final Counter counter(Id id) { + Meter m = meters.get(id); + if (m == null) { + Counter c = newCounter(id); + m = putIfAbsent(id, c, NoopCounter.INSTANCE); + if (m == null) { + m = c; + notifyOfAdd(m); + } + } + if (!(m instanceof Counter)) { + logTypeError(id, Counter.class, m.getClass()); + m = NoopCounter.INSTANCE; + } + return (Counter) m; + } + + /** {@inheritDoc} */ + @Override + public final DistributionSummary distributionSummary(Id id) { + Meter m = meters.get(id); + if (m == null) { + DistributionSummary s = newDistributionSummary(id); + m = putIfAbsent(id, s, NoopDistributionSummary.INSTANCE); + if (m == null) { + m = s; + notifyOfAdd(m); + } + } + if (!(m instanceof DistributionSummary)) { + logTypeError(id, DistributionSummary.class, m.getClass()); + m = NoopDistributionSummary.INSTANCE; + } + return (DistributionSummary) m; + } + + /** {@inheritDoc} */ + @Override + public final Timer timer(Id id) { + Meter m = meters.get(id); + if (m == null) { + Timer t = newTimer(id); + m = putIfAbsent(id, t, NoopTimer.INSTANCE); + if (m == null) { + m = t; + notifyOfAdd(m); + } + } + if (!(m instanceof Timer)) { + logTypeError(id, Timer.class, m.getClass()); + m = NoopTimer.INSTANCE; + } + return (Timer) m; + } + + /** {@inheritDoc} */ + @Override + public final Meter get(Id id) { + return meters.get(id); + } + + /** {@inheritDoc} */ + @Override + public final Iterator iterator() { + return meters.values().iterator(); + } + + /** {@inheritDoc} */ + @Override + public final void addListener(RegistryListener listener) { + listeners.put(listener, listener); + } + + /** {@inheritDoc} */ + @Override + public final void removeListener(RegistryListener listener) { + listeners.remove(listener); + } + + private void notifyOfAdd(Meter meter) { + for (RegistryListener listener : listeners.values()) { + listener.onAdd(meter); + } + } +} diff --git a/spectator-api/src/main/java/com/netflix/spectator/api/AggrMeter.java b/spectator-api/src/main/java/com/netflix/spectator/api/AggrMeter.java new file mode 100644 index 000000000..6c144d539 --- /dev/null +++ b/spectator-api/src/main/java/com/netflix/spectator/api/AggrMeter.java @@ -0,0 +1,77 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.netflix.spectator.api; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.concurrent.ConcurrentLinkedQueue; + +/** + * Composite meter that computes a sum aggregate of the overlapping measurements in meters in the + * set. This is typically used to combine the values of gauges that share the same id. + */ +class AggrMeter implements Meter { + private final Id id; + private final ConcurrentLinkedQueue queue; + + /** Create a new instance. */ + AggrMeter(Id id) { + this.id = id; + this.queue = new ConcurrentLinkedQueue<>(); + } + + /** {@inheritDoc} */ + @Override + public Id id() { + return id; + } + + /** {@inheritDoc} */ + @Override + public Iterable measure() { + Map measurements = new HashMap<>(); + Iterator iter = queue.iterator(); + while (iter.hasNext()) { + Meter meter = iter.next(); + if (meter.hasExpired()) { + iter.remove(); + } else { + for (Measurement m : meter.measure()) { + Measurement prev = measurements.get(m.id()); + if (prev == null) { + measurements.put(m.id(), m); + } else { + double v = prev.value() + m.value(); + measurements.put(prev.id(), new Measurement(prev.id(), prev.timestamp(), v)); + } + } + } + } + return measurements.values(); + } + + /** {@inheritDoc} */ + @Override + public boolean hasExpired() { + return queue.isEmpty(); + } + + /** Adds a meter to the set included in the aggregate. */ + void add(Meter m) { + queue.add(m); + } +} diff --git a/spectator-api/src/main/java/com/netflix/spectator/api/Clock.java b/spectator-api/src/main/java/com/netflix/spectator/api/Clock.java new file mode 100644 index 000000000..66222fdef --- /dev/null +++ b/spectator-api/src/main/java/com/netflix/spectator/api/Clock.java @@ -0,0 +1,52 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.netflix.spectator.api; + +/** + * A timing source that can be used to access the current wall time as well as a high resolution + * monotonic time to measuring elapsed times. Most of the time the {@link #SYSTEM} implementation + * that calls the builtin java methods is probably the right one to use. Other implementations + * would typically only get used for unit tests or other cases where precise control of the clock + * is needed. + */ +public interface Clock { + /** + * Current wall time in milliseconds since the epoch. Typically equivalent to + * System.currentTimeMillis. + */ + long wallTime(); + + /** + * Current time from a monotonic clock source. The value is only meaningful when compared with + * another snapshot to determine the elapsed time for an operation. The difference between two + * samples will have a unit of nanoseconds. The returned value is typically equivalent to + * System.nanoTime. + */ + long monotonicTime(); + + /** + * Default clock implementation based on corresponding calls in {@link java.lang.System}. + */ + Clock SYSTEM = new Clock() { + public long wallTime() { + return System.currentTimeMillis(); + } + + public long monotonicTime() { + return System.nanoTime(); + } + }; +} diff --git a/spectator-api/src/main/java/com/netflix/spectator/api/Config.java b/spectator-api/src/main/java/com/netflix/spectator/api/Config.java new file mode 100644 index 000000000..a32578b4d --- /dev/null +++ b/spectator-api/src/main/java/com/netflix/spectator/api/Config.java @@ -0,0 +1,52 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.netflix.spectator.api; + +/** Helper methods for accessing configuration settings. */ +final class Config { + + private static final String PREFIX = "spectator.api."; + + /** Value for registry class to explicitly indicate the service loader should be used. */ + static final String SERVICE_LOADER = "service-loader"; + + private Config() { + } + + /** Should an exception be thrown for warnings? */ + static boolean propagateWarnings() { + return Boolean.valueOf(System.getProperty(PREFIX + "propagateWarnings", "false")); + } + + /** Class implementing the {@link Registry} interface that should be loaded. */ + static String registryClass() { + return System.getProperty(PREFIX + "registryClass", SERVICE_LOADER); + } + + /** + * For classes based on {@link AbstractRegistry} this setting is used to determine the maximum + * number of registered meters permitted. This limit is used to help protect the system from a + * memory leak if there is a bug or irresponsible usage of registering meters. + * + * @return + * Maximum number of distinct meters that can be registered at a given time. The default is + * {@link java.lang.Integer#MAX_VALUE}. + */ + static int maxNumberOfMeters() { + final String v = System.getProperty(PREFIX + "maxNumberOfMeters"); + return (v == null) ? Integer.MAX_VALUE : Integer.parseInt(v); + } +} diff --git a/spectator-api/src/main/java/com/netflix/spectator/api/Counter.java b/spectator-api/src/main/java/com/netflix/spectator/api/Counter.java new file mode 100644 index 000000000..36643cd7d --- /dev/null +++ b/spectator-api/src/main/java/com/netflix/spectator/api/Counter.java @@ -0,0 +1,35 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.netflix.spectator.api; + +/** + * Measures the rate of change based on calls to increment. + */ +public interface Counter extends Meter { + /** Update the counter by one. */ + void increment(); + + /** + * Update the counter by {@code amount}. + * + * @param amount + * Amount to add to the counter. + */ + void increment(long amount); + + /** The cumulative count since this counter was created. */ + long count(); +} diff --git a/spectator-api/src/main/java/com/netflix/spectator/api/DefaultCounter.java b/spectator-api/src/main/java/com/netflix/spectator/api/DefaultCounter.java new file mode 100644 index 000000000..23646d424 --- /dev/null +++ b/spectator-api/src/main/java/com/netflix/spectator/api/DefaultCounter.java @@ -0,0 +1,72 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.netflix.spectator.api; + +import java.util.Collections; +import java.util.concurrent.atomic.AtomicLong; + +/** Counter implementation for the default registry. */ +final class DefaultCounter implements Counter { + + private final Clock clock; + private final Id id; + private final AtomicLong count; + + /** Create a new instance. */ + DefaultCounter(Clock clock, Id id) { + this.clock = clock; + this.id = id; + this.count = new AtomicLong(0L); + } + + /** {@inheritDoc} */ + @Override + public Id id() { + return id; + } + + /** {@inheritDoc} */ + @Override + public boolean hasExpired() { + return false; + } + + /** {@inheritDoc} */ + @Override + public Iterable measure() { + long now = clock.wallTime(); + long v = count.get(); + return Collections.singleton(new Measurement(id, now, v)); + } + + /** {@inheritDoc} */ + @Override + public void increment() { + count.incrementAndGet(); + } + + /** {@inheritDoc} */ + @Override + public void increment(long amount) { + count.addAndGet(amount); + } + + /** {@inheritDoc} */ + @Override + public long count() { + return count.get(); + } +} diff --git a/spectator-api/src/main/java/com/netflix/spectator/api/DefaultDistributionSummary.java b/spectator-api/src/main/java/com/netflix/spectator/api/DefaultDistributionSummary.java new file mode 100644 index 000000000..c45ec0ea8 --- /dev/null +++ b/spectator-api/src/main/java/com/netflix/spectator/api/DefaultDistributionSummary.java @@ -0,0 +1,83 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.netflix.spectator.api; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicLong; + +/** Distribution summary implementation for the default registry. */ +final class DefaultDistributionSummary implements DistributionSummary { + + private final Clock clock; + private final Id id; + private final AtomicLong count; + private final AtomicLong totalAmount; + + private final Id countId; + private final Id totalAmountId; + + /** Create a new instance. */ + DefaultDistributionSummary(Clock clock, Id id) { + this.clock = clock; + this.id = id; + count = new AtomicLong(0L); + totalAmount = new AtomicLong(0L); + countId = id.withTag("statistic", "count"); + totalAmountId = id.withTag("statistic", "totalAmount"); + } + + /** {@inheritDoc} */ + @Override + public Id id() { + return id; + } + + /** {@inheritDoc} */ + @Override + public boolean hasExpired() { + return false; + } + + /** {@inheritDoc} */ + @Override + public void record(long amount) { + totalAmount.addAndGet(amount); + count.incrementAndGet(); + } + + /** {@inheritDoc} */ + @Override + public Iterable measure() { + final long now = clock.wallTime(); + final List ms = new ArrayList<>(2); + ms.add(new Measurement(countId, now, count.get())); + ms.add(new Measurement(totalAmountId, now, totalAmount.get())); + return ms; + } + + /** {@inheritDoc} */ + @Override + public long count() { + return count.get(); + } + + /** {@inheritDoc} */ + @Override + public long totalAmount() { + return totalAmount.get(); + } +} diff --git a/spectator-api/src/main/java/com/netflix/spectator/api/DefaultId.java b/spectator-api/src/main/java/com/netflix/spectator/api/DefaultId.java new file mode 100644 index 000000000..a80db6619 --- /dev/null +++ b/spectator-api/src/main/java/com/netflix/spectator/api/DefaultId.java @@ -0,0 +1,122 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.netflix.spectator.api; + +import com.netflix.spectator.impl.Preconditions; + +import java.util.*; + +/** Id implementation for the default registry. */ +final class DefaultId implements Id { + + private static final Set EMPTY = new HashSet<>(); + + private final String name; + private final TagList tags; + + /** Create a new instance. */ + DefaultId(String name) { + this(name, TagList.EMPTY); + } + + /** Create a new instance. */ + DefaultId(String name, TagList tags) { + this.name = Preconditions.checkNotNull(name, "name"); + this.tags = tags; + } + + /** {@inheritDoc} */ + @Override + public String name() { + return name; + } + + /** {@inheritDoc} */ + @Override + public Iterable tags() { + return (tags == TagList.EMPTY) ? Collections.emptyList() : tags; + } + + /** {@inheritDoc} */ + @Override + public DefaultId withTag(Tag tag) { + return new DefaultId(name, new TagList(tag.key(), tag.value(), tags)); + } + + /** {@inheritDoc} */ + @Override + public DefaultId withTag(String key, String value) { + return new DefaultId(name, new TagList(key, value, tags)); + } + + /** + * Returns a new id with the tag list sorted by key and with no duplicate keys. If a duplicate + * is found the last entry in the list with a given key will be used. + */ + public DefaultId normalize() { + return rollup(EMPTY, false); + } + + /** + * Create a new id by removing tags from the list. This operation will 1) sort the list by the + * tag keys, 2) dedup entries if multiple have the same key, and 3) remove keys specified by + * the parameters. + * + * @param keys + * Set of keys to either keep or remove. + * @param keep + * If true, then the new id can only have tag keys in the provided set. Otherwise the new id + * can only have ids not in that set. + * @return + * New identifier after applying the rollup. + */ + public DefaultId rollup(Set keys, boolean keep) { + Map ts = new TreeMap<>(); + for (Tag t : tags) { + if (keys.contains(t.key()) == keep) { + ts.put(t.key(), t.value()); + } + } + return new DefaultId(name, TagList.create(ts)); + } + + /** {@inheritDoc} */ + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null || !(obj instanceof DefaultId)) return false; + DefaultId other = (DefaultId) obj; + return name.equals(other.name) + && ((tags == null && other.tags == null) || (tags != null && tags.equals(other.tags))); + } + + /** {@inheritDoc} */ + @Override + public int hashCode() { + return name.hashCode() + (tags == null ? 0 : tags.hashCode()); + } + + /** {@inheritDoc} */ + @Override + public String toString() { + StringBuilder buf = new StringBuilder(); + buf.append(name); + for (Tag t : tags()) { + buf.append(":").append(t.key()).append("=").append(t.value()); + } + return buf.toString(); + } +} diff --git a/spectator-api/src/main/java/com/netflix/spectator/api/DefaultLongTaskTimer.java b/spectator-api/src/main/java/com/netflix/spectator/api/DefaultLongTaskTimer.java new file mode 100644 index 000000000..fb4ccef21 --- /dev/null +++ b/spectator-api/src/main/java/com/netflix/spectator/api/DefaultLongTaskTimer.java @@ -0,0 +1,112 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.netflix.spectator.api; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; + +/** Long task timer implementation used by the extended registry. */ +final class DefaultLongTaskTimer implements LongTaskTimer { + + private static final double NANOS_PER_SECOND = (double) TimeUnit.SECONDS.toNanos(1L); + + private final Clock clock; + private final Id id; + private final ConcurrentMap tasks = new ConcurrentHashMap<>(); + private final AtomicLong nextTask = new AtomicLong(0L); + private final Id activeTasksId; + private final Id durationId; + + /** Create a new instance. */ + DefaultLongTaskTimer(Clock clock, Id id) { + this.clock = clock; + this.id = id; + + this.activeTasksId = id.withTag("statistic", "activeTasks"); + this.durationId = id.withTag("statistic", "duration"); + } + + /** {@inheritDoc} */ + @Override + public Id id() { + return id; + } + + /** {@inheritDoc} */ + @Override + public boolean hasExpired() { + return false; + } + + /** {@inheritDoc} */ + @Override + public long start() { + long task = nextTask.getAndIncrement(); + tasks.put(task, clock.monotonicTime()); + return task; + } + + /** {@inheritDoc} */ + @Override + public long stop(long task) { + Long startTime = tasks.get(task); + if (startTime != null) { + tasks.remove(task); + return clock.monotonicTime() - startTime; + } else { + return -1L; + } + } + + /** {@inheritDoc} */ + @Override + public long duration(long task) { + Long startTime = tasks.get(task); + return (startTime != null) ? (clock.monotonicTime() - startTime) : -1L; + } + + /** {@inheritDoc} */ + @Override + public long duration() { + long now = clock.monotonicTime(); + long sum = 0L; + for (long startTime : tasks.values()) { + sum += now - startTime; + } + return sum; + } + + /** {@inheritDoc} */ + @Override + public int activeTasks() { + return tasks.size(); + } + + /** {@inheritDoc} */ + @Override + public Iterable measure() { + final List ms = new ArrayList<>(2); + final long now = clock.wallTime(); + final double durationSeconds = duration() / NANOS_PER_SECOND; + ms.add(new Measurement(durationId, now, durationSeconds)); + ms.add(new Measurement(activeTasksId, now, activeTasks())); + return ms; + } +} diff --git a/spectator-api/src/main/java/com/netflix/spectator/api/DefaultRegistry.java b/spectator-api/src/main/java/com/netflix/spectator/api/DefaultRegistry.java new file mode 100644 index 000000000..e007fb8a8 --- /dev/null +++ b/spectator-api/src/main/java/com/netflix/spectator/api/DefaultRegistry.java @@ -0,0 +1,48 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.netflix.spectator.api; + +/** Default implementation of registry. */ +public final class DefaultRegistry extends AbstractRegistry { + + /** Create a new instance. */ + public DefaultRegistry() { + this(Clock.SYSTEM); + } + + /** Create a new instance. */ + public DefaultRegistry(Clock clock) { + super(clock); + } + + /** {@inheritDoc} */ + @Override + protected Counter newCounter(Id id) { + return new DefaultCounter(clock(), id); + } + + /** {@inheritDoc} */ + @Override + protected DistributionSummary newDistributionSummary(Id id) { + return new DefaultDistributionSummary(clock(), id); + } + + /** {@inheritDoc} */ + @Override + protected Timer newTimer(Id id) { + return new DefaultTimer(clock(), id); + } +} diff --git a/spectator-api/src/main/java/com/netflix/spectator/api/DefaultTimer.java b/spectator-api/src/main/java/com/netflix/spectator/api/DefaultTimer.java new file mode 100644 index 000000000..819f72bc4 --- /dev/null +++ b/spectator-api/src/main/java/com/netflix/spectator/api/DefaultTimer.java @@ -0,0 +1,110 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.netflix.spectator.api; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; + +/** Timer implementation for the default registry. */ +final class DefaultTimer implements Timer { + + private final Clock clock; + private final Id id; + private final AtomicLong count; + private final AtomicLong totalTime; + + private final Id countId; + private final Id totalTimeId; + + /** Create a new instance. */ + DefaultTimer(Clock clock, Id id) { + this.clock = clock; + this.id = id; + count = new AtomicLong(0L); + totalTime = new AtomicLong(0L); + countId = id.withTag("statistic", "count"); + totalTimeId = id.withTag("statistic", "totalTime"); + } + + /** {@inheritDoc} */ + @Override + public Id id() { + return id; + } + + /** {@inheritDoc} */ + @Override + public boolean hasExpired() { + return false; + } + + /** {@inheritDoc} */ + @Override + public void record(long amount, TimeUnit unit) { + final long nanos = TimeUnit.NANOSECONDS.convert(amount, unit); + totalTime.addAndGet(nanos); + count.incrementAndGet(); + } + + /** {@inheritDoc} */ + @Override + public Iterable measure() { + final long now = clock.wallTime(); + final List ms = new ArrayList<>(2); + ms.add(new Measurement(countId, now, count.get())); + ms.add(new Measurement(totalTimeId, now, totalTime.get())); + return ms; + } + + /** {@inheritDoc} */ + @Override + public T record(Callable f) throws Exception { + final long s = clock.monotonicTime(); + try { + return f.call(); + } finally { + final long e = clock.monotonicTime(); + record(e - s, TimeUnit.NANOSECONDS); + } + } + + /** {@inheritDoc} */ + @Override + public void record(Runnable f) { + final long s = clock.monotonicTime(); + try { + f.run(); + } finally { + final long e = clock.monotonicTime(); + record(e - s, TimeUnit.NANOSECONDS); + } + } + + /** {@inheritDoc} */ + @Override + public long count() { + return count.get(); + } + + /** {@inheritDoc} */ + @Override + public long totalTime() { + return totalTime.get(); + } +} diff --git a/spectator-api/src/main/java/com/netflix/spectator/api/DistributionSummary.java b/spectator-api/src/main/java/com/netflix/spectator/api/DistributionSummary.java new file mode 100644 index 000000000..6c3b4becc --- /dev/null +++ b/spectator-api/src/main/java/com/netflix/spectator/api/DistributionSummary.java @@ -0,0 +1,43 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.netflix.spectator.api; + +/** + * Track the sample distribution of events. An example would be the response sizes for requests + * hitting and http server. + * + * The precise set of information maintained depends on the implementation. Most should try to + * provide a consistent implementation of {@link #count()} and {@link #totalAmount()}, + * but some implementations may not. In particular, the implementation from {@link NoopRegistry} + * will always return 0. + */ +public interface DistributionSummary extends Meter { + + /** + * Updates the statistics kept by the summary with the specified amount. + * + * @param amount + * Amount for an event being measured. For example, if the size in bytes of responses + * from a server. If the amount is less than 0 the value will be dropped. + */ + void record(long amount); + + /** The number of times that record has been called since this timer was created. */ + long count(); + + /** The total amount of all recorded events since this summary was created. */ + long totalAmount(); +} diff --git a/spectator-api/src/main/java/com/netflix/spectator/api/DoubleFunction.java b/spectator-api/src/main/java/com/netflix/spectator/api/DoubleFunction.java new file mode 100644 index 000000000..5dd1da0d6 --- /dev/null +++ b/spectator-api/src/main/java/com/netflix/spectator/api/DoubleFunction.java @@ -0,0 +1,40 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.netflix.spectator.api; + +/** + * Function to extract a double value from an object. + */ +public abstract class DoubleFunction implements ValueFunction { + + /** {@inheritDoc} */ + @Override + public double apply(Object obj) { + return (obj instanceof Number) + ? apply(((Number) obj).doubleValue()) + : Double.NaN; + } + + /** + * Apply a transform to the value `v`. + * + * @param v + * Double value to transform. + * @return + * Result of applying this function to `v`. + */ + public abstract double apply(double v); +} diff --git a/spectator-api/src/main/java/com/netflix/spectator/api/ExtendedRegistry.java b/spectator-api/src/main/java/com/netflix/spectator/api/ExtendedRegistry.java new file mode 100644 index 000000000..18f7e98d2 --- /dev/null +++ b/spectator-api/src/main/java/com/netflix/spectator/api/ExtendedRegistry.java @@ -0,0 +1,549 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.netflix.spectator.api; + +import com.netflix.spectator.impl.Preconditions; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.lang.reflect.Method; +import java.util.Collection; +import java.util.Iterator; +import java.util.Map; + + +/** + * Wraps a registry and provides additional helper methods to make it easier to use. + */ +public final class ExtendedRegistry implements Registry { + + private static final Logger LOGGER = LoggerFactory.getLogger(ExtendedRegistry.class); + + private final Registry impl; + + /** Create a new instance. */ + public ExtendedRegistry(Registry impl) { + this.impl = Preconditions.checkNotNull(impl, "impl"); + } + + /** Returns the underlying registry implementation that is being wrapped. */ + public Registry underlying() { + return impl; + } + + /** {@inheritDoc} */ + @Override + public Clock clock() { + return impl.clock(); + } + + /** {@inheritDoc} */ + @Override + public Id createId(String name) { + return impl.createId(name); + } + + /** {@inheritDoc} */ + @Override + public Id createId(String name, Iterable tags) { + return impl.createId(name, tags); + } + + /** {@inheritDoc} */ + @Override + public void register(Meter meter) { + impl.register(meter); + } + + /** {@inheritDoc} */ + @Override + public Counter counter(Id id) { + return impl.counter(id); + } + + /** {@inheritDoc} */ + @Override + public DistributionSummary distributionSummary(Id id) { + return impl.distributionSummary(id); + } + + /** {@inheritDoc} */ + @Override + public Timer timer(Id id) { + return impl.timer(id); + } + + /** {@inheritDoc} */ + @Override + public Meter get(Id id) { + return impl.get(id); + } + + /** {@inheritDoc} */ + @Override + public Iterator iterator() { + return impl.iterator(); + } + + /** {@inheritDoc} */ + @Override + public void addListener(RegistryListener listener) { + impl.addListener(listener); + } + + /** {@inheritDoc} */ + @Override + public void removeListener(RegistryListener listener) { + impl.removeListener(listener); + } + + ///////////////////////////////////////////////////////////////// + // Additional helper methods below + + private Iterable toIterable(String[] tags) { + if (tags.length % 2 == 1) { + throw new IllegalArgumentException("size must be even, it is a set of key=value pairs"); + } + TagList ts = TagList.EMPTY; + for (int i = 0; i < tags.length; i += 2) { + ts = new TagList(tags[i], tags[i + 1], ts); + } + return ts; + } + + /** + * Creates an identifier for a meter. + * + * @param name + * Description of the measurement that is being collected. + * @param tags + * Other dimensions that can be used to classify the measurement. + * @return + * Identifier for a meter. + */ + public Id createId(String name, String... tags) { + return impl.createId(name, toIterable(tags)); + } + + /** + * Measures the rate of some activity. + * + * @param name + * Description of the measurement that is being collected. + * @return + * Counter instance with the corresponding id. + */ + public Counter counter(String name) { + return impl.counter(impl.createId(name)); + } + + /** + * Measures the rate of some activity. + * + * @param name + * Description of the measurement that is being collected. + * @param tags + * Other dimensions that can be used to classify the measurement. + * @return + * Counter instance with the corresponding id. + */ + public Counter counter(String name, Iterable tags) { + return impl.counter(impl.createId(name, tags)); + } + + /** + * Measures the rate of some activity. + * + * @param name + * Description of the measurement that is being collected. + * @param tags + * Other dimensions that can be used to classify the measurement. + * @return + * Counter instance with the corresponding id. + */ + public Counter counter(String name, String... tags) { + return impl.counter(impl.createId(name, toIterable(tags))); + } + + /** + * Measures the sample distribution of events. + * + * @param name + * Description of the measurement that is being collected. + * @return + * Summary instance with the corresponding id. + */ + public DistributionSummary distributionSummary(String name) { + return impl.distributionSummary(impl.createId(name)); + } + + /** + * Measures the sample distribution of events. + * + * @param name + * Description of the measurement that is being collected. + * @param tags + * Other dimensions that can be used to classify the measurement. + * @return + * Summary instance with the corresponding id. + */ + public DistributionSummary distributionSummary(String name, Iterable tags) { + return impl.distributionSummary(impl.createId(name, tags)); + } + + /** + * Measures the sample distribution of events. + * + * @param name + * Description of the measurement that is being collected. + * @param tags + * Other dimensions that can be used to classify the measurement. + * @return + * Summary instance with the corresponding id. + */ + public DistributionSummary distributionSummary(String name, String... tags) { + return impl.distributionSummary(impl.createId(name, toIterable(tags))); + } + + /** + * Measures the time taken for short tasks. + * + * @param name + * Description of the measurement that is being collected. + * @return + * Timer instance with the corresponding id. + */ + public Timer timer(String name) { + return impl.timer(impl.createId(name)); + } + + /** + * Measures the time taken for short tasks. + * + * @param name + * Description of the measurement that is being collected. + * @param tags + * Other dimensions that can be used to classify the measurement. + * @return + * Timer instance with the corresponding id. + */ + public Timer timer(String name, Iterable tags) { + return impl.timer(impl.createId(name, tags)); + } + + /** + * Measures the time taken for short tasks. + * + * @param name + * Description of the measurement that is being collected. + * @param tags + * Other dimensions that can be used to classify the measurement. + * @return + * Timer instance with the corresponding id. + */ + public Timer timer(String name, String... tags) { + return impl.timer(impl.createId(name, toIterable(tags))); + } + + /** + * Measures the time taken for long tasks. + * + * @param id + * Identifier for the metric being registered. + * @return + * Timer instance with the corresponding id. + */ + public LongTaskTimer longTaskTimer(Id id) { + LongTaskTimer taskTimer = new DefaultLongTaskTimer(clock(), id); + impl.register(taskTimer); // the AggrMeter has the right semantics for these type of timers + return taskTimer; + } + + /** + * Measures the time taken for long tasks. + * + * @param name + * Description of the measurement that is being collected. + * @return + * Timer instance with the corresponding id. + */ + public LongTaskTimer longTaskTimer(String name) { + return longTaskTimer(createId(name)); + } + + /** + * Measures the time taken for long tasks. + * + * @param name + * Description of the measurement that is being collected. + * @param tags + * Other dimensions that can be used to classify the measurement. + * @return + * Timer instance with the corresponding id. + */ + public LongTaskTimer longTaskTimer(String name, Iterable tags) { + return longTaskTimer(createId(name, tags)); + } + + /** + * Measures the time taken for long tasks. + * + * @param name + * Description of the measurement that is being collected. + * @param tags + * Other dimensions that can be used to classify the measurement. + * @return + * Timer instance with the corresponding id. + */ + public LongTaskTimer longTaskTimer(String name, String... tags) { + return longTaskTimer(createId(name, toIterable(tags))); + } + + /** + * Register a gauge that reports the value of the {@link java.lang.Number}. The registration + * will keep a weak reference to the number so it will not prevent garbage collection. + * The number implementation used should be thread safe. + * + * @param id + * Identifier for the metric being registered. + * @param number + * Thread-safe implementation of {@link Number} used to access the value. + * @return + * The number that was passed in so the registration can be done as part of an assignment + * statement. + */ + public T gauge(Id id, T number) { + return gauge(id, number, Functions.IDENTITY); + } + + /** + * Register a gauge that reports the value of the {@link java.lang.Number}. See + * {@link #gauge(Id, Number)}. + * + * @param name + * Name of the metric being registered. + * @param number + * Thread-safe implementation of {@link Number} used to access the value. + * @return + * The number that was passed in so the registration can be done as part of an assignment + * statement. + */ + public T gauge(String name, T number) { + return gauge(impl.createId(name), number); + } + + /** + * Register a gauge that reports the value of the {@link java.lang.Number}. See + * {@link #gauge(Id, Number)}. + * + * @param name + * Name of the metric being registered. + * @param tags + * Sequence of dimensions for breaking down the name. + * @param number + * Thread-safe implementation of {@link Number} used to access the value. + * @return + * The number that was passed in so the registration can be done as part of an assignment + * statement. + */ + public T gauge(String name, Iterable tags, T number) { + return gauge(impl.createId(name, tags), number); + } + + /** + * Register a gauge that reports the value of the object after the function + * {@code f} is applied. The registration will keep a weak reference to the number so it will + * not prevent garbage collection. The number implementation used should be thread safe. + * + * @param id + * Identifier for the metric being registered. + * @param obj + * Object used to compute a value. + * @param f + * Function that is applied on the value for the number. + * @return + * The number that was passed in so the registration can be done as part of an assignment + * statement. + */ + public T gauge(Id id, T obj, ValueFunction f) { + impl.register(new ObjectGauge(clock(), id, obj, f)); + return obj; + } + + /** + * Register a gauge that reports the value of the object. See + * {@link #gauge(Id, Object, ValueFunction)}. + * + * @param name + * Name of the metric being registered. + * @param obj + * Object used to compute a value. + * @param f + * Function that is applied on the value for the number. + * @return + * The number that was passed in so the registration can be done as part of an assignment + * statement. + */ + public T gauge(String name, T obj, ValueFunction f) { + return gauge(impl.createId(name), obj, f); + } + + /** + * Register a gauge that reports the size of the {@link java.util.Collection}. The registration + * will keep a weak reference to the collection so it will not prevent garbage collection. + * The collection implementation used should be thread safe. Note that calling + * {@link java.util.Collection#size()} can be expensive for some collection implementations + * and should be considered before registering. + * + * @param id + * Identifier for the metric being registered. + * @param collection + * Thread-safe implementation of {@link Collection} used to access the value. + * @return + * The number that was passed in so the registration can be done as part of an assignment + * statement. + */ + public > T collectionSize(Id id, T collection) { + return gauge(id, collection, Functions.COLLECTION_SIZE); + } + + /** + * Register a gauge that reports the size of the {@link java.util.Collection}. The registration + * will keep a weak reference to the collection so it will not prevent garbage collection. + * The collection implementation used should be thread safe. Note that calling + * {@link java.util.Collection#size()} can be expensive for some collection implementations + * and should be considered before registering. + * + * @param name + * Name of the metric being registered. + * @param collection + * Thread-safe implementation of {@link Collection} used to access the value. + * @return + * The number that was passed in so the registration can be done as part of an assignment + * statement. + */ + public > T collectionSize(String name, T collection) { + return collectionSize(impl.createId(name), collection); + } + + /** + * Register a gauge that reports the size of the {@link java.util.Map}. The registration + * will keep a weak reference to the collection so it will not prevent garbage collection. + * The collection implementation used should be thread safe. Note that calling + * {@link java.util.Map#size()} can be expensive for some collection implementations + * and should be considered before registering. + * + * @param id + * Identifier for the metric being registered. + * @param collection + * Thread-safe implementation of {@link Map} used to access the value. + * @return + * The number that was passed in so the registration can be done as part of an assignment + * statement. + */ + public > T mapSize(Id id, T collection) { + return gauge(id, collection, Functions.MAP_SIZE); + } + + /** + * Register a gauge that reports the size of the {@link java.util.Map}. The registration + * will keep a weak reference to the collection so it will not prevent garbage collection. + * The collection implementation used should be thread safe. Note that calling + * {@link java.util.Map#size()} can be expensive for some collection implementations + * and should be considered before registering. + * + * @param name + * Name of the metric being registered. + * @param collection + * Thread-safe implementation of {@link Map} used to access the value. + * @return + * The number that was passed in so the registration can be done as part of an assignment + * statement. + */ + public > T mapSize(String name, T collection) { + return mapSize(impl.createId(name), collection); + } + + /** + * Register a gauge that reports the return value of invoking the method on the object. The + * registration will keep a weak reference to the object so it will not prevent garbage + * collection. The registered method should be thread safe and cheap to invoke. Any potentially + * long running or expensive activity such as IO should not be performed inline. + * + * @param id + * Identifier for the metric being registered. + * @param obj + * Object used to compute a value. + * @param method + * Name of the method to invoke on the object. + */ + public void methodValue(Id id, Object obj, String method) { + try { + final Method m = getMethod(obj.getClass(), method); + try { + // Make sure we can cast the response to a Number + final Number n = (Number) m.invoke(obj); + LOGGER.debug("registering gauge {}, using method [{}], with initial value {}", id, m, n); + gauge(id, obj, Functions.invokeMethod(m)); + } catch (Exception e) { + final String msg = "exception thrown invoking method [" + m + + "], skipping registration of gauge " + id; + Throwables.propagate(msg, e); + } + } catch (NoSuchMethodException e) { + final String mname = obj.getClass().getName() + "." + method; + final String msg = "invalid method [" + mname + "], skipping registration of gauge " + id; + Throwables.propagate(msg, e); + } + } + + /** + * Register a gauge that reports the return value of invoking the method on the object. The + * registration will keep a weak reference to the object so it will not prevent garbage + * collection. The registered method should be thread safe and cheap to invoke. Any potentially + * long running or expensive activity such as IO should not be performed inline. + * + * @param name + * Name of the metric being registered. + * @param obj + * Object used to compute a value. + * @param method + * Name of the method to invoke on the object. + */ + public void methodValue(String name, Object obj, String method) { + methodValue(impl.createId(name), obj, method); + } + + /** Search for a method in the class and all superclasses. */ + Method getMethod(Class cls, String name) throws NoSuchMethodException { + NoSuchMethodException firstExc = null; + for (Class c = cls; c != null; c = c.getSuperclass()) { + try { + return c.getDeclaredMethod(name); + } catch (NoSuchMethodException e) { + if (firstExc == null) { + firstExc = e; + } + } + } + throw firstExc; + } + + @Override + public String toString() { + return "ExtendedRegistry(impl=" + impl + ')'; + } +} diff --git a/spectator-api/src/main/java/com/netflix/spectator/api/Functions.java b/spectator-api/src/main/java/com/netflix/spectator/api/Functions.java new file mode 100644 index 000000000..7161732fa --- /dev/null +++ b/spectator-api/src/main/java/com/netflix/spectator/api/Functions.java @@ -0,0 +1,113 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.netflix.spectator.api; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.lang.reflect.Method; +import java.util.Collection; +import java.util.Map; + + +/** + * Common functions for use with gauges. + */ +public final class Functions { + + private static final Logger LOGGER = LoggerFactory.getLogger(Functions.class); + + private Functions() { + } + + /** + * Identity function that just returns the passed in value if it implements the + * {@link java.lang.Number} interface. + */ + public static final DoubleFunction IDENTITY = new DoubleFunction() { + public double apply(double v) { + return v; + } + }; + + /** + * Returns the size of the collection. + */ + public static final ValueFunction COLLECTION_SIZE = new ValueFunction() { + public double apply(Object obj) { + return (obj instanceof Collection) ? ((Collection) obj).size() : Double.NaN; + } + }; + + /** + * Returns the size of the map. + */ + public static final ValueFunction MAP_SIZE = new ValueFunction() { + public double apply(Object obj) { + return (obj instanceof Map) ? ((Map) obj).size() : Double.NaN; + } + }; + + /** + * Age function based on the system clock. See {@link #age(Clock)} for more details. + */ + public static final DoubleFunction AGE = age(Clock.SYSTEM); + + /** + * Returns a function that computes the age in seconds. The value passed into the function + * should be a timestamp in milliseconds since the epoch. Typically this will be the return + * value from calling {@link java.lang.System#currentTimeMillis()}. + * + * @param clock + * Clock used to get the current time for comparing with the passed in value. + * @return + * Function that computes the age. + */ + public static DoubleFunction age(final Clock clock) { + return new DoubleFunction() { + public double apply(double t) { + return (clock.wallTime() - t) / 1000.0; + } + }; + } + + /** + * Returns a function that invokes a method on the object via reflection. If an + * exception of any kind occurs then NaN will be returned and the exception will be logged. + * The method must have an empty parameter list and return a primitive number value or a + * subclass of {@link java.lang.Number}. The method will be set accessible so that private + * methods can be used. + * + * @param method + * Method to execute on the passed in object. + * @return + * Value returned by the method or NaN if an exception is thrown. + */ + public static ValueFunction invokeMethod(final Method method) { + method.setAccessible(true); + return new ValueFunction() { + public double apply(Object obj) { + try { + final Number n = (Number) method.invoke(obj); + return n.doubleValue(); + } catch (Exception e) { + LOGGER.warn("exception from method registered as a gauge [" + method + "]", e); + return Double.NaN; + } + } + }; + } +} diff --git a/spectator-api/src/main/java/com/netflix/spectator/api/Gauge.java b/spectator-api/src/main/java/com/netflix/spectator/api/Gauge.java new file mode 100644 index 000000000..69efe1263 --- /dev/null +++ b/spectator-api/src/main/java/com/netflix/spectator/api/Gauge.java @@ -0,0 +1,25 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.netflix.spectator.api; + +/** + * A meter with a single value that can only be sampled at a point in time. A typical example is + * a queue size. + */ +public interface Gauge extends Meter { + /** Returns the current value. */ + double value(); +} diff --git a/spectator-api/src/main/java/com/netflix/spectator/api/Id.java b/spectator-api/src/main/java/com/netflix/spectator/api/Id.java new file mode 100644 index 000000000..12a66452f --- /dev/null +++ b/spectator-api/src/main/java/com/netflix/spectator/api/Id.java @@ -0,0 +1,33 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.netflix.spectator.api; + +/** + * Identifier for a meter or measurement. + */ +public interface Id { + /** Description of the measurement that is being collected. */ + String name(); + + /** Other dimensions that can be used to classify the measurement. */ + Iterable tags(); + + /** New id with an additional tag value. */ + Id withTag(String k, String v); + + /** New id with an additional tag value. */ + Id withTag(Tag t); +} diff --git a/spectator-api/src/main/java/com/netflix/spectator/api/LongTaskTimer.java b/spectator-api/src/main/java/com/netflix/spectator/api/LongTaskTimer.java new file mode 100644 index 000000000..f5b8965dc --- /dev/null +++ b/spectator-api/src/main/java/com/netflix/spectator/api/LongTaskTimer.java @@ -0,0 +1,55 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.netflix.spectator.api; + +/** + * Timer intended to track a small number of long running tasks. Example would be something like + * a batch hadoop job. Though "long running" is a bit subjective the assumption is that anything + * over a minute is long running. + */ +public interface LongTaskTimer extends Meter { + /** + * Start keeping time for a task and return a task id that can be used to look up how long + * it has been running. + */ + long start(); + + /** + * Indicates that a given task has completed. + * + * @param task + * Id for the task to stop. This should be the value returned from {@link #start()}. + * @return + * Duration for the task in nanoseconds. A -1 value will be returned for an unknown task. + */ + long stop(long task); + + /** + * Returns the current duration for a given active task. + * + * @param task + * Id for the task to stop. This should be the value returned from {@link #start()}. + * @return + * Duration for the task in nanoseconds. A -1 value will be returned for an unknown task. + */ + long duration(long task); + + /** Returns the cumulative duration of all current tasks in nanoseconds. */ + long duration(); + + /** Returns the current number of tasks being executed. */ + int activeTasks(); +} diff --git a/spectator-api/src/main/java/com/netflix/spectator/api/ManualClock.java b/spectator-api/src/main/java/com/netflix/spectator/api/ManualClock.java new file mode 100644 index 000000000..f74320b5d --- /dev/null +++ b/spectator-api/src/main/java/com/netflix/spectator/api/ManualClock.java @@ -0,0 +1,68 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.netflix.spectator.api; + +import java.util.concurrent.atomic.AtomicLong; + +/** + * Clock implementation that allows the user to explicitly control the time. Typically used for + * unit tests. + */ +public class ManualClock implements Clock { + + private final AtomicLong wall; + private final AtomicLong monotonic; + + /** Create a new instance. */ + public ManualClock() { + this(0L, 0L); + } + + /** + * Create a new instance. + * + * @param wallInit + * Initial value for the wall time. + * @param monotonicInit + * Initial value for the monotonic time. + */ + public ManualClock(long wallInit, long monotonicInit) { + wall = new AtomicLong(wallInit); + monotonic = new AtomicLong(monotonicInit); + } + + /** {@inheritDoc} */ + @Override + public long wallTime() { + return wall.get(); + } + + /** {@inheritDoc} */ + @Override + public long monotonicTime() { + return monotonic.get(); + } + + /** Set the wall time to the value {@code t}. */ + public void setWallTime(long t) { + wall.set(t); + } + + /** Set the monotonic time to the value {@code t}. */ + public void setMonotonicTime(long t) { + monotonic.set(t); + } +} diff --git a/spectator-api/src/main/java/com/netflix/spectator/api/Measurement.java b/spectator-api/src/main/java/com/netflix/spectator/api/Measurement.java new file mode 100644 index 000000000..fcec7ec2f --- /dev/null +++ b/spectator-api/src/main/java/com/netflix/spectator/api/Measurement.java @@ -0,0 +1,75 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.netflix.spectator.api; + +/** + * A measurement sampled from a meter. + */ +public final class Measurement { + + private final Id id; + private final long timestamp; + private final double value; + + /** Create a new instance. */ + public Measurement(Id id, long timestamp, double value) { + this.id = id; + this.timestamp = timestamp; + this.value = value; + } + + /** Identifier for the measurement. */ + public Id id() { + return id; + } + + /** + * The timestamp in milliseconds since the epoch for when the measurement was taken. + */ + public long timestamp() { + return timestamp; + } + + /** Value for the measurement. */ + public double value() { + return value; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null || !(obj instanceof Measurement)) return false; + Measurement other = (Measurement) obj; + return id.equals(other.id) + && timestamp == other.timestamp + && Double.compare(value, other.value) == 0; + } + + @Override + public int hashCode() { + final int prime = 31; + int hc = prime; + hc = prime * hc + id.hashCode(); + hc = prime * hc + Long.valueOf(timestamp).hashCode(); + hc = prime * hc + Double.valueOf(value).hashCode(); + return hc; + } + + @Override + public String toString() { + return "Measurement(" + id.toString() + "," + timestamp + "," + value + ")"; + } +} diff --git a/spectator-api/src/main/java/com/netflix/spectator/api/Meter.java b/spectator-api/src/main/java/com/netflix/spectator/api/Meter.java new file mode 100644 index 000000000..fa8c6d744 --- /dev/null +++ b/spectator-api/src/main/java/com/netflix/spectator/api/Meter.java @@ -0,0 +1,39 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.netflix.spectator.api; + +/** + * A device for collecting a set of measurements. Note, this interface is only intended to be + * implemented by registry implementations. + */ +public interface Meter { + + /** + * Identifier used to lookup this meter in the registry. + */ + Id id(); + + /** + * Get the set of measurements for this meter. + */ + Iterable measure(); + + /** + * Indicates whether the meter is expired. For example, a counter might expire if there is no + * activity within a given time frame. + */ + boolean hasExpired(); +} diff --git a/spectator-api/src/main/java/com/netflix/spectator/api/NoopCounter.java b/spectator-api/src/main/java/com/netflix/spectator/api/NoopCounter.java new file mode 100644 index 000000000..345d87af0 --- /dev/null +++ b/spectator-api/src/main/java/com/netflix/spectator/api/NoopCounter.java @@ -0,0 +1,58 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.netflix.spectator.api; + +import java.util.Collections; + +/** Counter implementation for the no-op registry. */ +enum NoopCounter implements Counter { + /** Singleton instance. */ + INSTANCE; + + /** {@inheritDoc} */ + @Override + public Id id() { + return NoopId.INSTANCE; + } + + /** {@inheritDoc} */ + @Override + public boolean hasExpired() { + return false; + } + + /** {@inheritDoc} */ + @Override + public void increment() { + } + + /** {@inheritDoc} */ + @Override + public void increment(long amount) { + } + + /** {@inheritDoc} */ + @Override + public Iterable measure() { + return Collections.emptyList(); + } + + /** {@inheritDoc} */ + @Override + public long count() { + return 0L; + } +} diff --git a/spectator-api/src/main/java/com/netflix/spectator/api/NoopDistributionSummary.java b/spectator-api/src/main/java/com/netflix/spectator/api/NoopDistributionSummary.java new file mode 100644 index 000000000..ff3711f76 --- /dev/null +++ b/spectator-api/src/main/java/com/netflix/spectator/api/NoopDistributionSummary.java @@ -0,0 +1,60 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.netflix.spectator.api; + +import java.util.Collections; + +/** Distribution summary implementation for the no-op registry. */ +enum NoopDistributionSummary implements DistributionSummary { + + /** Singleton instance. */ + INSTANCE; + + /** {@inheritDoc} */ + @Override + public Id id() { + return NoopId.INSTANCE; + } + + /** {@inheritDoc} */ + @Override + public boolean hasExpired() { + return false; + } + + /** {@inheritDoc} */ + @Override + public void record(long amount) { + } + + /** {@inheritDoc} */ + @Override + public Iterable measure() { + return Collections.emptyList(); + } + + /** {@inheritDoc} */ + @Override + public long count() { + return 0L; + } + + /** {@inheritDoc} */ + @Override + public long totalAmount() { + return 0L; + } +} diff --git a/spectator-api/src/main/java/com/netflix/spectator/api/NoopId.java b/spectator-api/src/main/java/com/netflix/spectator/api/NoopId.java new file mode 100644 index 000000000..d10544a86 --- /dev/null +++ b/spectator-api/src/main/java/com/netflix/spectator/api/NoopId.java @@ -0,0 +1,57 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.netflix.spectator.api; + +import java.util.Collections; + +/** Id implementation for the no-op registry. */ +final class NoopId implements Id { + /** Singleton instance. */ + static final Id INSTANCE = new NoopId(); + + private NoopId() { + } + + /** {@inheritDoc} */ + @Override + public String name() { + return "noop"; + } + + /** {@inheritDoc} */ + @Override + public Iterable tags() { + return Collections.emptyList(); + } + + /** {@inheritDoc} */ + @Override + public Id withTag(String k, String v) { + return this; + } + + /** {@inheritDoc} */ + @Override + public Id withTag(Tag tag) { + return this; + } + + /** {@inheritDoc} */ + @Override + public String toString() { + return name(); + } +} diff --git a/spectator-api/src/main/java/com/netflix/spectator/api/NoopRegistry.java b/spectator-api/src/main/java/com/netflix/spectator/api/NoopRegistry.java new file mode 100644 index 000000000..5df62851f --- /dev/null +++ b/spectator-api/src/main/java/com/netflix/spectator/api/NoopRegistry.java @@ -0,0 +1,90 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.netflix.spectator.api; + +import java.util.Collections; +import java.util.Iterator; + +/** + * Registry implementation that does nothing. This is typically used to allow for performance tests + * to see how much overhead is being added by instrumentation. This implementation tries to do the + * minimum amount possible without requiring code changes for users. + */ +public final class NoopRegistry implements Registry { + + /** {@inheritDoc} */ + @Override + public Clock clock() { + return Clock.SYSTEM; + } + + /** {@inheritDoc} */ + @Override + public Id createId(String name) { + return NoopId.INSTANCE; + } + + /** {@inheritDoc} */ + @Override + public Id createId(String name, Iterable tags) { + return NoopId.INSTANCE; + } + + /** {@inheritDoc} */ + @Override + public void register(Meter meter) { + } + + /** {@inheritDoc} */ + @Override + public Counter counter(Id id) { + return NoopCounter.INSTANCE; + } + + /** {@inheritDoc} */ + @Override + public DistributionSummary distributionSummary(Id id) { + return NoopDistributionSummary.INSTANCE; + } + + /** {@inheritDoc} */ + @Override + public Timer timer(Id id) { + return NoopTimer.INSTANCE; + } + + /** {@inheritDoc} */ + @Override + public Meter get(Id id) { + return null; + } + + /** {@inheritDoc} */ + @Override + public Iterator iterator() { + return Collections.emptyList().iterator(); + } + + /** {@inheritDoc} */ + @Override + public void addListener(RegistryListener listener) { + } + + /** {@inheritDoc} */ + @Override + public void removeListener(RegistryListener listener) { + } +} diff --git a/spectator-api/src/main/java/com/netflix/spectator/api/NoopTimer.java b/spectator-api/src/main/java/com/netflix/spectator/api/NoopTimer.java new file mode 100644 index 000000000..d75bf1ea1 --- /dev/null +++ b/spectator-api/src/main/java/com/netflix/spectator/api/NoopTimer.java @@ -0,0 +1,73 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.netflix.spectator.api; + +import java.util.Collections; +import java.util.concurrent.Callable; +import java.util.concurrent.TimeUnit; + +/** Counter implementation for the no-op registry. */ +enum NoopTimer implements Timer { + /** Singleton instance. */ + INSTANCE; + + /** {@inheritDoc} */ + @Override + public Id id() { + return NoopId.INSTANCE; + } + + /** {@inheritDoc} */ + @Override + public boolean hasExpired() { + return false; + } + + /** {@inheritDoc} */ + @Override + public void record(long amount, TimeUnit unit) { + } + + /** {@inheritDoc} */ + @Override + public Iterable measure() { + return Collections.emptyList(); + } + + /** {@inheritDoc} */ + @Override + public T record(Callable f) throws Exception { + return f.call(); + } + + /** {@inheritDoc} */ + @Override + public void record(Runnable f) { + f.run(); + } + + /** {@inheritDoc} */ + @Override + public long count() { + return 0L; + } + + /** {@inheritDoc} */ + @Override + public long totalTime() { + return 0L; + } +} diff --git a/spectator-api/src/main/java/com/netflix/spectator/api/ObjectGauge.java b/spectator-api/src/main/java/com/netflix/spectator/api/ObjectGauge.java new file mode 100644 index 000000000..7d2e4418a --- /dev/null +++ b/spectator-api/src/main/java/com/netflix/spectator/api/ObjectGauge.java @@ -0,0 +1,57 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.netflix.spectator.api; + +import java.util.Collections; + +/** + * Gauge that is defined by executing a {@link ValueFunction} on an object. + */ +class ObjectGauge extends AbstractMeter implements Gauge { + + private final ValueFunction f; + + /** + * Create a gauge that samples the provided number for the value. + * + * @param clock + * Clock used for accessing the current time. + * @param id + * Identifier for the gauge. + * @param obj + * {@link Object} used to access the value. + * @param f + * Function that is applied on the value for the number. The operation {@code f.apply(obj)} + * should be thread-safe. + */ + ObjectGauge(Clock clock, Id id, Object obj, ValueFunction f) { + super(clock, id, obj); + this.f = f; + } + + /** {@inheritDoc} */ + @Override + public Iterable measure() { + return Collections.singleton(new Measurement(id, clock.wallTime(), value())); + } + + /** {@inheritDoc} */ + @Override + public double value() { + final Object obj = ref.get(); + return (obj == null) ? Double.NaN : f.apply(obj); + } +} diff --git a/spectator-api/src/main/java/com/netflix/spectator/api/Registry.java b/spectator-api/src/main/java/com/netflix/spectator/api/Registry.java new file mode 100644 index 000000000..eec408768 --- /dev/null +++ b/spectator-api/src/main/java/com/netflix/spectator/api/Registry.java @@ -0,0 +1,99 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.netflix.spectator.api; + +import java.util.Iterator; + +/** + * Registry to manage a set of meters. + */ +public interface Registry extends Iterable { + + /** + * The clock used by the registry for timing events. + */ + Clock clock(); + + /** + * Creates an identifier for a meter. All ids passed into other calls should be created by the + * registry. + * + * @param name + * Description of the measurement that is being collected. + */ + Id createId(String name); + + /** + * Creates an identifier for a meter. All ids passed into other calls should be created by the + * registry. + * + * @param name + * Description of the measurement that is being collected. + * @param tags + * Other dimensions that can be used to classify the measurement. + */ + Id createId(String name, Iterable tags); + + /** + * Add a custom meter to the registry. + */ + void register(Meter meter); + + /** + * Measures the rate of some activity. A counter is for continuously incrementing sources like + * the number of requests that are coming into a server. + * + * @param id + * Identifier created by a call to {@link #createId} + */ + Counter counter(Id id); + + /** + * Measures the rate and variation in amount for some activity. For example, it could be used to + * get insight into the variation in response sizes for requests to a server. + * + * @param id + * Identifier created by a call to {@link #createId} + */ + DistributionSummary distributionSummary(Id id); + + /** + * Measures the rate and time taken for short running tasks. + * + * @param id + * Identifier created by a call to {@link #createId} + */ + Timer timer(Id id); + + /** + * Returns the meter associated with a given id. + * + * @param id + * Identifier for the meter. + * @return + * Instance of the meter or null if there is no match. + */ + Meter get(Id id); + + /** Iterator for traversing the set of meters in the registry. */ + Iterator iterator(); + + /** Add a listener that will get notified whenever a meter is added into the registry. */ + void addListener(RegistryListener listener); + + /** Remove a listener from the registry. */ + void removeListener(RegistryListener listener); +} diff --git a/spectator-api/src/main/java/com/netflix/spectator/api/RegistryListener.java b/spectator-api/src/main/java/com/netflix/spectator/api/RegistryListener.java new file mode 100644 index 000000000..b7b00de53 --- /dev/null +++ b/spectator-api/src/main/java/com/netflix/spectator/api/RegistryListener.java @@ -0,0 +1,25 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.netflix.spectator.api; + + +/** + * Callback interface to receive notifications when meters are added to the registry. + */ +public interface RegistryListener { + /** Invoked when a meter is added to the registry. */ + void onAdd(Meter meter); +} diff --git a/spectator-api/src/main/java/com/netflix/spectator/api/Spectator.java b/spectator-api/src/main/java/com/netflix/spectator/api/Spectator.java new file mode 100644 index 000000000..2cc0f4f6b --- /dev/null +++ b/spectator-api/src/main/java/com/netflix/spectator/api/Spectator.java @@ -0,0 +1,91 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.netflix.spectator.api; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Iterator; +import java.util.ServiceLoader; + +/** + * Static factory used to access the main global registry. The registry class to use can be + * set with the system property spectator.api.registryClass. + */ +public final class Spectator { + + private static final Logger LOGGER = LoggerFactory.getLogger(Spectator.class); + + private static final ExtendedRegistry REGISTRY = + new ExtendedRegistry(newInstance(Config.registryClass())); + + /** + * Create a new registry instance using {@link java.util.ServiceLoader}. If no implementations + * are found the default will be used. + */ + static Registry newInstanceUsingServiceLoader() { + final ServiceLoader loader = ServiceLoader.load(Registry.class); + final Iterator registryIterator = loader.iterator(); + if (registryIterator.hasNext()) { + Registry r = registryIterator.next(); + LOGGER.info("using first registry impl found in classpath: {}", r.getClass().getName()); + return r; + } else { + LOGGER.warn("no registry impl found in classpath, using default"); + return new DefaultRegistry(); + } + } + + /** + * Create a new registry instance using the class name specified by the system property + * {@code spectator.api.registryClass}. If no implementations are found the default will be used. + */ + static Registry newInstanceUsingClassName(String name) { + try { + final Class c = Class.forName(name); + return (Registry) c.newInstance(); + } catch (Exception e) { + final String msg = "failed to instantiate registry class '" + name + + "', falling back to default implementation"; + Throwables.propagate(new RuntimeException(msg, e)); + return new DefaultRegistry(); + } + } + + /** Create a new instance of the registry. */ + static Registry newInstance(String name) { + return Config.SERVICE_LOADER.equals(name) + ? newInstanceUsingServiceLoader() + : newInstanceUsingClassName(name); + } + + /** + * Return the default global registry implementation. The implementation used will depend on the + * system property {@code spectator.api.registryClass}. If not set or set to + * {@code service-loader} the registry class will be determined by scanning the classpath using + * {@link java.util.ServiceLoader}. Otherwise an instance of the classname specified will be + * used. If a registry cannot be loaded the fallback is to use the {@link DefaultRegistry}. + * When {@code spectator.api.propagateWarnings} is set to {@code true} and an explicit class name + * is specified a {@link java.lang.RuntimeException} will be thrown if the specified class cannot + * be used. + */ + public static ExtendedRegistry registry() { + return REGISTRY; + } + + private Spectator() { + } +} diff --git a/spectator-api/src/main/java/com/netflix/spectator/api/Tag.java b/spectator-api/src/main/java/com/netflix/spectator/api/Tag.java new file mode 100644 index 000000000..60fa61b07 --- /dev/null +++ b/spectator-api/src/main/java/com/netflix/spectator/api/Tag.java @@ -0,0 +1,25 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.netflix.spectator.api; + +/** Key/value pair used to classify and drill into measurements. */ +public interface Tag { + /** Key for the tag. */ + String key(); + + /** Value for the tag. */ + String value(); +} diff --git a/spectator-api/src/main/java/com/netflix/spectator/api/TagList.java b/spectator-api/src/main/java/com/netflix/spectator/api/TagList.java new file mode 100644 index 000000000..265358ff9 --- /dev/null +++ b/spectator-api/src/main/java/com/netflix/spectator/api/TagList.java @@ -0,0 +1,151 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.netflix.spectator.api; + +import com.netflix.spectator.impl.Preconditions; + +import java.util.Iterator; +import java.util.Map; +import java.util.NoSuchElementException; + +/** + * A tag list implemented as a singly linked list. Doesn't automatically dedup keys but supports + * a cheap prepend at the call site to allow for inexpensive dynamic ids. + */ +final class TagList implements Iterable, Tag { + + private final String key; + private final String value; + private final TagList next; + private final int hc; + + /** + * Create a new instance with a single pair in the list. + */ + TagList(String key, String value) { + this(key, value, EMPTY); + } + + /** + * Create a new instance with a new tag prepended to the list {@code next}. + */ + TagList(String key, String value, TagList next) { + this.key = Preconditions.checkNotNull(key, "key"); + this.value = Preconditions.checkNotNull(value, "value"); + this.next = next; + this.hc = 31 * (key.hashCode() + value.hashCode() + (next == null ? 23 : next.hashCode())); + } + + /** {@inheritDoc} */ + @Override + public String key() { + return key; + } + + /** {@inheritDoc} */ + @Override + public String value() { + return value; + } + + /** {@inheritDoc} */ + @Override + public Iterator iterator() { + final TagList root = this; + return new Iterator() { + private TagList current = root; + + public boolean hasNext() { + return current != null; + } + + public Tag next() { + if (current == null) { + throw new NoSuchElementException(); + } + Tag tmp = current; + current = current.next; + return tmp; + } + + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + + /** {@inheritDoc} */ + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null || !(obj instanceof TagList)) return false; + TagList other = (TagList) obj; + return key.equals(other.key) && value.equals(other.value) + && (next == other.next || (next != null && next.equals(other.next))); + } + + /** + * This object is immutable and the hash code is precomputed in the constructor. The id object + * is typically created to lookup a Meter based on dynamic dimensions so we assume that it is + * highly likely that the hash code method will be called and that it could be in a fairly + * high volume execution path. + * + * {@inheritDoc} + */ + @Override + public int hashCode() { + return hc; + } + + /** + * Create a new tag list from the key/value pairs in the iterable. + * + * @param tags + * Set of key/value pairs. + * @return + * New tag list with a copy of the data. + */ + public static TagList create(Iterable tags) { + if (tags instanceof TagList) { + return (TagList) tags; + } else { + TagList head = null; + for (Tag t : tags) { + head = new TagList(t.key(), t.value(), head); + } + return head; + } + } + + /** + * Create a new tag list from the key/value pairs in the map. + * + * @param tags + * Set of key/value pairs. + * @return + * New tag list with a copy of the data. + */ + public static TagList create(Map tags) { + TagList head = null; + for (Map.Entry t : tags.entrySet()) { + head = new TagList(t.getKey(), t.getValue(), head); + } + return head; + } + + /** Empty tag list. */ + static final TagList EMPTY = null; +} diff --git a/spectator-api/src/main/java/com/netflix/spectator/api/Throwables.java b/spectator-api/src/main/java/com/netflix/spectator/api/Throwables.java new file mode 100644 index 000000000..33db50adc --- /dev/null +++ b/spectator-api/src/main/java/com/netflix/spectator/api/Throwables.java @@ -0,0 +1,59 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.netflix.spectator.api; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** Helper functions for working with exceptions. */ +final class Throwables { + + private static final Logger LOGGER = LoggerFactory.getLogger(Throwables.class); + + /** + * Log a warning using the message from the exception and if enabled propagate the + * exception {@code t}. + * + * @param t + * Exception to log and optionally propagate. + */ + static void propagate(Throwable t) { + propagate(t.getMessage(), t); + } + + /** + * Log a warning and if enabled propagate the exception {@code t}. The propagation is controlled + * by the system property {@code spectator.api.propagateWarnings}. + * + * @param msg + * Message written out to the log. + * @param t + * Exception to log and optionally propagate. + */ + static void propagate(String msg, Throwable t) { + LOGGER.warn(msg, t); + if (Config.propagateWarnings()) { + if (t instanceof RuntimeException) { + throw (RuntimeException) t; + } else { + throw new RuntimeException(t); + } + } + } + + private Throwables() { + } +} diff --git a/spectator-api/src/main/java/com/netflix/spectator/api/Timer.java b/spectator-api/src/main/java/com/netflix/spectator/api/Timer.java new file mode 100644 index 000000000..6bad272a2 --- /dev/null +++ b/spectator-api/src/main/java/com/netflix/spectator/api/Timer.java @@ -0,0 +1,66 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.netflix.spectator.api; + +import java.util.concurrent.Callable; +import java.util.concurrent.TimeUnit; + +/** + * Timer intended to track a large number of short running events. Example would be something like + * an http request. Though "short running" is a bit subjective the assumption is that it should be + * under a minute. + * + * The precise set of information maintained by the timer depends on the implementation. Most + * should try to provide a consistent implementation of {@link #count()} and {@link #totalTime()}, + * but some implementations may not. In particular, the implementation from {@link NoopRegistry} + * will always return 0. + */ +public interface Timer extends Meter { + /** + * Updates the statistics kept by the counter with the specified amount. + * + * @param amount + * Duration of a single event being measured by this timer. If the amount is less than 0 + * the value will be dropped. + * @param unit + * Time unit for the amount being recorded. + */ + void record(long amount, TimeUnit unit); + + /** + * Executes the callable `f` and records the time taken. + * + * @param f + * Function to execute and measure the execution time. + * @return + * The return value of `f`. + */ + T record(Callable f) throws Exception; + + /** + * Executes the runnable `f` and records the time taken. + * + * @param f + * Function to execute and measure the execution time. + */ + void record(Runnable f); + + /** The number of times that record has been called since this timer was created. */ + long count(); + + /** The total time in nanoseconds of all recorded events since this timer was created. */ + long totalTime(); +} diff --git a/spectator-api/src/main/java/com/netflix/spectator/api/ValueFunction.java b/spectator-api/src/main/java/com/netflix/spectator/api/ValueFunction.java new file mode 100644 index 000000000..25caad0ab --- /dev/null +++ b/spectator-api/src/main/java/com/netflix/spectator/api/ValueFunction.java @@ -0,0 +1,31 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.netflix.spectator.api; + +/** + * Function to extract a double value from an object. + */ +public interface ValueFunction { + /** + * Returns a double value based on the object {@code ref}. + * + * @param ref + * An object to use for extracting the value. + * @return + * Double value based on the object. + */ + double apply(Object ref); +} diff --git a/spectator-api/src/main/java/com/netflix/spectator/api/package-info.java b/spectator-api/src/main/java/com/netflix/spectator/api/package-info.java new file mode 100644 index 000000000..e413c364d --- /dev/null +++ b/spectator-api/src/main/java/com/netflix/spectator/api/package-info.java @@ -0,0 +1,83 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Primary interfaces for working with spectator. To get started, here is a small code sample: + * + *
+ * Server s = new Server(Spectator.registry());
+ *
+ * class Server {
+ *   private final ExtendedRegistry registry;
+ *   private final Id requestCountId;
+ *   private final Timer requestLatency;
+ *   private final DistributionSummary responseSizes;
+ *
+ *   public Server(ExtendedRegistry registry) {
+ *     this.registry = registry;
+ *     requestCountId = registry.createId("server.requestCount");
+ *     requestLatency = registry.timer("server.requestLatency");
+ *     responseSizes = registry.distributionSummary("server.responseSizes");
+ *     registry.methodValue("server.numConnections", this, "getNumConnections");
+ *   }
+ *
+ *   public Response handle(Request req) {
+ *     final long s = System.nanoTime();
+ *     try {
+ *       Response res = doSomething(req);
+ *
+ *       final Id cntId = requestCountId
+ *         .withTag("country", req.country())
+ *         .withTag("status", res.status());
+ *       registry.counter(cntId).increment();
+ *
+ *       responseSizes.record(res.body().size());
+ *
+ *       return res;
+ *     } catch (Exception e) {
+ *       final Id cntId = requestCountId
+ *         .withTag("country", req.country())
+ *         .withTag("status", "exception")
+ *         .withTag("error", e.getClass().getSimpleName());
+ *       registry.counter(cntId).increment();
+ *       throw e;
+ *     } finally {
+ *       requestLatency.record(System.nanoTime() - s, TimeUnit.NANOSECONDS);
+ *     }
+ *   }
+ *
+ *   public int getNumConnections() {
+ *     // however we determine the current number of connections on the server
+ *   }
+ * }
+ * 
+ * + * The main classes you will need to understand: + * + *
    + *
  • {@link com.netflix.spectator.api.Spectator}: static entrypoint to access the registry.
  • + *
  • {@link com.netflix.spectator.api.ExtendedRegistry}: registry class used to create + * meters.
  • + *
  • {@link com.netflix.spectator.api.Counter}: meter type for measuring a rate of change.
  • + *
  • {@link com.netflix.spectator.api.Timer}: meter type for measuring the time for many short + * events.
  • + *
  • {@link com.netflix.spectator.api.LongTaskTimer}: meter type for measuring the time for a + * few long events.
  • + *
  • {@link com.netflix.spectator.api.DistributionSummary}: meter type for measuring the sample + * distribution of some type of events.
  • + *
+ */ +package com.netflix.spectator.api; diff --git a/spectator-api/src/main/java/com/netflix/spectator/impl/Preconditions.java b/spectator-api/src/main/java/com/netflix/spectator/impl/Preconditions.java new file mode 100644 index 000000000..4d46a892b --- /dev/null +++ b/spectator-api/src/main/java/com/netflix/spectator/impl/Preconditions.java @@ -0,0 +1,46 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.netflix.spectator.impl; + +/** + * Internal convenience methods that help a method or constructor check whether it was invoked + * correctly. Please notice that this should be considered an internal implementation detail, and + * it is subject to change without notice. + */ +public final class Preconditions { + private Preconditions() { + } + + /** + * Ensures the object reference is not null. + */ + public static T checkNotNull(T obj, String name) { + if (obj == null) { + String msg = String.format("parameter '%s' cannot be null", name); + throw new NullPointerException(msg); + } + return obj; + } + + /** + * Ensures the truth of an expression involving the state of the calling instance. + */ + public static void checkState(boolean expression, String errMsg) { + if (!expression) { + throw new IllegalStateException(errMsg); + } + } +} diff --git a/spectator-api/src/test/java/com/netflix/spectator/api/DefaultCounterTest.java b/spectator-api/src/test/java/com/netflix/spectator/api/DefaultCounterTest.java new file mode 100644 index 000000000..b7344702a --- /dev/null +++ b/spectator-api/src/test/java/com/netflix/spectator/api/DefaultCounterTest.java @@ -0,0 +1,63 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.netflix.spectator.api; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class DefaultCounterTest { + + private final ManualClock clock = new ManualClock(); + + @Test + public void testInit() { + Counter c = new DefaultCounter(clock, NoopId.INSTANCE); + Assert.assertEquals(c.count(), 0L); + } + + @Test + public void testIncrement() { + Counter c = new DefaultCounter(clock, NoopId.INSTANCE); + c.increment(); + Assert.assertEquals(c.count(), 1L); + c.increment(); + c.increment(); + Assert.assertEquals(c.count(), 3L); + } + + @Test + public void testIncrementAmount() { + Counter c = new DefaultCounter(clock, NoopId.INSTANCE); + c.increment(42); + Assert.assertEquals(c.count(), 42L); + } + + @Test + public void testMeasure() { + Counter c = new DefaultCounter(clock, NoopId.INSTANCE); + c.increment(42); + clock.setWallTime(3712345L); + for (Measurement m : c.measure()) { + Assert.assertEquals(m.id(), c.id()); + Assert.assertEquals(m.timestamp(), 3712345L); + Assert.assertEquals(m.value(), 42.0, 0.1e-12); + } + } + +} diff --git a/spectator-api/src/test/java/com/netflix/spectator/api/DefaultDistributionSummaryTest.java b/spectator-api/src/test/java/com/netflix/spectator/api/DefaultDistributionSummaryTest.java new file mode 100644 index 000000000..fa61feeb0 --- /dev/null +++ b/spectator-api/src/test/java/com/netflix/spectator/api/DefaultDistributionSummaryTest.java @@ -0,0 +1,60 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.netflix.spectator.api; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class DefaultDistributionSummaryTest { + + private final ManualClock clock = new ManualClock(); + + @Test + public void testInit() { + DistributionSummary t = new DefaultDistributionSummary(clock, NoopId.INSTANCE); + Assert.assertEquals(t.count(), 0L); + Assert.assertEquals(t.totalAmount(), 0L); + } + + @Test + public void testRecord() { + DistributionSummary t = new DefaultDistributionSummary(clock, NoopId.INSTANCE); + t.record(42); + Assert.assertEquals(t.count(), 1L); + Assert.assertEquals(t.totalAmount(), 42L); + } + + @Test + public void testMeasure() { + DistributionSummary t = new DefaultDistributionSummary(clock, new DefaultId("foo")); + t.record(42); + clock.setWallTime(3712345L); + for (Measurement m : t.measure()) { + Assert.assertEquals(m.timestamp(), 3712345L); + if (m.id().equals(t.id().withTag("statistic", "count"))) { + Assert.assertEquals(m.value(), 1.0, 0.1e-12); + } else if (m.id().equals(t.id().withTag("statistic", "totalAmount"))) { + Assert.assertEquals(m.value(), 42.0, 0.1e-12); + } else { + Assert.fail("unexpected id: " + m.id()); + } + } + } + +} diff --git a/spectator-api/src/test/java/com/netflix/spectator/api/DefaultIdTest.java b/spectator-api/src/test/java/com/netflix/spectator/api/DefaultIdTest.java new file mode 100644 index 000000000..2daa8098f --- /dev/null +++ b/spectator-api/src/test/java/com/netflix/spectator/api/DefaultIdTest.java @@ -0,0 +1,98 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.netflix.spectator.api; + +import nl.jqno.equalsverifier.EqualsVerifier; +import nl.jqno.equalsverifier.Warning; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.util.HashSet; +import java.util.Set; + +@RunWith(JUnit4.class) +public class DefaultIdTest { + + @Test(expected = NullPointerException.class) + public void testNullName() { + new DefaultId(null); + } + + @Test + public void testName() { + Id id = new DefaultId("foo"); + Assert.assertEquals(id.name(), "foo"); + } + + @Test + public void testTags() { + TagList ts = new TagList("k1", "v1"); + Id id = new DefaultId("foo", ts); + Assert.assertEquals(id.name(), "foo"); + Assert.assertEquals(id.tags(), ts); + } + + @Test + public void testTagsEmpty() { + Id id = new DefaultId("foo"); + Assert.assertTrue(!id.tags().iterator().hasNext()); + } + + @Test + public void equalsContractTest() { + TagList ts1 = new TagList("k1", "v1"); + TagList ts2 = new TagList("k2", "v2", ts1); + EqualsVerifier + .forClass(DefaultId.class) + .withPrefabValues(TagList.class, ts1, ts2) + .suppress(Warning.NULL_FIELDS) + .verify(); + } + + @Test + public void testNormalize() { + DefaultId id12 = (new DefaultId("foo")).withTag("k1", "v1").withTag("k2", "v2"); + DefaultId id21 = (new DefaultId("foo")).withTag("k2", "v2").withTag("k1", "v1"); + Assert.assertTrue(!id12.equals(id21)); + Assert.assertEquals(id12, id21.normalize()); + } + + @Test + public void testRollup() { + Set keys = new HashSet<>(); + keys.add("k1"); + keys.add("foo"); + DefaultId id = (new DefaultId("foo")).withTag("k1", "v1").withTag("k2", "v2"); + DefaultId keepId = (new DefaultId("foo")).withTag("k1", "v1"); + DefaultId dropId = (new DefaultId("foo")).withTag("k2", "v2"); + Assert.assertEquals(keepId, id.rollup(keys, true)); + Assert.assertEquals(dropId, id.rollup(keys, false)); + } + + @Test + public void testToString() { + DefaultId id = (new DefaultId("foo")).withTag("k1", "v1").withTag("k2", "v2"); + Assert.assertEquals(id.toString(), "foo:k2=v2:k1=v1"); + } + + @Test + public void testToStringNameOnly() { + DefaultId id = new DefaultId("foo"); + Assert.assertEquals(id.toString(), "foo"); + } +} diff --git a/spectator-api/src/test/java/com/netflix/spectator/api/DefaultLongTaskTimerTest.java b/spectator-api/src/test/java/com/netflix/spectator/api/DefaultLongTaskTimerTest.java new file mode 100644 index 000000000..897003438 --- /dev/null +++ b/spectator-api/src/test/java/com/netflix/spectator/api/DefaultLongTaskTimerTest.java @@ -0,0 +1,95 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.netflix.spectator.api; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class DefaultLongTaskTimerTest { + private final ManualClock clock = new ManualClock(); + + @Test + public void testInit() { + LongTaskTimer t = new DefaultLongTaskTimer(clock, NoopId.INSTANCE); + Assert.assertEquals(t.duration(), 0L); + Assert.assertEquals(t.activeTasks(), 0L); + } + + + @Test + public void testStart() { + LongTaskTimer t = new DefaultLongTaskTimer(clock, NoopId.INSTANCE); + + long task1 = t.start(); + long task2 = t.start(); + + Assert.assertFalse(task1 == task2); + Assert.assertEquals(t.activeTasks(), 2); + Assert.assertEquals(t.duration(), 0L); + } + + @Test + public void testStop() { + LongTaskTimer t = new DefaultLongTaskTimer(clock, NoopId.INSTANCE); + + long task1 = t.start(); + long task2 = t.start(); + + Assert.assertEquals(t.activeTasks(), 2); + clock.setMonotonicTime(5L); + Assert.assertEquals(t.duration(), 10L); + + long elapsed1 = t.stop(task1); + Assert.assertEquals(elapsed1, 5L); + Assert.assertEquals(t.duration(task2), 5L); + Assert.assertEquals(t.duration(task1), -1L); // task is gone, should return default + Assert.assertEquals(t.duration(), 5L); + } + + static void assertLongTaskTimer(Meter t, long timestamp, int activeTasks, double duration) { + for (Measurement m : t.measure()) { + Assert.assertEquals(m.timestamp(), timestamp); + if (m.id().equals(t.id().withTag("statistic", "activeTasks"))) { + Assert.assertEquals(m.value(), activeTasks, 1.0e-12); + } else if (m.id().equals(t.id().withTag("statistic", "duration"))) { + Assert.assertEquals(m.value(), duration, 1.0e-12); + } else { + Assert.fail("unexpected id: " + m.id()); + } + } + } + + @Test + public void testMeasure() { + LongTaskTimer t = new DefaultLongTaskTimer(clock, new DefaultId("foo")); + long task1 = t.start(); + clock.setMonotonicTime(1_000_000_000L); + clock.setWallTime(1L); + assertLongTaskTimer(t, 1L, 1, 1.0); + + long task2 = t.start(); + assertLongTaskTimer(t, 1L, 2, 1.0); + + t.stop(task1); + assertLongTaskTimer(t, 1L, 1, 0.0); + + t.stop(task2); + assertLongTaskTimer(t, 1L, 0, 0.0); + } +} diff --git a/spectator-api/src/test/java/com/netflix/spectator/api/DefaultRegistryTest.java b/spectator-api/src/test/java/com/netflix/spectator/api/DefaultRegistryTest.java new file mode 100644 index 000000000..b9a20f7e0 --- /dev/null +++ b/spectator-api/src/test/java/com/netflix/spectator/api/DefaultRegistryTest.java @@ -0,0 +1,267 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.netflix.spectator.api; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +@RunWith(JUnit4.class) +public class DefaultRegistryTest { + + private final ManualClock clock = new ManualClock(); + + @Before + public void init() { + System.setProperty("spectator.api.propagateWarnings", "true"); + System.setProperty("spectator.api.maxNumberOfMeters", "10000"); + } + + @Test + public void testCreateId() { + Registry r = new DefaultRegistry(clock); + Assert.assertEquals(r.createId("foo"), new DefaultId("foo")); + } + + @Test + public void testCreateIdWithTags() { + Registry r = new DefaultRegistry(clock); + TagList ts = new TagList("k", "v"); + Assert.assertEquals(r.createId("foo", ts), new DefaultId("foo", ts)); + } + + @Test + public void testRegister() { + Registry r = new DefaultRegistry(clock); + Counter c = new DefaultCounter(clock, r.createId("foo")); + r.register(c); + c.increment(); + Assert.assertEquals(c.count(), 1L); + r.register(c); + Meter meter = r.get(c.id()); + for (Measurement m : meter.measure()) { + Assert.assertEquals(m.value(), 2.0, 1e-12); + } + } + + @Test + public void testCounter() { + Registry r = new DefaultRegistry(clock); + Counter c = r.counter(r.createId("foo")); + c.increment(); + Assert.assertEquals(c.count(), 1L); + + Counter c2 = r.counter(r.createId("foo")); + Assert.assertSame(c, c2); + } + + @Test + public void testTimer() { + Registry r = new DefaultRegistry(clock); + Timer t = r.timer(r.createId("foo")); + t.record(42L, TimeUnit.MILLISECONDS); + Assert.assertEquals(t.count(), 1L); + + Timer t2 = r.timer(r.createId("foo")); + Assert.assertSame(t, t2); + } + + @Test + public void testDistributionSummary() { + Registry r = new DefaultRegistry(clock); + DistributionSummary t = r.distributionSummary(r.createId("foo")); + t.record(42L); + Assert.assertEquals(t.count(), 1L); + + DistributionSummary t2 = r.distributionSummary(r.createId("foo")); + Assert.assertSame(t, t2); + } + + @Test(expected = IllegalStateException.class) + public void testRegisterBadTypeAccess() { + Registry r = new DefaultRegistry(clock); + Counter c = new DefaultCounter(clock, r.createId("foo")); + r.register(c); + r.counter(c.id()); + } + + @Test(expected = IllegalStateException.class) + public void testCounterBadTypeAccess() { + Registry r = new DefaultRegistry(clock); + r.counter(r.createId("foo")); + r.distributionSummary(r.createId("foo")); + } + + @Test(expected = IllegalStateException.class) + public void testTimerBadTypeAccess() { + Registry r = new DefaultRegistry(clock); + r.timer(r.createId("foo")); + r.counter(r.createId("foo")); + } + + @Test(expected = IllegalStateException.class) + public void testDistributionSummaryBadTypeAccess() { + Registry r = new DefaultRegistry(clock); + r.distributionSummary(r.createId("foo")); + r.timer(r.createId("foo")); + } + + @Test + public void testRegisterBadTypeAccessNoThrow() { + System.setProperty("spectator.api.propagateWarnings", "false"); + Registry r = new DefaultRegistry(clock); + Counter c = new DefaultCounter(clock, r.createId("foo")); + r.counter(c.id()); + r.register(c); + Assert.assertNotSame(r.get(c.id()), c); + } + + @Test + public void testCounterBadTypeAccessNoThrow() { + System.setProperty("spectator.api.propagateWarnings", "false"); + Registry r = new DefaultRegistry(clock); + r.counter(r.createId("foo")); + Assert.assertEquals(r.distributionSummary(r.createId("foo")), NoopDistributionSummary.INSTANCE); + } + + @Test + public void testTimerBadTypeAccessNoThrow() { + System.setProperty("spectator.api.propagateWarnings", "false"); + Registry r = new DefaultRegistry(clock); + r.timer(r.createId("foo")); + Assert.assertEquals(r.counter(r.createId("foo")), NoopCounter.INSTANCE); + } + + @Test + public void testDistributionSummaryBadTypeAccessNoThrow() { + System.setProperty("spectator.api.propagateWarnings", "false"); + Registry r = new DefaultRegistry(clock); + r.distributionSummary(r.createId("foo")); + Assert.assertEquals(r.timer(r.createId("foo")), NoopTimer.INSTANCE); + } + + @Test + public void testMaxLimitExceededCounter() { + System.setProperty("spectator.api.maxNumberOfMeters", "1"); + Registry r = new DefaultRegistry(clock); + Assert.assertNotSame(r.counter(r.createId("c1")), NoopCounter.INSTANCE); + Assert.assertSame(r.counter(r.createId("c2")), NoopCounter.INSTANCE); + Assert.assertNotSame(r.counter(r.createId("c1")), NoopCounter.INSTANCE); + } + + @Test + public void testMaxLimitExceededTimer() { + System.setProperty("spectator.api.maxNumberOfMeters", "1"); + Registry r = new DefaultRegistry(clock); + Assert.assertNotSame(r.timer(r.createId("c1")), NoopTimer.INSTANCE); + Assert.assertSame(r.timer(r.createId("c2")), NoopTimer.INSTANCE); + Assert.assertNotSame(r.timer(r.createId("c1")), NoopTimer.INSTANCE); + } + + @Test + public void testMaxLimitExceededDistributionSummary() { + System.setProperty("spectator.api.maxNumberOfMeters", "1"); + Registry r = new DefaultRegistry(clock); + Assert.assertNotSame(r.distributionSummary(r.createId("c1")), NoopDistributionSummary.INSTANCE); + Assert.assertSame(r.distributionSummary(r.createId("c2")), NoopDistributionSummary.INSTANCE); + Assert.assertNotSame(r.distributionSummary(r.createId("c1")), NoopDistributionSummary.INSTANCE); + } + + @Test + public void testMaxLimitExceededRegister() { + final AtomicInteger count = new AtomicInteger(0); + RegistryListener listener = new RegistryListener() { + public void onAdd(Meter m) { + count.incrementAndGet(); + } + }; + + System.setProperty("spectator.api.maxNumberOfMeters", "1"); + Registry r = new DefaultRegistry(clock); + r.addListener(listener); + Assert.assertEquals(count.get(), 0); + r.register(new DefaultCounter(clock, r.createId("c1"))); + Assert.assertEquals(count.get(), 1); + r.register(new DefaultCounter(clock, r.createId("c2"))); + Assert.assertEquals(count.get(), 1); + r.register(new DefaultCounter(clock, r.createId("c1"))); + Assert.assertEquals(count.get(), 2); + } + + @Test + public void testGet() { + Registry r = new DefaultRegistry(clock); + Counter c = r.counter(r.createId("foo")); + Meter m = r.get(c.id()); + Assert.assertSame(c, m); + } + + @Test + public void testIteratorEmpty() { + Registry r = new DefaultRegistry(clock); + for (Meter m : r) { + Assert.fail("should be empty, but found " + m.id()); + } + } + + @Test + public void testIterator() { + Registry r = new DefaultRegistry(clock); + r.counter(r.createId("foo")); + r.counter(r.createId("bar")); + Set expected = new HashSet<>(); + expected.add(r.createId("foo")); + expected.add(r.createId("bar")); + for (Meter m : r) { + expected.remove(m.id()); + } + Assert.assertTrue(expected.isEmpty()); + } + + @Test + public void testListener() { + final Set seen = new HashSet<>(); + RegistryListener listener = new RegistryListener() { + public void onAdd(Meter m) { + Assert.assertTrue(!seen.contains(m.id())); + seen.add(m.id()); + } + }; + + Registry r = new DefaultRegistry(clock); + r.counter(r.createId("pre")); + r.addListener(listener); + r.counter(r.createId("foo")); + r.timer(r.createId("bar")); + r.distributionSummary(r.createId("baz")); + r.removeListener(listener); + r.counter(r.createId("post")); + + Set expected = new HashSet<>(); + expected.add(r.createId("foo")); + expected.add(r.createId("bar")); + expected.add(r.createId("baz")); + + Assert.assertEquals(expected, seen); + } +} diff --git a/spectator-api/src/test/java/com/netflix/spectator/api/DefaultTimerTest.java b/spectator-api/src/test/java/com/netflix/spectator/api/DefaultTimerTest.java new file mode 100644 index 000000000..b7bf723b9 --- /dev/null +++ b/spectator-api/src/test/java/com/netflix/spectator/api/DefaultTimerTest.java @@ -0,0 +1,131 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.netflix.spectator.api; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.util.concurrent.Callable; +import java.util.concurrent.TimeUnit; + +@RunWith(JUnit4.class) +public class DefaultTimerTest { + + private final ManualClock clock = new ManualClock(); + + @Test + public void testInit() { + Timer t = new DefaultTimer(clock, NoopId.INSTANCE); + Assert.assertEquals(t.count(), 0L); + Assert.assertEquals(t.totalTime(), 0L); + } + + @Test + public void testRecord() { + Timer t = new DefaultTimer(clock, NoopId.INSTANCE); + t.record(42, TimeUnit.MILLISECONDS); + Assert.assertEquals(t.count(), 1L); + Assert.assertEquals(t.totalTime(), 42000000L); + } + + @Test + public void testRecordCallable() throws Exception { + Timer t = new DefaultTimer(clock, NoopId.INSTANCE); + clock.setMonotonicTime(100L); + int v = t.record(new Callable() { + public Integer call() throws Exception { + clock.setMonotonicTime(500L); + return 42; + } + }); + Assert.assertEquals(v, 42); + Assert.assertEquals(t.count(), 1L); + Assert.assertEquals(t.totalTime(), 400L); + } + + @Test + public void testRecordCallableException() throws Exception { + Timer t = new DefaultTimer(clock, NoopId.INSTANCE); + clock.setMonotonicTime(100L); + boolean seen = false; + try { + t.record(new Callable() { + public Integer call() throws Exception { + clock.setMonotonicTime(500L); + throw new RuntimeException("foo"); + } + }); + } catch (Exception e) { + seen = true; + } + Assert.assertTrue(seen); + Assert.assertEquals(t.count(), 1L); + Assert.assertEquals(t.totalTime(), 400L); + } + + @Test + public void testRecordRunnable() throws Exception { + Timer t = new DefaultTimer(clock, NoopId.INSTANCE); + clock.setMonotonicTime(100L); + t.record(new Runnable() { + public void run() { + clock.setMonotonicTime(500L); + } + }); + Assert.assertEquals(t.count(), 1L); + Assert.assertEquals(t.totalTime(), 400L); + } + + @Test + public void testRecordRunnableException() throws Exception { + Timer t = new DefaultTimer(clock, NoopId.INSTANCE); + clock.setMonotonicTime(100L); + boolean seen = false; + try { + t.record(new Runnable() { + public void run() { + clock.setMonotonicTime(500L); + throw new RuntimeException("foo"); + } + }); + } catch (Exception e) { + seen = true; + } + Assert.assertTrue(seen); + Assert.assertEquals(t.count(), 1L); + Assert.assertEquals(t.totalTime(), 400L); + } + + @Test + public void testMeasure() { + Timer t = new DefaultTimer(clock, new DefaultId("foo")); + t.record(42, TimeUnit.MILLISECONDS); + clock.setWallTime(3712345L); + for (Measurement m : t.measure()) { + Assert.assertEquals(m.timestamp(), 3712345L); + if (m.id().equals(t.id().withTag("statistic", "count"))) { + Assert.assertEquals(m.value(), 1.0, 0.1e-12); + } else if (m.id().equals(t.id().withTag("statistic", "totalTime"))) { + Assert.assertEquals(m.value(), 42e6, 0.1e-12); + } else { + Assert.fail("unexpected id: " + m.id()); + } + } + } + +} diff --git a/spectator-api/src/test/java/com/netflix/spectator/api/ExtendedRegistryTest.java b/spectator-api/src/test/java/com/netflix/spectator/api/ExtendedRegistryTest.java new file mode 100644 index 000000000..65dfd5a5c --- /dev/null +++ b/spectator-api/src/test/java/com/netflix/spectator/api/ExtendedRegistryTest.java @@ -0,0 +1,192 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.netflix.spectator.api; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.atomic.AtomicLong; + +@RunWith(JUnit4.class) +public class ExtendedRegistryTest { + + @Test + public void testCreateIdArray() { + ExtendedRegistry r = new ExtendedRegistry(new DefaultRegistry()); + Id id1 = r.createId("foo", "bar", "baz", "k", "v"); + Id id2 = r.createId("foo", new TagList("k", "v", new TagList("bar", "baz"))); + Assert.assertEquals(id1, id2); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateIdArrayOdd() { + ExtendedRegistry r = new ExtendedRegistry(new DefaultRegistry()); + r.createId("foo", "bar", "baz", "k"); + } + + @Test + public void testCounterHelpers() { + ExtendedRegistry r = new ExtendedRegistry(new DefaultRegistry()); + Counter c1 = r.counter("foo", "bar", "baz", "k", "v"); + Counter c2 = r.counter("foo", new TagList("k", "v", new TagList("bar", "baz"))); + Counter c3 = r.counter("foo"); + Assert.assertSame(c1, c2); + Assert.assertNotSame(c1, c3); + } + + @Test + public void testDistributionSummaryHelpers() { + ExtendedRegistry r = new ExtendedRegistry(new DefaultRegistry()); + DistributionSummary c1 = r.distributionSummary("foo", "bar", "baz", "k", "v"); + DistributionSummary c2 = r.distributionSummary("foo", + new TagList("k", "v", new TagList("bar", "baz"))); + DistributionSummary c3 = r.distributionSummary("foo"); + Assert.assertSame(c1, c2); + Assert.assertNotSame(c1, c3); + } + + @Test + public void testTimerHelpers() { + ExtendedRegistry r = new ExtendedRegistry(new DefaultRegistry()); + Timer c1 = r.timer("foo", "bar", "baz", "k", "v"); + Timer c2 = r.timer("foo", new TagList("k", "v", new TagList("bar", "baz"))); + Timer c3 = r.timer("foo"); + Assert.assertSame(c1, c2); + Assert.assertNotSame(c1, c3); + } + + @Test + public void testLongTaskTimerHelpers() { + ManualClock clock = new ManualClock(); + ExtendedRegistry r = new ExtendedRegistry(new DefaultRegistry(clock)); + LongTaskTimer c1 = r.longTaskTimer("foo", "bar", "baz", "k", "v"); + Meter m1 = r.get(c1.id()); + Assert.assertEquals(c1.id(), m1.id()); // registration + + LongTaskTimer c2 = r.longTaskTimer("foo", new TagList("k", "v", new TagList("bar", "baz"))); + Assert.assertEquals(c1.id(), c2.id()); + + long t1 = c1.start(); + long t2 = c2.start(); + clock.setMonotonicTime(1000L); + clock.setWallTime(1L); + DefaultLongTaskTimerTest.assertLongTaskTimer(r.get(c1.id()), 1L, 2, 2.0e-6); + + c1.stop(t1); + DefaultLongTaskTimerTest.assertLongTaskTimer(r.get(c1.id()), 1L, 1, 1.0e-6); + + c2.stop(t2); + DefaultLongTaskTimerTest.assertLongTaskTimer(r.get(c1.id()), 1L, 0, 0L); + } + + @Test + public void testGaugeHelpers() { + AtomicLong al1 = new AtomicLong(1L); + AtomicLong al2 = new AtomicLong(2L); + AtomicLong al4 = new AtomicLong(4L); + ExtendedRegistry r = new ExtendedRegistry(new DefaultRegistry()); + AtomicLong v1 = r.gauge(r.createId("foo", "bar", "baz", "k", "v"), al1); + AtomicLong v2 = r.gauge("foo", new TagList("k", "v", new TagList("bar", "baz")), al2); + AtomicLong v3 = r.gauge("foo", al4); + Assert.assertSame(v1, al1); + Assert.assertSame(v2, al2); + Assert.assertSame(v3, al4); + Id id1 = r.createId("foo", "bar", "baz", "k", "v"); + Id id2 = r.createId("foo"); + Assert.assertEquals(r.get(id1).measure().iterator().next().value(), 3.0, 1e-12); + Assert.assertEquals(r.get(id2).measure().iterator().next().value(), 4.0, 1e-12); + } + + @Test + public void testGaugeHelpersWithFunction() { + AtomicLong al1 = new AtomicLong(1L); + ExtendedRegistry r = new ExtendedRegistry(new DefaultRegistry(new ManualClock(40, 0))); + DoubleFunction f = Functions.age(r.clock()); + AtomicLong v1 = r.gauge("foo", al1, f); + Assert.assertSame(v1, al1); + Id id1 = r.createId("foo"); + Assert.assertEquals(r.get(id1).measure().iterator().next().value(), 39.0 / 1000.0, 1e-12); + } + + @Test + public void testCollectionSizeHelpers() { + ExtendedRegistry r = new ExtendedRegistry(new DefaultRegistry()); + LinkedBlockingDeque q1 = new LinkedBlockingDeque<>(); + LinkedBlockingDeque q2 = r.collectionSize("queueSize", q1); + Assert.assertSame(q1, q2); + Id id = r.createId("queueSize"); + Assert.assertEquals(r.get(id).measure().iterator().next().value(), 0.0, 1e-12); + q2.push("foo"); + Assert.assertEquals(r.get(id).measure().iterator().next().value(), 1.0, 1e-12); + } + + @Test + public void testMapSizeHelpers() { + ExtendedRegistry r = new ExtendedRegistry(new DefaultRegistry()); + ConcurrentHashMap q1 = new ConcurrentHashMap<>(); + ConcurrentHashMap q2 = r.mapSize("mapSize", q1); + Assert.assertSame(q1, q2); + Id id = r.createId("mapSize"); + Assert.assertEquals(r.get(id).measure().iterator().next().value(), 0.0, 1e-12); + q2.put("foo", "bar"); + Assert.assertEquals(r.get(id).measure().iterator().next().value(), 1.0, 1e-12); + } + + @Test + public void testMethodValueHelpers() { + ExtendedRegistry r = new ExtendedRegistry(new DefaultRegistry()); + LinkedBlockingDeque q1 = new LinkedBlockingDeque<>(); + r.methodValue("queueSize", q1, "size"); + Id id = r.createId("queueSize"); + Assert.assertEquals(r.get(id).measure().iterator().next().value(), 0.0, 1e-12); + q1.push("foo"); + Assert.assertEquals(r.get(id).measure().iterator().next().value(), 1.0, 1e-12); + } + + @Test(expected = ClassCastException.class) + public void methodValueBadReturnType() { + System.setProperty("spectator.api.propagateWarnings", "true"); + ExtendedRegistry r = new ExtendedRegistry(new DefaultRegistry()); + r.methodValue("queueSize", this, "toString"); + } + + @Test + public void methodValueBadReturnTypeNoPropagate() { + System.setProperty("spectator.api.propagateWarnings", "false"); + ExtendedRegistry r = new ExtendedRegistry(new DefaultRegistry()); + r.methodValue("queueSize", this, "toString"); + Assert.assertNull(r.get(r.createId("queueSize"))); + } + + @Test(expected = RuntimeException.class) + public void methodValueUnknown() { + System.setProperty("spectator.api.propagateWarnings", "true"); + ExtendedRegistry r = new ExtendedRegistry(new DefaultRegistry()); + r.methodValue("queueSize", this, "unknownMethod"); + } + + @Test + public void methodValueUnknownNoPropagate() { + System.setProperty("spectator.api.propagateWarnings", "false"); + ExtendedRegistry r = new ExtendedRegistry(new DefaultRegistry()); + r.methodValue("queueSize", this, "unknownMethod"); + Assert.assertNull(r.get(r.createId("queueSize"))); + } +} diff --git a/spectator-api/src/test/java/com/netflix/spectator/api/FunctionsTest.java b/spectator-api/src/test/java/com/netflix/spectator/api/FunctionsTest.java new file mode 100644 index 000000000..6c587994b --- /dev/null +++ b/spectator-api/src/test/java/com/netflix/spectator/api/FunctionsTest.java @@ -0,0 +1,135 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.netflix.spectator.api; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class FunctionsTest { + + private final ManualClock clock = new ManualClock(); + private final ExtendedRegistry registry = new ExtendedRegistry(new DefaultRegistry()); + + @Test + public void ageFunction() { + clock.setWallTime(5000L); + final DoubleFunction f = Functions.age(clock); + Assert.assertEquals(f.apply(1000L), 4.0, 1e-12); + } + + private byte byteMethod() { + return (byte) 1; + } + + @Test + public void invokeMethodByte() throws Exception { + final ValueFunction f = Functions.invokeMethod(registry.getMethod(getClass(), "byteMethod")); + Assert.assertEquals(f.apply(this), 1.0, 1e-12); + } + + private short shortMethod() { + return (short) 2; + } + + @Test + public void invokeMethodShort() throws Exception { + final ValueFunction f = Functions.invokeMethod(registry.getMethod(getClass(), "shortMethod")); + Assert.assertEquals(f.apply(this), 2.0, 1e-12); + } + + private int intMethod() { + return 3; + } + + @Test + public void invokeMethodInt() throws Exception { + final ValueFunction f = Functions.invokeMethod(registry.getMethod(getClass(), "intMethod")); + Assert.assertEquals(f.apply(this), 3.0, 1e-12); + } + + private long longMethod() { + return 4L; + } + + @Test + public void invokeMethodLong() throws Exception { + final ValueFunction f = Functions.invokeMethod(registry.getMethod(getClass(), "longMethod")); + Assert.assertEquals(f.apply(this), 4.0, 1e-12); + } + + private Long wrapperLongMethod() { + return 5L; + } + + @Test + public void invokeMethodWrapperLong() throws Exception { + final ValueFunction f = Functions.invokeMethod( + registry.getMethod(getClass(), "wrapperLongMethod")); + Assert.assertEquals(f.apply(this), 5.0, 1e-12); + } + + private Long throwsMethod() { + throw new IllegalStateException("fubar"); + } + + @Test + public void invokeBadMethod() throws Exception { + final ValueFunction f = Functions.invokeMethod(registry.getMethod(getClass(), "throwsMethod")); + Assert.assertEquals(f.apply(this), Double.NaN, 1e-12); + } + + @Test(expected = NoSuchMethodException.class) + public void invokeNoSuchMethod() throws Exception { + Functions.invokeMethod(registry.getMethod(getClass(), "unknownMethod")); + } + + @Test + public void invokeOnSubclass() throws Exception { + final ValueFunction f = Functions.invokeMethod(registry.getMethod(B.class, "two")); + Assert.assertEquals(f.apply(new B()), 2.0, 1e-12); + } + + @Test + public void invokeOneA() throws Exception { + final ValueFunction f = Functions.invokeMethod(registry.getMethod(A.class, "one")); + Assert.assertEquals(f.apply(new A()), 1.0, 1e-12); + } + + @Test + public void invokeOneB() throws Exception { + final ValueFunction f = Functions.invokeMethod(registry.getMethod(B.class, "one")); + Assert.assertEquals(f.apply(new B()), -1.0, 1e-12); + } + + private static class A { + public int one() { + return 1; + } + } + + private static class B extends A { + public int one() { + return -1; + } + + public int two() { + return 2; + } + } +} diff --git a/spectator-api/src/test/java/com/netflix/spectator/api/MeasurementTest.java b/spectator-api/src/test/java/com/netflix/spectator/api/MeasurementTest.java new file mode 100644 index 000000000..927142d03 --- /dev/null +++ b/spectator-api/src/test/java/com/netflix/spectator/api/MeasurementTest.java @@ -0,0 +1,42 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.netflix.spectator.api; + +import nl.jqno.equalsverifier.EqualsVerifier; +import nl.jqno.equalsverifier.Warning; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class MeasurementTest { + + @Test + public void testEqualsContract() { + EqualsVerifier + .forClass(Measurement.class) + .suppress(Warning.NULL_FIELDS) + .verify(); + } + + @Test + public void testToString() { + Id id = new DefaultId("foo"); + Measurement m = new Measurement(id, 42L, 42.0); + Assert.assertEquals(m.toString(), "Measurement(foo,42,42.0)"); + } +} diff --git a/spectator-api/src/test/java/com/netflix/spectator/api/NoopCounterTest.java b/spectator-api/src/test/java/com/netflix/spectator/api/NoopCounterTest.java new file mode 100644 index 000000000..26f754803 --- /dev/null +++ b/spectator-api/src/test/java/com/netflix/spectator/api/NoopCounterTest.java @@ -0,0 +1,52 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.netflix.spectator.api; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class NoopCounterTest { + @Test + public void testId() { + Assert.assertEquals(NoopCounter.INSTANCE.id(), NoopId.INSTANCE); + Assert.assertTrue(!NoopCounter.INSTANCE.hasExpired()); + } + + @Test + public void testIncrement() { + NoopCounter c = NoopCounter.INSTANCE; + c.increment(); + Assert.assertEquals(c.count(), 0L); + } + + @Test + public void testIncrementAmount() { + NoopCounter c = NoopCounter.INSTANCE; + c.increment(42); + Assert.assertEquals(c.count(), 0L); + } + + @Test + public void testMeasure() { + NoopCounter c = NoopCounter.INSTANCE; + c.increment(42); + Assert.assertTrue(!c.measure().iterator().hasNext()); + } + +} diff --git a/spectator-api/src/test/java/com/netflix/spectator/api/NoopDistributionSummaryTest.java b/spectator-api/src/test/java/com/netflix/spectator/api/NoopDistributionSummaryTest.java new file mode 100644 index 000000000..ff0c35bcb --- /dev/null +++ b/spectator-api/src/test/java/com/netflix/spectator/api/NoopDistributionSummaryTest.java @@ -0,0 +1,46 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.netflix.spectator.api; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class NoopDistributionSummaryTest { + @Test + public void testId() { + Assert.assertEquals(NoopDistributionSummary.INSTANCE.id(), NoopId.INSTANCE); + Assert.assertTrue(!NoopDistributionSummary.INSTANCE.hasExpired()); + } + + @Test + public void testIncrement() { + NoopDistributionSummary t = NoopDistributionSummary.INSTANCE; + t.record(42); + Assert.assertEquals(t.count(), 0L); + Assert.assertEquals(t.totalAmount(), 0L); + } + + @Test + public void testMeasure() { + NoopDistributionSummary t = NoopDistributionSummary.INSTANCE; + t.record(42); + Assert.assertTrue(!t.measure().iterator().hasNext()); + } + +} diff --git a/spectator-api/src/test/java/com/netflix/spectator/api/NoopIdTest.java b/spectator-api/src/test/java/com/netflix/spectator/api/NoopIdTest.java new file mode 100644 index 000000000..cb4d09566 --- /dev/null +++ b/spectator-api/src/test/java/com/netflix/spectator/api/NoopIdTest.java @@ -0,0 +1,39 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.netflix.spectator.api; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class NoopIdTest { + @Test + public void testTags() { + Assert.assertTrue(!NoopId.INSTANCE.tags().iterator().hasNext()); + } + + @Test + public void testWithTag() { + Assert.assertEquals(NoopId.INSTANCE.withTag(new TagList("k", "v")), NoopId.INSTANCE); + } + + @Test + public void testToString() { + Assert.assertEquals(NoopId.INSTANCE.toString(), "noop"); + } +} diff --git a/spectator-api/src/test/java/com/netflix/spectator/api/NoopRegistryTest.java b/spectator-api/src/test/java/com/netflix/spectator/api/NoopRegistryTest.java new file mode 100644 index 000000000..d87c7fb30 --- /dev/null +++ b/spectator-api/src/test/java/com/netflix/spectator/api/NoopRegistryTest.java @@ -0,0 +1,126 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.netflix.spectator.api; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.util.concurrent.TimeUnit; + +@RunWith(JUnit4.class) +public class NoopRegistryTest { + + private final ManualClock clock = new ManualClock(); + + @Test + public void testCreateId() { + Registry r = new NoopRegistry(); + Assert.assertEquals(r.createId("foo"), NoopId.INSTANCE); + } + + @Test + public void testCreateIdWithTags() { + Registry r = new NoopRegistry(); + TagList ts = new TagList("k", "v"); + Assert.assertEquals(r.createId("foo", ts), NoopId.INSTANCE); + } + + @Test + public void testRegister() { + Registry r = new NoopRegistry(); + Counter c = new DefaultCounter(clock, r.createId("foo")); + r.register(c); + Assert.assertNull(r.get(c.id())); + } + + @Test + public void testCounter() { + Registry r = new NoopRegistry(); + Counter c = r.counter(r.createId("foo")); + c.increment(); + Assert.assertEquals(c.count(), 0L); + + Counter c2 = r.counter(r.createId("foo")); + Assert.assertSame(c, c2); + } + + @Test + public void testTimer() { + Registry r = new NoopRegistry(); + Timer t = r.timer(r.createId("foo")); + t.record(42L, TimeUnit.MILLISECONDS); + Assert.assertEquals(t.count(), 0L); + + Timer t2 = r.timer(r.createId("foo")); + Assert.assertSame(t, t2); + } + + @Test + public void testDistributionSummary() { + Registry r = new NoopRegistry(); + DistributionSummary t = r.distributionSummary(r.createId("foo")); + t.record(42L); + Assert.assertEquals(t.count(), 0L); + + DistributionSummary t2 = r.distributionSummary(r.createId("foo")); + Assert.assertSame(t, t2); + } + + @Test + public void testGet() { + Registry r = new NoopRegistry(); + Counter c = r.counter(r.createId("foo")); + Assert.assertNull(r.get(c.id())); + } + + @Test + public void testIteratorEmpty() { + Registry r = new NoopRegistry(); + for (Meter m : r) { + Assert.fail("should be empty, but found " + m.id()); + } + } + + @Test + public void testIterator() { + Registry r = new NoopRegistry(); + r.counter(r.createId("foo")); + r.counter(r.createId("bar")); + for (Meter m : r) { + Assert.fail("should be empty, but found " + m.id()); + } + } + + @Test + public void testListener() { + RegistryListener listener = new RegistryListener() { + public void onAdd(Meter m) { + Assert.fail("shouldn't notify listeners"); + } + }; + + Registry r = new NoopRegistry(); + r.counter(r.createId("pre")); + r.addListener(listener); + r.counter(r.createId("foo")); + r.timer(r.createId("bar")); + r.distributionSummary(r.createId("baz")); + r.removeListener(listener); + r.counter(r.createId("post")); + } +} diff --git a/spectator-api/src/test/java/com/netflix/spectator/api/NoopTimerTest.java b/spectator-api/src/test/java/com/netflix/spectator/api/NoopTimerTest.java new file mode 100644 index 000000000..bf6961a54 --- /dev/null +++ b/spectator-api/src/test/java/com/netflix/spectator/api/NoopTimerTest.java @@ -0,0 +1,48 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.netflix.spectator.api; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.util.concurrent.TimeUnit; + +@RunWith(JUnit4.class) +public class NoopTimerTest { + @Test + public void testId() { + Assert.assertEquals(NoopTimer.INSTANCE.id(), NoopId.INSTANCE); + Assert.assertTrue(!NoopTimer.INSTANCE.hasExpired()); + } + + @Test + public void testIncrement() { + NoopTimer t = NoopTimer.INSTANCE; + t.record(42, TimeUnit.MILLISECONDS); + Assert.assertEquals(t.count(), 0L); + Assert.assertEquals(t.totalTime(), 0L); + } + + @Test + public void testMeasure() { + NoopTimer t = NoopTimer.INSTANCE; + t.record(42, TimeUnit.MILLISECONDS); + Assert.assertTrue(!t.measure().iterator().hasNext()); + } + +} diff --git a/spectator-api/src/test/java/com/netflix/spectator/api/ObjectGaugeTest.java b/spectator-api/src/test/java/com/netflix/spectator/api/ObjectGaugeTest.java new file mode 100644 index 000000000..ce8ce7d80 --- /dev/null +++ b/spectator-api/src/test/java/com/netflix/spectator/api/ObjectGaugeTest.java @@ -0,0 +1,46 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.netflix.spectator.api; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.util.concurrent.atomic.AtomicLong; + +@RunWith(JUnit4.class) +public class ObjectGaugeTest { + + private final ManualClock clock = new ManualClock(); + + @Test + public void testGC() { + ObjectGauge g = new ObjectGauge( + clock, NoopId.INSTANCE, new AtomicLong(42L), Functions.IDENTITY); + for (Measurement m : g.measure()) { + Assert.assertEquals(m.value(), 42.0, 1e-12); + } + + // Verify we get NaN after gc, this is quite possibly flakey and can be commented out + // if needed + System.gc(); + Assert.assertTrue(g.hasExpired()); + for (Measurement m : g.measure()) { + Assert.assertEquals(m.value(), Double.NaN, 1e-12); + } + } +} diff --git a/spectator-api/src/test/java/com/netflix/spectator/api/SpectatorTest.java b/spectator-api/src/test/java/com/netflix/spectator/api/SpectatorTest.java new file mode 100644 index 000000000..15f5f1aff --- /dev/null +++ b/spectator-api/src/test/java/com/netflix/spectator/api/SpectatorTest.java @@ -0,0 +1,41 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.netflix.spectator.api; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class SpectatorTest { + @Test + public void testRegistry() { + Assert.assertNotNull(Spectator.registry()); + } + + @Test + public void testNewInstanceBadClass() { + System.setProperty("spectator.api.propagateWarnings", "false"); + Assert.assertTrue(Spectator.newInstance("fubar") instanceof DefaultRegistry); + } + + @Test(expected = RuntimeException.class) + public void testNewInstanceBadClassPropagate() { + System.setProperty("spectator.api.propagateWarnings", "true"); + Spectator.newInstance("fubar"); + } +} diff --git a/spectator-api/src/test/java/com/netflix/spectator/api/TagListTest.java b/spectator-api/src/test/java/com/netflix/spectator/api/TagListTest.java new file mode 100644 index 000000000..87a7cafb4 --- /dev/null +++ b/spectator-api/src/test/java/com/netflix/spectator/api/TagListTest.java @@ -0,0 +1,91 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.netflix.spectator.api; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +@RunWith(JUnit4.class) +public class TagListTest { + + @Test + public void equalsContractTest() { + // NOTE: EqualsVerifier doesn't work with cached hash code + TagList ts1 = new TagList("k1", "v1"); + TagList ts2 = new TagList("k2", "v2", ts1); + Assert.assertTrue(ts1.equals(ts1)); + Assert.assertTrue(ts2.equals(ts2)); + Assert.assertTrue(!ts1.equals(null)); + Assert.assertTrue(!ts1.equals(new Object())); + Assert.assertTrue(!ts1.equals(new TagList("k1", "v2"))); + Assert.assertTrue(!ts1.equals(new TagList("k2", "v1"))); + Assert.assertTrue(!ts1.equals(new TagList("k1", "v1", ts2))); + Assert.assertTrue(ts2.equals(new TagList("k2", "v2", ts1))); + Assert.assertTrue(ts2.equals(new TagList("k2", "v2", new TagList("k1", "v1")))); + } + + @Test + public void testSingle() { + TagList ts = new TagList("k", "v"); + for (Tag t : ts) { + Assert.assertEquals(t, ts); + Assert.assertEquals(t.key(), "k"); + Assert.assertEquals(t.value(), "v"); + } + } + + @Test(expected = NullPointerException.class) + public void testNullKey() { + new TagList(null, "v"); + } + + @Test(expected = NullPointerException.class) + public void testNullValue() { + new TagList("k", null); + } + + @Test + public void testCreateFromMap() { + Map m = new HashMap<>(); + m.put("k", "v"); + TagList ts1 = TagList.create(m); + TagList ts2 = new TagList("k", "v"); + Assert.assertEquals(ts1, ts2); + } + + @Test + public void testCreateFromTagList() { + TagList ts = new TagList("k", "v"); + TagList ts1 = TagList.create(ts); + TagList ts2 = new TagList("k", "v"); + Assert.assertEquals(ts1, ts2); + } + + @Test + public void testCreateFromIterable() { + Collection coll = Collections.singleton(new TagList("k", "v")); + TagList ts1 = TagList.create(coll); + TagList ts2 = new TagList("k", "v"); + Assert.assertEquals(ts1, ts2); + } +} diff --git a/spectator-ext-gc/src/main/java/com/netflix/spectator/gc/CircularBuffer.java b/spectator-ext-gc/src/main/java/com/netflix/spectator/gc/CircularBuffer.java new file mode 100644 index 000000000..fdf7750d6 --- /dev/null +++ b/spectator-ext-gc/src/main/java/com/netflix/spectator/gc/CircularBuffer.java @@ -0,0 +1,64 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.netflix.spectator.gc; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReferenceArray; + +/** + * Fixed size buffer that overwrites previous entries after filling up all slots. + */ +class CircularBuffer { + + private final AtomicInteger nextIndex; + private final AtomicReferenceArray data; + + /** Create a new instance. */ + CircularBuffer(int length) { + nextIndex = new AtomicInteger(0); + data = new AtomicReferenceArray<>(length); + } + + /** Add a new item to the buffer. If the buffer is full a previous entry will get overwritten. */ + void add(T item) { + int i = nextIndex.getAndIncrement() % data.length(); + data.set(i, item); + } + + /** Get the item in the buffer at position {@code i} or return null if it isn't set. */ + T get(int i) { + return data.get(i); + } + + /** Return the capacity of the buffer. */ + int size() { + return data.length(); + } + + /** Return a list with a copy of the data in the buffer. */ + List toList() { + List items = new ArrayList<>(data.length()); + for (int i = 0; i < data.length(); ++i) { + T item = data.get(i); + if (item != null) { + items.add(item); + } + } + return items; + } +} diff --git a/spectator-ext-gc/src/main/java/com/netflix/spectator/gc/GcEvent.java b/spectator-ext-gc/src/main/java/com/netflix/spectator/gc/GcEvent.java new file mode 100644 index 000000000..9111c6802 --- /dev/null +++ b/spectator-ext-gc/src/main/java/com/netflix/spectator/gc/GcEvent.java @@ -0,0 +1,119 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.netflix.spectator.gc; + +import com.sun.management.GarbageCollectionNotificationInfo; +import com.sun.management.GcInfo; + +import java.util.Comparator; +import java.util.Date; + +/** + * Metadata about a garbage collection event. + */ +public class GcEvent { + + private final String name; + private final GarbageCollectionNotificationInfo info; + private final GcType type; + private final long startTime; + + /** + * Create a new instance. + * + * @param info + * The info object from the notification emitter on the + * {@link java.lang.management.GarbageCollectorMXBean}. + * @param startTime + * Start time in milliseconds since the epoch. Note the info object has a start time relative + * to the time the jvm process was started. + */ + public GcEvent(GarbageCollectionNotificationInfo info, long startTime) { + this.name = info.getGcName(); + this.info = info; + this.type = HelperFunctions.getGcType(name); + this.startTime = startTime; + } + + /** Type of GC event that occurred. */ + public GcType getType() { + return type; + } + + /** Name of the collector for the event. */ + public String getName() { + return name; + } + + /** Start time in milliseconds since the epoch. */ + public long getStartTime() { + return startTime; + } + + /** + * Info object from the {@link java.lang.management.GarbageCollectorMXBean} notification + * emitter. + */ + public GarbageCollectionNotificationInfo getInfo() { + return info; + } + + @Override + public String toString() { + GcInfo gcInfo = info.getGcInfo(); + long totalBefore = HelperFunctions.getTotalUsage(gcInfo.getMemoryUsageBeforeGc()); + long totalAfter = HelperFunctions.getTotalUsage(gcInfo.getMemoryUsageAfterGc()); + long max = HelperFunctions.getTotalMaxUsage(gcInfo.getMemoryUsageAfterGc()); + + String unit = "K"; + double cnv = 1000.0; + if (max > 1000000000L) { + unit = "G"; + cnv = 1e9; + } else if (max > 1000000L) { + unit = "M"; + cnv = 1e6; + } + + String change = String.format( + "%.1f%s => %.1f%s / %.1f%s", + totalBefore / cnv, unit, + totalAfter / cnv, unit, + max / cnv, unit); + String percentChange = String.format( + "%.1f%% => %.1f%%", 100.0 * totalBefore / max, 100.0 * totalAfter / max); + + final Date d = new Date(startTime); + return type.toString() + ": " + + name + ", id=" + gcInfo.getId() + ", at=" + d.toString() + + ", duration=" + gcInfo.getDuration() + "ms" + ", cause=[" + info.getGcCause() + "]" + + ", " + change + " (" + percentChange + ")"; + } + + /** Order events from oldest to newest. */ + public static final Comparator TIME_ORDER = new Comparator() { + public int compare(GcEvent e1, GcEvent e2) { + return (int) (e1.getStartTime() - e2.getStartTime()); + } + }; + + /** Order events from newest to oldest. */ + public static final Comparator REVERSE_TIME_ORDER = new Comparator() { + public int compare(GcEvent e1, GcEvent e2) { + return (int) (e2.getStartTime() - e1.getStartTime()); + } + }; +} diff --git a/spectator-ext-gc/src/main/java/com/netflix/spectator/gc/GcEventListener.java b/spectator-ext-gc/src/main/java/com/netflix/spectator/gc/GcEventListener.java new file mode 100644 index 000000000..17fc545c9 --- /dev/null +++ b/spectator-ext-gc/src/main/java/com/netflix/spectator/gc/GcEventListener.java @@ -0,0 +1,22 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.netflix.spectator.gc; + +/** Listener for GC events. */ +public interface GcEventListener { + /** Invoked after a GC event occurs. */ + void onComplete(GcEvent event); +} diff --git a/spectator-ext-gc/src/main/java/com/netflix/spectator/gc/GcLogger.java b/spectator-ext-gc/src/main/java/com/netflix/spectator/gc/GcLogger.java new file mode 100644 index 000000000..ee211a998 --- /dev/null +++ b/spectator-ext-gc/src/main/java/com/netflix/spectator/gc/GcLogger.java @@ -0,0 +1,220 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.netflix.spectator.gc; + +import com.netflix.spectator.api.Counter; +import com.netflix.spectator.api.Id; +import com.netflix.spectator.api.Spectator; +import com.netflix.spectator.api.Timer; +import com.netflix.spectator.impl.Preconditions; +import com.sun.management.GarbageCollectionNotificationInfo; +import com.sun.management.GcInfo; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.management.ListenerNotFoundException; +import javax.management.Notification; +import javax.management.NotificationEmitter; +import javax.management.NotificationListener; +import javax.management.openmbean.CompositeData; +import java.lang.management.GarbageCollectorMXBean; +import java.lang.management.ManagementFactory; +import java.lang.management.MemoryPoolMXBean; +import java.lang.management.MemoryUsage; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; + +/** + * Logger to collect GC notifcation events. + */ +public final class GcLogger { + + private static final Logger LOGGER = LoggerFactory.getLogger(GcLogger.class); + + // One major GC per hour would require 168 for a week + // One minor GC per minute would require 180 for three hours + private static final int BUFFER_SIZE = 256; + + // Max size of old generation memory pool + private static final AtomicLong MAX_DATA_SIZE = + Spectator.registry().gauge("jvm.gc.maxDataSize", new AtomicLong(0L)); + + // Size of old generation memory pool after a full GC + private static final AtomicLong LIVE_DATA_SIZE = + Spectator.registry().gauge("jvm.gc.liveDataSize", new AtomicLong(0L)); + + // Incremented for any positive increases in the size of the old generation memory pool + // before GC to after GC + private static final Counter PROMOTION_RATE = + Spectator.registry().counter("jvm.gc.promotionRate"); + + // Incremented for the increase in the size of the young generation memory pool after one GC + // to before the next + private static final Counter ALLOCATION_RATE = + Spectator.registry().counter("jvm.gc.allocationRate"); + + // Pause time due to GC event + private static final Id PAUSE_TIME = Spectator.registry().createId("jvm.gc.pause"); + + private final long jvmStartTime; + + private final ConcurrentHashMap> gcLogs = new ConcurrentHashMap<>(); + + private long youngGenSizeAfter = 0L; + + private String youngGenPoolName = null; + private String oldGenPoolName = null; + + private GcNotificationListener notifListener = null; + + private GcEventListener eventListener = null; + + /** Create a new instance. */ + public GcLogger() { + jvmStartTime = ManagementFactory.getRuntimeMXBean().getStartTime(); + for (GarbageCollectorMXBean mbean : ManagementFactory.getGarbageCollectorMXBeans()) { + CircularBuffer buffer = new CircularBuffer<>(BUFFER_SIZE); + gcLogs.put(mbean.getName(), buffer); + } + + for (MemoryPoolMXBean mbean : ManagementFactory.getMemoryPoolMXBeans()) { + if (HelperFunctions.isYoungGenPool(mbean.getName())) { + youngGenPoolName = mbean.getName(); + } + if (HelperFunctions.isOldGenPool(mbean.getName())) { + oldGenPoolName = mbean.getName(); + } + } + } + + /** + * Start collecting data about GC events. + * + * @param listener + * If not null, the listener will be called with the event objects after metrics and the + * log buffer is updated. + */ + public synchronized void start(GcEventListener listener) { + Preconditions.checkState(notifListener == null, "logger already started"); + eventListener = listener; + notifListener = new GcNotificationListener(); + for (GarbageCollectorMXBean mbean : ManagementFactory.getGarbageCollectorMXBeans()) { + if (mbean instanceof NotificationEmitter) { + final NotificationEmitter emitter = (NotificationEmitter) mbean; + emitter.addNotificationListener(notifListener, null, null); + } + } + } + + /** Stop collecting GC events. */ + public synchronized void stop() { + Preconditions.checkState(notifListener != null, "logger has not been started"); + for (GarbageCollectorMXBean mbean : ManagementFactory.getGarbageCollectorMXBeans()) { + if (mbean instanceof NotificationEmitter) { + final NotificationEmitter emitter = (NotificationEmitter) mbean; + try { + emitter.removeNotificationListener(notifListener); + } catch (ListenerNotFoundException e) { + LOGGER.warn("could not remove gc listener", e); + } + } + } + notifListener = null; + } + + /** Return the current set of GC events in the in-memory log. */ + public List getLogs() { + final List logs = new ArrayList<>(); + for (CircularBuffer buffer : gcLogs.values()) { + logs.addAll(buffer.toList()); + } + Collections.sort(logs, GcEvent.REVERSE_TIME_ORDER); + return logs; + } + + private void updateMetrics(String name, GcInfo info) { + final Map before = info.getMemoryUsageBeforeGc(); + final Map after = info.getMemoryUsageAfterGc(); + + if (oldGenPoolName != null) { + final long oldBefore = before.get(oldGenPoolName).getUsed(); + final long oldAfter = after.get(oldGenPoolName).getUsed(); + final long delta = oldAfter - oldBefore; + if (delta > 0L) { + PROMOTION_RATE.increment(delta); + } + + if (HelperFunctions.getGcType(name) == GcType.OLD) { + LIVE_DATA_SIZE.set(oldAfter); + final long oldMaxAfter = after.get(oldGenPoolName).getMax(); + MAX_DATA_SIZE.set(oldMaxAfter); + } + } + + if (youngGenPoolName != null) { + final long youngBefore = before.get(youngGenPoolName).getUsed(); + final long youngAfter = after.get(youngGenPoolName).getUsed(); + final long delta = youngBefore - youngGenSizeAfter; + youngGenSizeAfter = youngAfter; + if (delta > 0L) { + ALLOCATION_RATE.increment(delta); + } + } + } + + private void processGcEvent(GarbageCollectionNotificationInfo info) { + GcEvent event = new GcEvent(info, jvmStartTime + info.getGcInfo().getStartTime()); + gcLogs.get(info.getGcName()).add(event); + if (LOGGER.isInfoEnabled()) { + LOGGER.info(event.toString()); + } + + // Update pause timer for the action and cause... + Id eventId = PAUSE_TIME + .withTag("action", info.getGcAction()) + .withTag("cause", info.getGcCause()); + Timer timer = Spectator.registry().timer(eventId); + timer.record(info.getGcInfo().getDuration(), TimeUnit.MILLISECONDS); + + // Update promotion and allocation counters + updateMetrics(info.getGcName(), info.getGcInfo()); + + // Notify an event listener if registered + if (eventListener != null) { + try { + eventListener.onComplete(event); + } catch (Exception e) { + LOGGER.warn("exception thrown by event listener", e); + } + } + } + + private class GcNotificationListener implements NotificationListener { + public void handleNotification(Notification notification, Object ref) { + final String type = notification.getType(); + if (type.equals(GarbageCollectionNotificationInfo.GARBAGE_COLLECTION_NOTIFICATION)) { + CompositeData cd = (CompositeData) notification.getUserData(); + GarbageCollectionNotificationInfo info = GarbageCollectionNotificationInfo.from(cd); + processGcEvent(info); + } + } + } +} diff --git a/spectator-ext-gc/src/main/java/com/netflix/spectator/gc/GcType.java b/spectator-ext-gc/src/main/java/com/netflix/spectator/gc/GcType.java new file mode 100644 index 000000000..4b7dce360 --- /dev/null +++ b/spectator-ext-gc/src/main/java/com/netflix/spectator/gc/GcType.java @@ -0,0 +1,30 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.netflix.spectator.gc; + +/** + * Simple classification of gc type to avoid reliance on names than can vary. + */ +public enum GcType { + /** Major collection. */ + OLD, + + /** Minor collection. */ + YOUNG, + + /** Could not determine the collection type. */ + UNKNOWN +} diff --git a/spectator-ext-gc/src/main/java/com/netflix/spectator/gc/HelperFunctions.java b/spectator-ext-gc/src/main/java/com/netflix/spectator/gc/HelperFunctions.java new file mode 100644 index 000000000..136d20d11 --- /dev/null +++ b/spectator-ext-gc/src/main/java/com/netflix/spectator/gc/HelperFunctions.java @@ -0,0 +1,89 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.netflix.spectator.gc; + +import com.sun.management.GcInfo; + +import java.lang.management.MemoryUsage; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** Utility functions for GC. */ +final class HelperFunctions { + + private static final Map KNOWN_COLLECTOR_NAMES = knownCollectors(); + + private HelperFunctions() { + } + + private static Map knownCollectors() { + Map m = new HashMap<>(); + m.put("ConcurrentMarkSweep", GcType.OLD); + m.put("Copy", GcType.YOUNG); + m.put("G1 Old Generation", GcType.OLD); + m.put("G1 Young Generation", GcType.YOUNG); + m.put("MarkSweepCompact", GcType.OLD); + m.put("PS MarkSweep", GcType.OLD); + m.put("PS Scavenge", GcType.YOUNG); + m.put("ParNew", GcType.YOUNG); + return Collections.unmodifiableMap(m); + } + + /** Determine the type, old or young, based on the name of the collector. */ + static GcType getGcType(String name) { + GcType t = KNOWN_COLLECTOR_NAMES.get(name); + return (t == null) ? GcType.UNKNOWN : t; + } + + /** Returns true if memory pool name matches an old generation pool. */ + static boolean isOldGenPool(String name) { + return name.endsWith("Old Gen") || name.endsWith("Tenured Gen"); + } + + /** Returns true if memory pool name matches an young generation pool. */ + static boolean isYoungGenPool(String name) { + return name.endsWith("Eden Space"); + } + + /** Compute the total usage across all pools. */ + static long getTotalUsage(Map usages) { + long sum = 0L; + for (Map.Entry e : usages.entrySet()) { + sum += e.getValue().getUsed(); + } + return sum; + } + + /** Compute the max usage across all pools. */ + static long getTotalMaxUsage(Map usages) { + long sum = 0L; + for (Map.Entry e : usages.entrySet()) { + long max = e.getValue().getMax(); + if (max > 0) { + sum += e.getValue().getMax(); + } + } + return sum; + } + + /** Compute the amount of data promoted during a GC event. */ + static long getPromotionSize(GcInfo info) { + long totalBefore = getTotalUsage(info.getMemoryUsageBeforeGc()); + long totalAfter = getTotalUsage(info.getMemoryUsageAfterGc()); + return totalAfter - totalBefore; + } +} diff --git a/spectator-nflx/src/main/java/com/netflix/spectator/nflx/ChronosGcEventListener.java b/spectator-nflx/src/main/java/com/netflix/spectator/nflx/ChronosGcEventListener.java new file mode 100644 index 000000000..ab0ae9aa6 --- /dev/null +++ b/spectator-nflx/src/main/java/com/netflix/spectator/nflx/ChronosGcEventListener.java @@ -0,0 +1,131 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.netflix.spectator.nflx; + +import com.netflix.client.http.HttpRequest; +import com.netflix.client.http.HttpResponse; +import com.netflix.config.DynamicBooleanProperty; +import com.netflix.config.DynamicPropertyFactory; +import com.netflix.niws.client.http.RestClient; +import com.netflix.spectator.gc.GcEvent; +import com.netflix.spectator.gc.GcEventListener; +import com.netflix.spectator.ribbon.RestClientFactory; +import com.sun.management.GcInfo; +import org.codehaus.jackson.map.ObjectMapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.net.URI; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; + +/** + * Listener that sends GC events to a chronos backend. + */ +public class ChronosGcEventListener implements GcEventListener { + + private static final DynamicBooleanProperty ENABLED = + DynamicPropertyFactory.getInstance().getBooleanProperty("spectator.gc.chronosEnabled", true); + + private final Logger logger = LoggerFactory.getLogger(getClass()); + + private final ObjectMapper mapper = new ObjectMapper(); + + private final ExecutorService executor = Executors.newSingleThreadExecutor( + new ThreadFactory() { + public Thread newThread(Runnable r) { + Thread t = new Thread(r, "ChronosGcEventListener"); + t.setDaemon(true); + return t; + } + }); + + private final RestClient client = RestClientFactory.getClient("chronos_gc"); + + private String getenv(String k) { + String v = System.getenv(k); + return (v == null || v.length() == 0) ? "unknown" : v; + } + + /** Convert a GC event into a map. */ + Map toGcInfoMap(GcEvent event) { + final GcInfo info = event.getInfo().getGcInfo(); + Map map = new HashMap<>(); + map.put("id", info.getId()); + map.put("startTime", event.getStartTime()); + map.put("endTime", event.getStartTime() + info.getEndTime()); + map.put("duration", info.getDuration()); + map.put("memoryBeforeGc", info.getMemoryUsageBeforeGc()); + map.put("memoryAfterGc", info.getMemoryUsageAfterGc()); + return map; + } + + /** Convert a GC event into a map. */ + Map toEventMap(GcEvent event) { + Map map = new HashMap<>(); + map.put("action", event.getInfo().getGcAction()); + map.put("cause", event.getInfo().getGcCause()); + map.put("name", event.getName()); + map.put("gcInfo", toGcInfoMap(event)); + map.put("app", getenv("NETFLIX_APP")); + map.put("cluster", getenv("NETFLIX_CLUSTER")); + map.put("asg", getenv("NETFLIX_AUTO_SCALE_GROUP")); + map.put("region", getenv("EC2_REGION")); + map.put("zone", getenv("EC2_AVAILABILITY_ZONE")); + map.put("ami", getenv("EC2_AMI_ID")); + map.put("node", getenv("EC2_INSTANCE_ID")); + return map; + } + + @Override + public void onComplete(final GcEvent event) { + if (!ENABLED.get()) { + return; + } + + try { + final byte[] json = mapper.writeValueAsBytes(toEventMap(event)); + executor.submit(new Runnable() { + public void run() { + HttpRequest request = new HttpRequest.Builder() + .verb(HttpRequest.Verb.POST) + .uri(URI.create("/api/v2/event")) + .header("Content-Type", "application/json") + .entity(json) + .build(); + try (HttpResponse response = client.executeWithLoadBalancer(request)) { + if (response.getStatus() != 200) { + logger.warn("failed to send GC event to chronos (status={})", response.getStatus()); + } + } catch (Exception e) { + logger.warn("failed to send GC event to chronos", e); + } + } + }); + } catch (IOException e) { + logger.warn("failed to send GC event to chronos", e); + } + } + + /** Shutdown the executor used to send data to chronos. */ + public void shutdown() { + executor.shutdown(); + } +} diff --git a/spectator-nflx/src/main/java/com/netflix/spectator/nflx/Plugin.java b/spectator-nflx/src/main/java/com/netflix/spectator/nflx/Plugin.java new file mode 100644 index 000000000..b3ef4e90f --- /dev/null +++ b/spectator-nflx/src/main/java/com/netflix/spectator/nflx/Plugin.java @@ -0,0 +1,51 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.netflix.spectator.nflx; + +import com.netflix.config.ConfigurationManager; +import com.netflix.governator.annotations.AutoBindSingleton; +import com.netflix.spectator.gc.GcLogger; +import org.apache.commons.configuration.AbstractConfiguration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.PostConstruct; +import java.io.IOException; + +/** + * Plugin for setting up spectator to report correctly into the standard Netflix stack. + */ +@AutoBindSingleton +public final class Plugin { + + private static final String CONFIG_FILE = "spectator.properties"; + + private static final GcLogger GC_LOGGER = new GcLogger(); + + private static final Logger LOGGER = LoggerFactory.getLogger(Plugin.class); + + @PostConstruct + private void init() throws IOException { + ConfigurationManager.loadPropertiesFromResources(CONFIG_FILE); + AbstractConfiguration config = ConfigurationManager.getConfigInstance(); + if (config.getBoolean("spectator.gc.loggingEnabled")) { + GC_LOGGER.start(new ChronosGcEventListener()); + LOGGER.info("gc logging started"); + } else { + LOGGER.info("gc logging is not enabled"); + } + } +} diff --git a/spectator-nflx/src/main/java/com/netflix/spectator/ribbon/MeteredRestClient.java b/spectator-nflx/src/main/java/com/netflix/spectator/ribbon/MeteredRestClient.java new file mode 100644 index 000000000..f12f2d2f2 --- /dev/null +++ b/spectator-nflx/src/main/java/com/netflix/spectator/ribbon/MeteredRestClient.java @@ -0,0 +1,98 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.netflix.spectator.ribbon; + +import com.netflix.client.ClientException; +import com.netflix.client.config.IClientConfig; +import com.netflix.client.http.HttpRequest; +import com.netflix.client.http.HttpResponse; +import com.netflix.niws.client.http.RestClient; +import com.netflix.spectator.api.Id; +import com.netflix.spectator.api.Spectator; +import com.netflix.spectator.api.Timer; + +import java.util.concurrent.TimeUnit; + +/** + * Subclass that provides instrumentation of requests, latency, and failures. + */ +public class MeteredRestClient extends RestClient { + + private Timer latency; + + private Id requests; + private Id exceptions; + + private Id niwsRequests; + private Id niwsExceptions; + + @Override + public void initWithNiwsConfig(IClientConfig config) { + super.initWithNiwsConfig(config); + + final String client = "client"; + final String cname = getClientName(); + latency = Spectator.registry().timer("ribbon.http.latency", client, cname); + + requests = Spectator.registry().createId("ribbon.http.requests", client, cname); + exceptions = Spectator.registry().createId("ribbon.http.exceptions", client, cname); + + niwsRequests = Spectator.registry().createId("ribbon.http.niwsRequests", client, cname); + niwsExceptions = Spectator.registry().createId("ribbon.http.niwsExceptions", client, cname); + } + + @Override + public HttpResponse execute(HttpRequest req) throws Exception { + final long start = System.nanoTime(); + try { + final HttpResponse res = super.execute(req); + final String status = String.format("%d", res.getStatus()); + Spectator.registry().counter(requests.withTag("status", status)).increment(); + return res; + } catch (ClientException e) { + final String m = e.getErrorType().name(); + Spectator.registry().counter(exceptions.withTag("error", m)).increment(); + throw e; + } catch (Exception e) { + final String c = e.getClass().getSimpleName(); + Spectator.registry().counter(exceptions.withTag("error", c)).increment(); + throw e; + } finally { + latency.record(System.nanoTime() - start, TimeUnit.NANOSECONDS); + } + } + + @Override + public HttpResponse executeWithLoadBalancer(HttpRequest req) throws ClientException { + final long start = System.nanoTime(); + try { + final HttpResponse res = super.executeWithLoadBalancer(req); + final String status = String.format("%d", res.getStatus()); + Spectator.registry().counter(niwsRequests.withTag("status", status)).increment(); + return res; + } catch (ClientException e) { + final String m = e.getErrorType().name(); + Spectator.registry().counter(niwsExceptions.withTag("error", m)).increment(); + throw e; + } catch (Exception e) { + final String c = e.getClass().getSimpleName(); + Spectator.registry().counter(niwsExceptions.withTag("error", c)).increment(); + throw e; + } finally { + latency.record(System.nanoTime() - start, TimeUnit.NANOSECONDS); + } + } +} diff --git a/spectator-nflx/src/main/java/com/netflix/spectator/ribbon/RestClientFactory.java b/spectator-nflx/src/main/java/com/netflix/spectator/ribbon/RestClientFactory.java new file mode 100644 index 000000000..fcb61d0d1 --- /dev/null +++ b/spectator-nflx/src/main/java/com/netflix/spectator/ribbon/RestClientFactory.java @@ -0,0 +1,49 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.netflix.spectator.ribbon; + +import com.netflix.client.ClientFactory; +import com.netflix.niws.client.http.RestClient; + +/** + * Helper for creating a {@link com.netflix.niws.client.http.RestClient} using the spectator + * client config implementation. + */ +public final class RestClientFactory { + private RestClientFactory() { + } + + /** + * Get or create a {@link com.netflix.niws.client.http.RestClient} with the specified name. The + * client will use the {@link com.netflix.spectator.ribbon.RibbonClientConfigImpl} that changes + * some of the defaults to make the common cases work easier: + * + *
    + *
  • Namespace for the clients defaults to {@code niws.client} to avoid property name changes + * if switching between internal {@code platform-ipc} and {@code ribbon}.
  • + *
  • The default server list class is set to {@code DiscoveryEnabledNIWSServerList}.
  • + *
  • An instrumented RestClient class is returned.
  • + *
+ * + * @param name + * Name of the client to retrieve. + * @return + * Rest client for the specified name. + */ + public static RestClient getClient(String name) { + return (RestClient) ClientFactory.getNamedClient(name, RibbonClientConfigImpl.class); + } +} diff --git a/spectator-nflx/src/main/java/com/netflix/spectator/ribbon/RibbonClientConfigImpl.java b/spectator-nflx/src/main/java/com/netflix/spectator/ribbon/RibbonClientConfigImpl.java new file mode 100644 index 000000000..19f086f56 --- /dev/null +++ b/spectator-nflx/src/main/java/com/netflix/spectator/ribbon/RibbonClientConfigImpl.java @@ -0,0 +1,39 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.netflix.spectator.ribbon; + +import com.netflix.client.config.DefaultClientConfigImpl; + +/** + * Customize some of the default settings used for rest clients. + */ +public class RibbonClientConfigImpl extends DefaultClientConfigImpl { + + @Override + public String getNameSpace() { + return "niws.client"; + } + + @Override + public String getDefaultClientClassname() { + return "com.netflix.spectator.ribbon.MeteredRestClient"; + } + + @Override + public String getDefaultSeverListClass() { + return "com.netflix.niws.loadbalancer.DiscoveryEnabledNIWSServerList"; + } +} diff --git a/spectator-nflx/src/main/resources/spectator.properties b/spectator-nflx/src/main/resources/spectator.properties new file mode 100644 index 000000000..8c2f3cf42 --- /dev/null +++ b/spectator-nflx/src/main/resources/spectator.properties @@ -0,0 +1,32 @@ +# +# Copyright 2014 Netflix, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + + +# Should we enable gc logging? Only checked at startup. +spectator.gc.loggingEnabled=true + +# Should we send gc events to chronos backend? Logging must be enabled. This property is only +# checked at startup. +spectator.gc.chronosEnabled=true + +# Rest client for chronos gc backend +chronos_gc.niws.client.AppName=CHRONOS_BACKEND +chronos_gc.niws.client.ReadTimeout=15000 +chronos_gc.niws.client.ConnectTimeout=5000 +chronos_gc.niws.client.MaxAutoRetries=0 +chronos_gc.niws.client.MaxAutoRetriesNextServer=2 +chronos_gc.niws.client.OkToRetryOnAllOperations=true +chronos_gc.niws.client.DeploymentContextBasedVipAddresses=chronos_backend-gc:7001 diff --git a/spectator-nflx/src/test/java/com/netflix/spectator/ribbon/MeteredRestClientTest.java b/spectator-nflx/src/test/java/com/netflix/spectator/ribbon/MeteredRestClientTest.java new file mode 100644 index 000000000..307325941 --- /dev/null +++ b/spectator-nflx/src/test/java/com/netflix/spectator/ribbon/MeteredRestClientTest.java @@ -0,0 +1,133 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.netflix.spectator.ribbon; + +import com.netflix.client.http.HttpRequest; +import com.netflix.client.http.HttpResponse; +import com.netflix.niws.client.http.RestClient; +import com.netflix.spectator.api.ExtendedRegistry; +import com.netflix.spectator.api.Id; +import com.netflix.spectator.api.Spectator; +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import com.sun.net.httpserver.HttpServer; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.URI; + +@RunWith(JUnit4.class) +public class MeteredRestClientTest { + + private static final String client = "MeteredRestClientTest"; + + private static HttpServer server; + private static int port; + + @BeforeClass + public static void startServer() throws Exception { + server = HttpServer.create(new InetSocketAddress(0), 0); + port = server.getAddress().getPort(); + + server.createContext("/ok", new HttpHandler() { + @Override + public void handle(HttpExchange exchange) throws IOException { + exchange.sendResponseHeaders(200, 0L); + exchange.close(); + } + }); + + server.start(); + + System.setProperty(client + ".niws.client.NIWSServerListClassName", + "com.netflix.loadbalancer.ConfigurationBasedServerList"); + System.setProperty(client + ".niws.client.listOfServers", + "localhost:" + port); + } + + @AfterClass + public static void stopServer() { + server.stop(0); + } + + private int get(String loc) { + URI uri = URI.create(loc); + HttpRequest req = new HttpRequest.Builder() + .verb(HttpRequest.Verb.GET) + .uri(uri) + .build(); + RestClient c = RestClientFactory.getClient(client); + try (HttpResponse res = uri.isAbsolute() ? c.execute(req) : c.executeWithLoadBalancer(req)) { + return res.getStatus(); + } catch (Exception e) { + e.printStackTrace(); + return -1; + } + } + + private long reqCount(int status) { + ExtendedRegistry r = Spectator.registry(); + Id requests = r.createId("ribbon.http.requests", "client", client, "status", "" + status); + return r.counter(requests).count(); + } + + private long niwsReqCount(int status) { + ExtendedRegistry r = Spectator.registry(); + Id requests = r.createId("ribbon.http.niwsRequests", "client", client, "status", "" + status); + return r.counter(requests).count(); + } + + @Test + public void executeOk() { + long before = reqCount(200); + Assert.assertEquals(get("http://localhost:" + port + "/ok"), 200); + Assert.assertEquals(reqCount(200), before + 1); + } + + @Test + public void executeNotFound() { + long before200 = reqCount(200); + long before404 = reqCount(404); + Assert.assertEquals(get("http://localhost:" + port + "/not-found"), 404); + Assert.assertEquals(reqCount(200), before200); + Assert.assertEquals(reqCount(404), before404 + 1); + } + + @Test + public void executeWithLbOk() { + long before = reqCount(200); + long nbefore = niwsReqCount(200); + Assert.assertEquals(get("/ok"), 200); + Assert.assertEquals(reqCount(200), before + 1); + Assert.assertEquals(niwsReqCount(200), nbefore + 1); + } + + @Test + public void executeWithLbNotFound() { + long before200 = niwsReqCount(200); + long before404 = niwsReqCount(404); + Assert.assertEquals(get("/not-found"), 404); + Assert.assertEquals(niwsReqCount(200), before200); + Assert.assertEquals(niwsReqCount(404), before404 + 1); + } +} + diff --git a/spectator-reg-metrics2/src/main/java/com/netflix/spectator/metrics2/MetricsCounter.java b/spectator-reg-metrics2/src/main/java/com/netflix/spectator/metrics2/MetricsCounter.java new file mode 100644 index 000000000..de72bf4fa --- /dev/null +++ b/spectator-reg-metrics2/src/main/java/com/netflix/spectator/metrics2/MetricsCounter.java @@ -0,0 +1,76 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.netflix.spectator.metrics2; + +import com.netflix.spectator.api.Clock; +import com.netflix.spectator.api.Counter; +import com.netflix.spectator.api.Id; +import com.netflix.spectator.api.Measurement; + +import java.util.Collections; + +/** Counter implementation for the metrics2 registry. */ +class MetricsCounter implements Counter { + + private final Clock clock; + private final Id id; + private final com.yammer.metrics.core.Meter impl; + + /** Create a new instance. */ + MetricsCounter(Clock clock, Id id, com.yammer.metrics.core.Meter impl) { + this.clock = clock; + this.id = id; + this.impl = impl; + } + + /** {@inheritDoc} */ + @Override + public Id id() { + return id; + } + + /** {@inheritDoc} */ + @Override + public boolean hasExpired() { + return false; + } + + /** {@inheritDoc} */ + @Override + public Iterable measure() { + long now = clock.wallTime(); + long v = impl.count(); + return Collections.singleton(new Measurement(id, now, v)); + } + + /** {@inheritDoc} */ + @Override + public void increment() { + impl.mark(); + } + + /** {@inheritDoc} */ + @Override + public void increment(long amount) { + impl.mark(amount); + } + + /** {@inheritDoc} */ + @Override + public long count() { + return impl.count(); + } +} diff --git a/spectator-reg-metrics2/src/main/java/com/netflix/spectator/metrics2/MetricsDistributionSummary.java b/spectator-reg-metrics2/src/main/java/com/netflix/spectator/metrics2/MetricsDistributionSummary.java new file mode 100644 index 000000000..78e5baf72 --- /dev/null +++ b/spectator-reg-metrics2/src/main/java/com/netflix/spectator/metrics2/MetricsDistributionSummary.java @@ -0,0 +1,75 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.netflix.spectator.metrics2; + +import com.netflix.spectator.api.Clock; +import com.netflix.spectator.api.DistributionSummary; +import com.netflix.spectator.api.Id; +import com.netflix.spectator.api.Measurement; + +import java.util.Collections; + +/** Distribution summary implementation for the metric2 registry. */ +class MetricsDistributionSummary implements DistributionSummary { + + private final Clock clock; + private final Id id; + private final com.yammer.metrics.core.Histogram impl; + + /** Create a new instance. */ + MetricsDistributionSummary(Clock clock, Id id, com.yammer.metrics.core.Histogram impl) { + this.clock = clock; + this.id = id; + this.impl = impl; + } + + /** {@inheritDoc} */ + @Override + public Id id() { + return id; + } + + /** {@inheritDoc} */ + @Override + public boolean hasExpired() { + return false; + } + + /** {@inheritDoc} */ + @Override + public void record(long amount) { + impl.update(amount); + } + + /** {@inheritDoc} */ + @Override + public Iterable measure() { + final long now = clock.wallTime(); + return Collections.singleton(new Measurement(id, now, impl.mean())); + } + + /** {@inheritDoc} */ + @Override + public long count() { + return impl.count(); + } + + /** {@inheritDoc} */ + @Override + public long totalAmount() { + return (long) impl.sum(); + } +} diff --git a/spectator-reg-metrics2/src/main/java/com/netflix/spectator/metrics2/MetricsRegistry.java b/spectator-reg-metrics2/src/main/java/com/netflix/spectator/metrics2/MetricsRegistry.java new file mode 100644 index 000000000..06f1d6b5f --- /dev/null +++ b/spectator-reg-metrics2/src/main/java/com/netflix/spectator/metrics2/MetricsRegistry.java @@ -0,0 +1,72 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.netflix.spectator.metrics2; + +import com.netflix.spectator.api.*; +import com.yammer.metrics.Metrics; +import com.yammer.metrics.core.MetricName; + +import java.util.concurrent.TimeUnit; + +/** Registry implementation that maps spectator types to the metrics2 library. */ +public class MetricsRegistry extends AbstractRegistry { + + private final com.yammer.metrics.core.MetricsRegistry impl; + + /** Create a new instance. */ + public MetricsRegistry() { + this(Clock.SYSTEM, Metrics.defaultRegistry()); + } + + /** Create a new instance. */ + public MetricsRegistry(Clock clock, com.yammer.metrics.core.MetricsRegistry impl) { + super(clock); + this.impl = impl; + } + + private MetricName toMetricName(Id id) { + final String name = id.name(); + final int pos = name.lastIndexOf("."); + if (pos != -1) { + final String prefix = name.substring(0, pos); + final String suffix = name.substring(pos + 1); + return new MetricName("spectator", prefix, suffix); + } else { + return new MetricName("spectator", "default", id.name()); + } + } + + /** {@inheritDoc} */ + @Override + protected Counter newCounter(Id id) { + final MetricName name = toMetricName(id); + return new MetricsCounter(clock(), id, impl.newMeter(name, "calls", TimeUnit.SECONDS)); + } + + /** {@inheritDoc} */ + @Override + protected DistributionSummary newDistributionSummary(Id id) { + final MetricName name = toMetricName(id); + return new MetricsDistributionSummary(clock(), id, impl.newHistogram(name, false)); + } + + /** {@inheritDoc} */ + @Override + protected Timer newTimer(Id id) { + final MetricName name = toMetricName(id); + return new MetricsTimer(clock(), id, impl.newTimer(name, TimeUnit.SECONDS, TimeUnit.SECONDS)); + } +} diff --git a/spectator-reg-metrics2/src/main/java/com/netflix/spectator/metrics2/MetricsTimer.java b/spectator-reg-metrics2/src/main/java/com/netflix/spectator/metrics2/MetricsTimer.java new file mode 100644 index 000000000..dc2aecd21 --- /dev/null +++ b/spectator-reg-metrics2/src/main/java/com/netflix/spectator/metrics2/MetricsTimer.java @@ -0,0 +1,101 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.netflix.spectator.metrics2; + +import com.netflix.spectator.api.Clock; +import com.netflix.spectator.api.Id; +import com.netflix.spectator.api.Measurement; +import com.netflix.spectator.api.Timer; + +import java.util.Collections; +import java.util.concurrent.Callable; +import java.util.concurrent.TimeUnit; + +/** Timer implementation for the metrics2 registry. */ +class MetricsTimer implements Timer { + + private final Clock clock; + private final Id id; + private final com.yammer.metrics.core.Timer impl; + + /** Create a new instance. */ + MetricsTimer(Clock clock, Id id, com.yammer.metrics.core.Timer impl) { + this.clock = clock; + this.id = id; + this.impl = impl; + } + + /** {@inheritDoc} */ + @Override + public Id id() { + return id; + } + + /** {@inheritDoc} */ + @Override + public boolean hasExpired() { + return false; + } + + /** {@inheritDoc} */ + @Override + public void record(long amount, TimeUnit unit) { + impl.update(amount, unit); + } + + /** {@inheritDoc} */ + @Override + public Iterable measure() { + final long now = clock.wallTime(); + return Collections.singleton(new Measurement(id, now, impl.meanRate())); + } + + /** {@inheritDoc} */ + @Override + public T record(Callable f) throws Exception { + final long s = clock.monotonicTime(); + try { + return f.call(); + } finally { + final long e = clock.monotonicTime(); + record(e - s, TimeUnit.NANOSECONDS); + } + } + + /** {@inheritDoc} */ + @Override + public void record(Runnable f) { + final long s = clock.monotonicTime(); + try { + f.run(); + } finally { + final long e = clock.monotonicTime(); + record(e - s, TimeUnit.NANOSECONDS); + } + } + + /** {@inheritDoc} */ + @Override + public long count() { + return impl.count(); + } + + /** {@inheritDoc} */ + @Override + public long totalTime() { + return (long) impl.sum(); + } +} diff --git a/spectator-reg-metrics2/src/main/resources/META-INF/services/com.netflix.spectator.api.Registry b/spectator-reg-metrics2/src/main/resources/META-INF/services/com.netflix.spectator.api.Registry new file mode 100644 index 000000000..dc8ac67dc --- /dev/null +++ b/spectator-reg-metrics2/src/main/resources/META-INF/services/com.netflix.spectator.api.Registry @@ -0,0 +1 @@ +com.netflix.spectator.metrics2.MetricsRegistry diff --git a/spectator-reg-metrics3/src/main/java/com/netflix/spectator/metrics3/MetricsCounter.java b/spectator-reg-metrics3/src/main/java/com/netflix/spectator/metrics3/MetricsCounter.java new file mode 100644 index 000000000..08e8d33bb --- /dev/null +++ b/spectator-reg-metrics3/src/main/java/com/netflix/spectator/metrics3/MetricsCounter.java @@ -0,0 +1,76 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.netflix.spectator.metrics3; + +import com.netflix.spectator.api.Clock; +import com.netflix.spectator.api.Counter; +import com.netflix.spectator.api.Id; +import com.netflix.spectator.api.Measurement; + +import java.util.Collections; + +/** Counter implementation for the metric3 registry. */ +class MetricsCounter implements Counter { + + private final Clock clock; + private final Id id; + private final com.codahale.metrics.Meter impl; + + /** Create a new instance. */ + MetricsCounter(Clock clock, Id id, com.codahale.metrics.Meter impl) { + this.clock = clock; + this.id = id; + this.impl = impl; + } + + /** {@inheritDoc} */ + @Override + public Id id() { + return id; + } + + /** {@inheritDoc} */ + @Override + public boolean hasExpired() { + return false; + } + + /** {@inheritDoc} */ + @Override + public Iterable measure() { + long now = clock.wallTime(); + long v = impl.getCount(); + return Collections.singleton(new Measurement(id, now, v)); + } + + /** {@inheritDoc} */ + @Override + public void increment() { + impl.mark(); + } + + /** {@inheritDoc} */ + @Override + public void increment(long amount) { + impl.mark(amount); + } + + /** {@inheritDoc} */ + @Override + public long count() { + return impl.getCount(); + } +} diff --git a/spectator-reg-metrics3/src/main/java/com/netflix/spectator/metrics3/MetricsDistributionSummary.java b/spectator-reg-metrics3/src/main/java/com/netflix/spectator/metrics3/MetricsDistributionSummary.java new file mode 100644 index 000000000..7d103ed32 --- /dev/null +++ b/spectator-reg-metrics3/src/main/java/com/netflix/spectator/metrics3/MetricsDistributionSummary.java @@ -0,0 +1,80 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.netflix.spectator.metrics3; + +import com.codahale.metrics.Snapshot; +import com.netflix.spectator.api.Clock; +import com.netflix.spectator.api.DistributionSummary; +import com.netflix.spectator.api.Id; +import com.netflix.spectator.api.Measurement; + +import java.util.Collections; +import java.util.concurrent.atomic.AtomicLong; + +/** Distribution summary implementation for the metric3 registry. */ +class MetricsDistributionSummary implements DistributionSummary { + + private final Clock clock; + private final Id id; + private final com.codahale.metrics.Histogram impl; + private final AtomicLong totalAmount; + + /** Create a new instance. */ + MetricsDistributionSummary(Clock clock, Id id, com.codahale.metrics.Histogram impl) { + this.clock = clock; + this.id = id; + this.impl = impl; + this.totalAmount = new AtomicLong(0L); + } + + /** {@inheritDoc} */ + @Override + public Id id() { + return id; + } + + /** {@inheritDoc} */ + @Override + public boolean hasExpired() { + return false; + } + + /** {@inheritDoc} */ + @Override + public void record(long amount) { + impl.update(amount); + } + + /** {@inheritDoc} */ + @Override + public Iterable measure() { + final long now = clock.wallTime(); + final Snapshot snapshot = impl.getSnapshot(); + return Collections.singleton(new Measurement(id, now, snapshot.getMean())); + } + + /** {@inheritDoc} */ + @Override + public long count() { + return impl.getCount(); + } + + /** {@inheritDoc} */ + @Override + public long totalAmount() { + return totalAmount.get(); + } +} diff --git a/spectator-reg-metrics3/src/main/java/com/netflix/spectator/metrics3/MetricsRegistry.java b/spectator-reg-metrics3/src/main/java/com/netflix/spectator/metrics3/MetricsRegistry.java new file mode 100644 index 000000000..3666693e2 --- /dev/null +++ b/spectator-reg-metrics3/src/main/java/com/netflix/spectator/metrics3/MetricsRegistry.java @@ -0,0 +1,65 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.netflix.spectator.metrics3; + +import com.netflix.spectator.api.*; + +/** Registry implementation that maps spectator types to the metrics3 library. */ +public class MetricsRegistry extends AbstractRegistry { + + private final com.codahale.metrics.MetricRegistry impl; + + /** Create a new instance. */ + public MetricsRegistry() { + this(Clock.SYSTEM, new com.codahale.metrics.MetricRegistry()); + } + + /** Create a new instance. */ + public MetricsRegistry(Clock clock, com.codahale.metrics.MetricRegistry impl) { + super(clock); + this.impl = impl; + } + + private String toMetricName(Id id) { + StringBuilder buf = new StringBuilder(); + buf.append(id.name()); + for (Tag t : id.tags()) { + buf.append(t.key()).append("-").append(t.value()); + } + return buf.toString(); + } + + /** {@inheritDoc} */ + @Override + protected Counter newCounter(Id id) { + final String name = toMetricName(id); + return new MetricsCounter(clock(), id, impl.meter(name)); + } + + /** {@inheritDoc} */ + @Override + protected DistributionSummary newDistributionSummary(Id id) { + final String name = toMetricName(id); + return new MetricsDistributionSummary(clock(), id, impl.histogram(name)); + } + + /** {@inheritDoc} */ + @Override + protected Timer newTimer(Id id) { + final String name = toMetricName(id); + return new MetricsTimer(clock(), id, impl.timer(name)); + } +} diff --git a/spectator-reg-metrics3/src/main/java/com/netflix/spectator/metrics3/MetricsTimer.java b/spectator-reg-metrics3/src/main/java/com/netflix/spectator/metrics3/MetricsTimer.java new file mode 100644 index 000000000..67779a36e --- /dev/null +++ b/spectator-reg-metrics3/src/main/java/com/netflix/spectator/metrics3/MetricsTimer.java @@ -0,0 +1,104 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.netflix.spectator.metrics3; + +import com.netflix.spectator.api.Clock; +import com.netflix.spectator.api.Id; +import com.netflix.spectator.api.Measurement; +import com.netflix.spectator.api.Timer; + +import java.util.Collections; +import java.util.concurrent.Callable; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; + +/** Timer implementation for the metrics3 registry. */ +class MetricsTimer implements Timer { + + private final Clock clock; + private final Id id; + private final com.codahale.metrics.Timer impl; + private final AtomicLong totalTime; + + /** Create a new instance. */ + MetricsTimer(Clock clock, Id id, com.codahale.metrics.Timer impl) { + this.clock = clock; + this.id = id; + this.impl = impl; + this.totalTime = new AtomicLong(0L); + } + + /** {@inheritDoc} */ + @Override + public Id id() { + return id; + } + + /** {@inheritDoc} */ + @Override + public boolean hasExpired() { + return false; + } + + /** {@inheritDoc} */ + @Override + public void record(long amount, TimeUnit unit) { + impl.update(amount, unit); + } + + /** {@inheritDoc} */ + @Override + public Iterable measure() { + final long now = clock.wallTime(); + return Collections.singleton(new Measurement(id, now, impl.getMeanRate())); + } + + /** {@inheritDoc} */ + @Override + public T record(Callable f) throws Exception { + final long s = clock.monotonicTime(); + try { + return f.call(); + } finally { + final long e = clock.monotonicTime(); + record(e - s, TimeUnit.NANOSECONDS); + } + } + + /** {@inheritDoc} */ + @Override + public void record(Runnable f) { + final long s = clock.monotonicTime(); + try { + f.run(); + } finally { + final long e = clock.monotonicTime(); + record(e - s, TimeUnit.NANOSECONDS); + } + } + + /** {@inheritDoc} */ + @Override + public long count() { + return impl.getCount(); + } + + /** {@inheritDoc} */ + @Override + public long totalTime() { + return totalTime.get(); + } +} diff --git a/spectator-reg-metrics3/src/main/resources/META-INF/services/com.netflix.spectator.api.Registry b/spectator-reg-metrics3/src/main/resources/META-INF/services/com.netflix.spectator.api.Registry new file mode 100644 index 000000000..de9ad58ee --- /dev/null +++ b/spectator-reg-metrics3/src/main/resources/META-INF/services/com.netflix.spectator.api.Registry @@ -0,0 +1 @@ +com.netflix.spectator.metrics3.MetricsRegistry diff --git a/spectator-reg-servo/src/main/java/com/netflix/spectator/servo/ServoCounter.java b/spectator-reg-servo/src/main/java/com/netflix/spectator/servo/ServoCounter.java new file mode 100644 index 000000000..c27ec79c7 --- /dev/null +++ b/spectator-reg-servo/src/main/java/com/netflix/spectator/servo/ServoCounter.java @@ -0,0 +1,90 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.netflix.spectator.servo; + +import com.netflix.servo.monitor.Monitor; +import com.netflix.spectator.api.Clock; +import com.netflix.spectator.api.Counter; +import com.netflix.spectator.api.Id; +import com.netflix.spectator.api.Measurement; + +import java.util.Collections; +import java.util.concurrent.atomic.AtomicLong; + +/** Counter implementation for the servo registry. */ +class ServoCounter implements Counter, ServoMeter { + + private final Clock clock; + private final ServoId id; + private final com.netflix.servo.monitor.Counter impl; + + // Local count so that we have more flexibility on servo counter impl without changing the + // value returned by the {@link #count()} method. + private final AtomicLong count; + + /** Create a new instance. */ + ServoCounter(Clock clock, ServoId id, com.netflix.servo.monitor.Counter impl) { + this.clock = clock; + this.id = id; + this.impl = impl; + this.count = new AtomicLong(0L); + } + + @Override + public Monitor monitor() { + return impl; + } + + /** {@inheritDoc} */ + @Override + public Id id() { + return id; + } + + /** {@inheritDoc} */ + @Override + public boolean hasExpired() { + return false; + } + + /** {@inheritDoc} */ + @Override + public Iterable measure() { + long now = clock.wallTime(); + long v = count.get(); + return Collections.singleton(new Measurement(id, now, v)); + } + + /** {@inheritDoc} */ + @Override + public void increment() { + impl.increment(); + count.incrementAndGet(); + } + + /** {@inheritDoc} */ + @Override + public void increment(long amount) { + impl.increment(amount); + count.addAndGet(amount); + } + + /** {@inheritDoc} */ + @Override + public long count() { + return count.get(); + } +} diff --git a/spectator-reg-servo/src/main/java/com/netflix/spectator/servo/ServoDistributionSummary.java b/spectator-reg-servo/src/main/java/com/netflix/spectator/servo/ServoDistributionSummary.java new file mode 100644 index 000000000..d74c68c69 --- /dev/null +++ b/spectator-reg-servo/src/main/java/com/netflix/spectator/servo/ServoDistributionSummary.java @@ -0,0 +1,102 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.netflix.spectator.servo; + +import com.netflix.servo.monitor.BasicDistributionSummary; +import com.netflix.servo.monitor.Monitor; +import com.netflix.spectator.api.Clock; +import com.netflix.spectator.api.DistributionSummary; +import com.netflix.spectator.api.Id; +import com.netflix.spectator.api.Measurement; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicLong; + +/** Distribution summary implementation for the servo registry. */ +class ServoDistributionSummary implements DistributionSummary, ServoMeter { + + private final Clock clock; + private final ServoId id; + private final BasicDistributionSummary impl; + + // Local count so that we have more flexibility on servo counter impl without changing the + // value returned by the {@link #count()} method. + private final AtomicLong count; + private final AtomicLong totalAmount; + + private final Id countId; + private final Id totalAmountId; + + /** Create a new instance. */ + ServoDistributionSummary(Clock clock, ServoId id, BasicDistributionSummary impl) { + this.clock = clock; + this.id = id; + this.impl = impl; + this.count = new AtomicLong(0L); + this.totalAmount = new AtomicLong(0L); + countId = id.withTag("statistic", "count"); + totalAmountId = id.withTag("statistic", "totalAmount"); + } + + /** {@inheritDoc} */ + @Override + public Monitor monitor() { + return impl; + } + + /** {@inheritDoc} */ + @Override + public Id id() { + return id; + } + + /** {@inheritDoc} */ + @Override + public boolean hasExpired() { + return false; + } + + /** {@inheritDoc} */ + @Override + public void record(long amount) { + impl.record(amount); + totalAmount.addAndGet(amount); + count.incrementAndGet(); + } + + /** {@inheritDoc} */ + @Override + public Iterable measure() { + final long now = clock.wallTime(); + final List ms = new ArrayList<>(2); + ms.add(new Measurement(countId, now, count.get())); + ms.add(new Measurement(totalAmountId, now, totalAmount.get())); + return ms; + } + + /** {@inheritDoc} */ + @Override + public long count() { + return count.get(); + } + + /** {@inheritDoc} */ + @Override + public long totalAmount() { + return totalAmount.get(); + } +} diff --git a/spectator-reg-servo/src/main/java/com/netflix/spectator/servo/ServoId.java b/spectator-reg-servo/src/main/java/com/netflix/spectator/servo/ServoId.java new file mode 100644 index 000000000..2d03a021f --- /dev/null +++ b/spectator-reg-servo/src/main/java/com/netflix/spectator/servo/ServoId.java @@ -0,0 +1,80 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.netflix.spectator.servo; + +import com.netflix.servo.monitor.MonitorConfig; +import com.netflix.spectator.api.Id; +import com.netflix.spectator.api.Tag; + +import java.util.Iterator; + +/** Id implementation for the servo registry. */ +class ServoId implements Id { + + private final MonitorConfig config; + + /** Create a new instance. */ + public ServoId(MonitorConfig config) { + this.config = config; + } + + /** Return the monitor config this id is based on. */ + MonitorConfig config() { + return config; + } + + /** {@inheritDoc} */ + @Override + public String name() { + return config.getName(); + } + + /** {@inheritDoc} */ + @Override + public Iterable tags() { + return new Iterable() { + public Iterator iterator() { + return new Iterator() { + private final Iterator iter = config.getTags().iterator(); + + public boolean hasNext() { + return iter.hasNext(); + } + + public Tag next() { + return new ServoTag(iter.next()); + } + + public void remove() { + iter.remove(); + } + }; + } + }; + } + + /** {@inheritDoc} */ + @Override + public Id withTag(String k, String v) { + return new ServoId((new MonitorConfig.Builder(config)).withTag(k, v).build()); + } + + /** {@inheritDoc} */ + @Override + public Id withTag(Tag t) { + return withTag(t.key(), t.value()); + } +} diff --git a/spectator-reg-servo/src/main/java/com/netflix/spectator/servo/ServoMeter.java b/spectator-reg-servo/src/main/java/com/netflix/spectator/servo/ServoMeter.java new file mode 100644 index 000000000..373565b25 --- /dev/null +++ b/spectator-reg-servo/src/main/java/com/netflix/spectator/servo/ServoMeter.java @@ -0,0 +1,24 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.netflix.spectator.servo; + +import com.netflix.servo.monitor.Monitor; + +/** Meter that can return a servo monitor. */ +interface ServoMeter { + /** Returns the monitor corresponding to this meter. */ + Monitor monitor(); +} diff --git a/spectator-reg-servo/src/main/java/com/netflix/spectator/servo/ServoRegistry.java b/spectator-reg-servo/src/main/java/com/netflix/spectator/servo/ServoRegistry.java new file mode 100644 index 000000000..4d7e310e9 --- /dev/null +++ b/spectator-reg-servo/src/main/java/com/netflix/spectator/servo/ServoRegistry.java @@ -0,0 +1,118 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.netflix.spectator.servo; + +import com.netflix.servo.DefaultMonitorRegistry; +import com.netflix.servo.monitor.*; +import com.netflix.spectator.api.*; +import com.netflix.spectator.api.Counter; +import com.netflix.spectator.api.Timer; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** Registry that maps spectator types to servo. */ +public class ServoRegistry extends AbstractRegistry implements CompositeMonitor { + + private static final MonitorConfig DEFAULT_CONFIG = + (new MonitorConfig.Builder("spectator.registry")).build(); + + private final MonitorConfig config; + + /** Create a new instance. */ + public ServoRegistry() { + this(Clock.SYSTEM); + } + + /** Create a new instance. */ + public ServoRegistry(Clock clock) { + this(clock, DEFAULT_CONFIG); + } + + /** Create a new instance. */ + ServoRegistry(Clock clock, MonitorConfig config) { + super(clock); + this.config = config; + DefaultMonitorRegistry.getInstance().register(this); + } + + private MonitorConfig toMonitorConfig(Id id) { + MonitorConfig.Builder builder = new MonitorConfig.Builder(id.name()); + for (Tag t : id.tags()) { + builder.withTag(t.key(), t.value()); + } + return builder.build(); + } + + /** {@inheritDoc} */ + @Override + protected Counter newCounter(Id id) { + MonitorConfig cfg = toMonitorConfig(id); + StepCounter counter = new StepCounter(cfg); + return new ServoCounter(clock(), new ServoId(cfg), counter); + } + + /** {@inheritDoc} */ + @Override + protected DistributionSummary newDistributionSummary(Id id) { + MonitorConfig cfg = toMonitorConfig(id); + BasicDistributionSummary distributionSummary = new BasicDistributionSummary(cfg); + return new ServoDistributionSummary(clock(), new ServoId(cfg), distributionSummary); + } + + /** {@inheritDoc} */ + @Override + protected Timer newTimer(Id id) { + MonitorConfig cfg = toMonitorConfig(id); + BasicTimer timer = new BasicTimer(cfg, TimeUnit.SECONDS); + return new ServoTimer(clock(), new ServoId(cfg), timer); + } + + /** {@inheritDoc} */ + @Override + public Integer getValue() { + return 0; + } + + /** {@inheritDoc} */ + @Override + public Integer getValue(int pollerIndex) { + return 0; + } + + /** {@inheritDoc} */ + @Override + public MonitorConfig getConfig() { + return config; + } + + /** {@inheritDoc} */ + @Override + public List> getMonitors() { + List> monitors = new ArrayList<>(); + for (Meter meter : this) { + if (meter instanceof ServoMeter) { + monitors.add(((ServoMeter) meter).monitor()); + } else { + for (Measurement m : meter.measure()) { + monitors.add(new NumberGauge(toMonitorConfig(m.id()), m.value())); + } + } + } + return monitors; + } +} diff --git a/spectator-reg-servo/src/main/java/com/netflix/spectator/servo/ServoTag.java b/spectator-reg-servo/src/main/java/com/netflix/spectator/servo/ServoTag.java new file mode 100644 index 000000000..43f538081 --- /dev/null +++ b/spectator-reg-servo/src/main/java/com/netflix/spectator/servo/ServoTag.java @@ -0,0 +1,41 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.netflix.spectator.servo; + +import com.netflix.spectator.api.Tag; + +/** Tag implementation for the servo registry. */ +class ServoTag implements Tag { + + private final com.netflix.servo.tag.Tag tag; + + /** Create a new instance. */ + public ServoTag(com.netflix.servo.tag.Tag tag) { + this.tag = tag; + } + + /** {@inheritDoc} */ + @Override + public String key() { + return tag.getKey(); + } + + /** {@inheritDoc} */ + @Override + public String value() { + return tag.getValue(); + } +} diff --git a/spectator-reg-servo/src/main/java/com/netflix/spectator/servo/ServoTimer.java b/spectator-reg-servo/src/main/java/com/netflix/spectator/servo/ServoTimer.java new file mode 100644 index 000000000..e7d42b6de --- /dev/null +++ b/spectator-reg-servo/src/main/java/com/netflix/spectator/servo/ServoTimer.java @@ -0,0 +1,128 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.netflix.spectator.servo; + +import com.netflix.servo.monitor.Monitor; +import com.netflix.spectator.api.Clock; +import com.netflix.spectator.api.Id; +import com.netflix.spectator.api.Measurement; +import com.netflix.spectator.api.Timer; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; + +/** Timer implementation for the servo registry. */ +class ServoTimer implements Timer, ServoMeter { + + private final Clock clock; + private final ServoId id; + private final com.netflix.servo.monitor.Timer impl; + + // Local count so that we have more flexibility on servo counter impl without changing the + // value returned by the {@link #count()} method. + private final AtomicLong count; + private final AtomicLong totalTime; + + private final Id countId; + private final Id totalTimeId; + + /** Create a new instance. */ + ServoTimer(Clock clock, ServoId id, com.netflix.servo.monitor.Timer impl) { + this.clock = clock; + this.id = id; + this.impl = impl; + this.count = new AtomicLong(0L); + this.totalTime = new AtomicLong(0L); + countId = id.withTag("statistic", "count"); + totalTimeId = id.withTag("statistic", "totalTime"); + } + + /** {@inheritDoc} */ + @Override + public Monitor monitor() { + return impl; + } + + /** {@inheritDoc} */ + @Override + public Id id() { + return id; + } + + /** {@inheritDoc} */ + @Override + public boolean hasExpired() { + return false; + } + + /** {@inheritDoc} */ + @Override + public void record(long amount, TimeUnit unit) { + final long nanos = unit.toNanos(amount); + impl.record(amount, unit); + totalTime.addAndGet(nanos); + count.incrementAndGet(); + } + + /** {@inheritDoc} */ + @Override + public Iterable measure() { + final long now = clock.wallTime(); + final List ms = new ArrayList<>(2); + ms.add(new Measurement(countId, now, count.get())); + ms.add(new Measurement(totalTimeId, now, totalTime.get())); + return ms; + } + + /** {@inheritDoc} */ + @Override + public T record(Callable f) throws Exception { + final long s = clock.monotonicTime(); + try { + return f.call(); + } finally { + final long e = clock.monotonicTime(); + record(e - s, TimeUnit.NANOSECONDS); + } + } + + /** {@inheritDoc} */ + @Override + public void record(Runnable f) { + final long s = clock.monotonicTime(); + try { + f.run(); + } finally { + final long e = clock.monotonicTime(); + record(e - s, TimeUnit.NANOSECONDS); + } + } + + /** {@inheritDoc} */ + @Override + public long count() { + return count.get(); + } + + /** {@inheritDoc} */ + @Override + public long totalTime() { + return totalTime.get(); + } +} diff --git a/spectator-reg-servo/src/main/resources/META-INF/services/com.netflix.spectator.api.Registry b/spectator-reg-servo/src/main/resources/META-INF/services/com.netflix.spectator.api.Registry new file mode 100644 index 000000000..a764a73fb --- /dev/null +++ b/spectator-reg-servo/src/main/resources/META-INF/services/com.netflix.spectator.api.Registry @@ -0,0 +1 @@ +com.netflix.spectator.servo.ServoRegistry