├── visualization ├── influxdb-dashboard │ ├── Procfile │ ├── .gitignore │ ├── public │ │ ├── images │ │ │ └── favicon.ico │ │ ├── fonts │ │ │ ├── glyphicons-halflings-regular.eot │ │ │ ├── glyphicons-halflings-regular.ttf │ │ │ ├── glyphicons-halflings-regular.woff │ │ │ └── glyphicons-halflings-regular.woff2 │ │ ├── scripts │ │ │ ├── config.js │ │ │ ├── flamegraph.js │ │ │ ├── jquery.redirect.js │ │ │ ├── view-util.js │ │ │ ├── index-client.js │ │ │ ├── toc.min.js │ │ │ ├── jquery.sticky-kit.min.js │ │ │ ├── influxdb.js │ │ │ └── render-client.js │ │ └── css │ │ │ ├── toc.css │ │ │ ├── style.css │ │ │ ├── style.styl │ │ │ └── bootstrap-theme.min.css │ ├── example-dashboard-config.json │ ├── views │ │ ├── footer.jade │ │ ├── header.jade │ │ ├── layout.jade │ │ ├── header-render.jade │ │ ├── index.jade │ │ ├── layout-render.jade │ │ └── render.jade │ ├── package.json │ ├── app.js │ ├── routes │ │ └── index.js │ └── README.md ├── README.md ├── graphite_dump.py └── influxdb_dump.py ├── .gitignore ├── .travis.yml ├── example ├── README.md ├── statsd-jvm-profiler.properties └── StatsDProfilerFlowListener.scala ├── src ├── test │ └── java │ │ └── com │ │ └── etsy │ │ └── statsd │ │ └── profiler │ │ ├── profilers │ │ ├── MemoryProfilerTest.java │ │ ├── MockReportingProfiler.java │ │ ├── MockProfilerWithArguments.java │ │ ├── MockProfiler1.java │ │ └── MockProfiler2.java │ │ ├── worker │ │ ├── ProfilerThreadFactoryTest.java │ │ ├── ProfilerWorkerThreadTest.java │ │ └── ProfilerShutdownHookWorkerTest.java │ │ ├── util │ │ ├── MapUtilTest.java │ │ ├── TimeUtilTest.java │ │ ├── MockArguments.java │ │ ├── CPUTracesTest.java │ │ ├── StackTraceFormatterTest.java │ │ ├── TagUtilTest.java │ │ └── StackTraceFilterTest.java │ │ ├── reporter │ │ ├── mock │ │ │ ├── ReporterAnswer.java │ │ │ └── BaseReporterTest.java │ │ ├── ReporterTest.java │ │ ├── StatsDReporterTest.java │ │ ├── MockReporter.java │ │ └── InfluxDBReporterTest.java │ │ ├── ProfilerTest.java │ │ ├── server │ │ └── ProfilerServerTest.java │ │ └── ArgumentsTest.java └── main │ └── java │ └── com │ └── etsy │ └── statsd │ └── profiler │ ├── worker │ ├── ProfilerShutdownHookWorker.java │ ├── ProfilerThreadFactory.java │ └── ProfilerWorkerThread.java │ ├── util │ ├── Range.java │ ├── TimeUtil.java │ ├── StackTraceFormatter.java │ ├── MapUtil.java │ ├── CPUTraces.java │ ├── TagUtil.java │ ├── ThreadDumper.java │ └── StackTraceFilter.java │ ├── server │ ├── ProfilerServer.java │ └── RequestHandler.java │ ├── reporter │ ├── StatsDReporter.java │ ├── Reporter.java │ └── InfluxDBReporter.java │ ├── profilers │ ├── CPULoadProfiler.java │ ├── CPUTracingProfiler.java │ └── MemoryProfiler.java │ ├── Profiler.java │ ├── Agent.java │ └── Arguments.java ├── LICENSE ├── CONTRIBUTING.md ├── checkstyle.xml ├── pom.xml └── README.md /visualization/influxdb-dashboard/Procfile: -------------------------------------------------------------------------------- 1 | web: node app.js -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | target 3 | dependency-reduced-pom.xml 4 | *.iml 5 | -------------------------------------------------------------------------------- /visualization/influxdb-dashboard/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dashboard-config.json 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: java 3 | jdk: 4 | - oraclejdk8 5 | - oraclejdk7 6 | -------------------------------------------------------------------------------- /visualization/influxdb-dashboard/public/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DataDog/spark-jvm-profiler/HEAD/visualization/influxdb-dashboard/public/images/favicon.ico -------------------------------------------------------------------------------- /visualization/influxdb-dashboard/public/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DataDog/spark-jvm-profiler/HEAD/visualization/influxdb-dashboard/public/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /visualization/influxdb-dashboard/public/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DataDog/spark-jvm-profiler/HEAD/visualization/influxdb-dashboard/public/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /visualization/influxdb-dashboard/public/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DataDog/spark-jvm-profiler/HEAD/visualization/influxdb-dashboard/public/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /visualization/influxdb-dashboard/public/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DataDog/spark-jvm-profiler/HEAD/visualization/influxdb-dashboard/public/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /visualization/influxdb-dashboard/example-dashboard-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "prefix": "bigdata.profiler", 3 | "influxdb": { 4 | "host": "localhost", 5 | "port": 8086, 6 | "username": "profiler_user", 7 | "password": "profiler_password", 8 | "database": "profiler_database" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /visualization/influxdb-dashboard/public/scripts/config.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | 3 | exports.getConfig = function(callback) { 4 | fs.readFile('dashboard-config.json', 'UTF-8', function(err, data) { 5 | if (err) { 6 | console.log(err); 7 | } 8 | 9 | callback(JSON.parse(data)); 10 | }); 11 | 12 | } 13 | -------------------------------------------------------------------------------- /visualization/influxdb-dashboard/views/footer.jade: -------------------------------------------------------------------------------- 1 | .container 2 | .footer 3 | hr(style='margin: 30px 0 10px 0;') 4 | p ©  5 | a(href='//www.etsy.com') Etsy 6 | | 2015 7 | 8 | link(rel='stylesheet', href='/css/style.css') 9 | script(src='/scripts/bootstrap.min.js') 10 | 11 | block footer 12 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | This directory contains some examples of how to use the profiler. 2 | 3 | ## StatsDProfilerFlowListener 4 | 5 | This is an example Cascading FlowListener for using the profiler with Scalding/Cascading jobs. 6 | 7 | It reads various properties from a configuration file on the classpath, an example of which is provided in `statsd-jvm-profiler.properties`. -------------------------------------------------------------------------------- /visualization/influxdb-dashboard/views/header.jade: -------------------------------------------------------------------------------- 1 | div.navbar.navbar-inverse.navbar-fixed-top(role='navigation') 2 | div.container 3 | div.navbar-header 4 | button.navbar-toggle(type='button', data-toggle='collapse', data-target='.navbar-collapse') 5 | span.icon-bar 6 | span.icon-bar 7 | span.icon-bar 8 | a.navbar-brand(href="#") 9 | | Profiler 10 | div.collapse.navbar-collapse 11 | ul.nav.navbar-nav 12 | -------------------------------------------------------------------------------- /visualization/influxdb-dashboard/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "statsd-jvm-profiler-dash", 3 | "version": "0.0.1", 4 | "dependencies": { 5 | "request-json": "*", 6 | "express": "*", 7 | "jade": "*", 8 | "stylus": "*", 9 | "serve-favicon": "*", 10 | "morgan": "*", 11 | "body-parser": "*", 12 | "method-override": "*", 13 | "influx": "*" 14 | }, 15 | "engines": { 16 | "node": "0.10.30", 17 | "npm": "1.4.x" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/test/java/com/etsy/statsd/profiler/profilers/MemoryProfilerTest.java: -------------------------------------------------------------------------------- 1 | package com.etsy.statsd.profiler.profilers; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | public class MemoryProfilerTest { 8 | @Test 9 | public void getRssMemory() throws Exception { 10 | assertEquals(MemoryProfiler.getRssMemory("VmRSS: 248484 kB"), 248484L * 1024L); 11 | 12 | assertEquals(MemoryProfiler.getRssMemory("VmRSS:\t 2117224 kB"), 2117224L * 1024L); 13 | } 14 | 15 | } -------------------------------------------------------------------------------- /example/statsd-jvm-profiler.properties: -------------------------------------------------------------------------------- 1 | jar.location=/usr/etsy/statsd-jvm-profiler/statsd-jvm-profiler.jar 2 | host=influxdb.host 3 | port=8086 4 | influxdb.user=profiler 5 | influxdb.password=password 6 | influxdb.database=profiler 7 | reporter=InfluxDBReporter 8 | tagMapping=SKIP.SKIP.username.job.flow.stage.phase 9 | package.blacklist=java.util.zip.ZipFile.getEntry:java.lang.ClassLoader.defineClass1 10 | package.whitelist=com.etsy:com.twitter.scalding:cascading 11 | dashboard.url=dashboard.host:3888/render?user=%%{user}&job=%%{job_name}&run=%%{flow_id}&stage=%%{stage}&phase=%s -------------------------------------------------------------------------------- /visualization/influxdb-dashboard/views/layout.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | block head 5 | block title 6 | title Profiler Dashboard 7 | block meta 8 | meta(name='description', content='') 9 | meta(name='keywords', content='') 10 | link(rel='stylesheet', href='/css/bootstrap.min.css') 11 | | 14 | script(src='//ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js') 15 | script(src='//cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js') 16 | script(src='/scripts/view-util.js') 17 | body 18 | include header 19 | 20 | block content 21 | 22 | include footer 23 | -------------------------------------------------------------------------------- /src/test/java/com/etsy/statsd/profiler/worker/ProfilerThreadFactoryTest.java: -------------------------------------------------------------------------------- 1 | package com.etsy.statsd.profiler.worker; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | public class ProfilerThreadFactoryTest { 8 | @Test 9 | public void testThreadName() { 10 | ProfilerThreadFactory factory = new ProfilerThreadFactory(); 11 | Thread t = factory.newThread(new Runnable() { 12 | @Override 13 | public void run() { 14 | System.out.println("dummy"); 15 | } 16 | }); 17 | 18 | assertTrue(t.getName().startsWith(ProfilerThreadFactory.NAME_PREFIX)); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/test/java/com/etsy/statsd/profiler/util/MapUtilTest.java: -------------------------------------------------------------------------------- 1 | package com.etsy.statsd.profiler.util; 2 | 3 | import org.junit.Test; 4 | 5 | import java.util.HashMap; 6 | import java.util.Map; 7 | 8 | import static org.junit.Assert.*; 9 | 10 | public class MapUtilTest { 11 | 12 | @Test 13 | public void testSetOrIncrementMap() { 14 | Map map = new HashMap<>(); 15 | MapUtil.setOrIncrementMap(map, "key", 1); 16 | assertEquals(new Long(1L), map.get("key")); 17 | 18 | MapUtil.setOrIncrementMap(map, "key", 1); 19 | assertEquals(new Long(2L), map.get("key")); 20 | 21 | MapUtil.setOrIncrementMap(map, "key2", 1); 22 | assertEquals(new Long(1L), map.get("key2")); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /visualization/influxdb-dashboard/views/header-render.jade: -------------------------------------------------------------------------------- 1 | div.navbar.navbar-inverse.navbar-fixed-top(role='navigation') 2 | div.container 3 | div.navbar-header 4 | button.navbar-toggle(type='button', data-toggle='collapse', data-target='.navbar-collapse') 5 | span.icon-bar 6 | span.icon-bar 7 | span.icon-bar 8 | a.navbar-brand(href="/") 9 | | Profiler 10 | div.collapse.navbar-collapse 11 | ul.nav.navbar-nav 12 | li.inactive 13 | a(href="#") User: #{user} 14 | li.inactive 15 | a(href="#") Job: #{job} 16 | li.inactive 17 | a(href="#") Flow Id: #{flow} 18 | li.inactive 19 | a(href="#") Stage: #{stage} 20 | li.inactive 21 | a(href="#") Phase: #{phase} 22 | li.inactive 23 | a(href="#") jvmName: #{jvmName} 24 | -------------------------------------------------------------------------------- /src/test/java/com/etsy/statsd/profiler/reporter/mock/ReporterAnswer.java: -------------------------------------------------------------------------------- 1 | package com.etsy.statsd.profiler.reporter.mock; 2 | 3 | import com.google.common.base.Function; 4 | import org.mockito.invocation.InvocationOnMock; 5 | import org.mockito.stubbing.Answer; 6 | 7 | /** 8 | * Mockito Answer for use testing reporters 9 | */ 10 | public class ReporterAnswer implements Answer { 11 | private Function testCase; 12 | 13 | public ReporterAnswer(Function testCase) { 14 | this.testCase = testCase; 15 | } 16 | 17 | @Override 18 | public Object answer(InvocationOnMock invocationOnMock) throws Throwable { 19 | Object[] args = invocationOnMock.getArguments(); 20 | testCase.apply(args); 21 | 22 | return null; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/test/java/com/etsy/statsd/profiler/util/TimeUtilTest.java: -------------------------------------------------------------------------------- 1 | package com.etsy.statsd.profiler.util; 2 | 3 | import org.junit.Test; 4 | 5 | import java.util.concurrent.TimeUnit; 6 | 7 | import static org.junit.Assert.*; 8 | 9 | public class TimeUtilTest { 10 | @Test 11 | public void testConvertReportingPeriod() { 12 | assertEquals(1, TimeUtil.convertReportingPeriod(1, TimeUnit.MILLISECONDS, 100, TimeUnit.MICROSECONDS)); 13 | assertEquals(10000, TimeUtil.convertReportingPeriod(1, TimeUnit.MILLISECONDS, 10, TimeUnit.SECONDS)); 14 | assertEquals(1000, TimeUtil.convertReportingPeriod(10, TimeUnit.MILLISECONDS, 10, TimeUnit.SECONDS)); 15 | assertEquals(100000, TimeUtil.convertReportingPeriod(100, TimeUnit.MICROSECONDS, 10, TimeUnit.SECONDS)); 16 | } 17 | } 18 | 19 | -------------------------------------------------------------------------------- /visualization/influxdb-dashboard/views/index.jade: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block content 4 | div.container 5 | h1 Profiler Dashboard 6 | 7 | div.panel.panel-default 8 | div.panel-heading(style='text-align:center') 9 | h2 Select Job 10 | div.panel-body 11 | form(action='/render', method='GET')#jobselector 12 | label(for=users) User 13 | select#users 14 | label(for=jobs) Job 15 | select#jobs 16 | label(for=flows) Flow 17 | select#flows 18 | label(for=stages) Stage 19 | select#stages 20 | label(for=phases) Phase 21 | select#phases 22 | label(for=jvms) JVM 23 | select#jvms 24 | br 25 | br 26 | button(type=submit)#go Render 27 | 28 | script(src='/scripts/jquery.redirect.js') 29 | script(src='/scripts/index-client.js') 30 | -------------------------------------------------------------------------------- /src/test/java/com/etsy/statsd/profiler/reporter/ReporterTest.java: -------------------------------------------------------------------------------- 1 | package com.etsy.statsd.profiler.reporter; 2 | 3 | import com.etsy.statsd.profiler.profilers.MockReportingProfiler; 4 | import org.junit.Test; 5 | 6 | import java.util.HashMap; 7 | import java.util.Map; 8 | 9 | import static org.junit.Assert.assertEquals; 10 | 11 | public class ReporterTest { 12 | @Test 13 | public void testReporter() { 14 | MockReporter mockReporter = new MockReporter(); 15 | MockReportingProfiler profiler = new MockReportingProfiler(mockReporter); 16 | 17 | profiler.profile(); 18 | profiler.flushData(); 19 | 20 | Map expected = new HashMap<>(); 21 | expected.put("profile", 1L); 22 | expected.put("flushData", 1L); 23 | assertEquals(expected, mockReporter.getOutput()); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /visualization/influxdb-dashboard/public/css/toc.css: -------------------------------------------------------------------------------- 1 | #toc { 2 | top: 49px; 3 | left: 0px; 4 | height: 100%; 5 | position: absolute; 6 | width: 150px; 7 | padding-top: 20px; 8 | margin: 0; 9 | border-style: none; 10 | } 11 | 12 | #toc.affix { 13 | left: 0px; 14 | height: 100%; 15 | position: fixed; 16 | width: 150px; 17 | padding-top: 20px; 18 | margin: 0; 19 | border-style: none; 20 | } 21 | 22 | #toc ul { 23 | margin: 0; 24 | padding: 0; 25 | list-style: none; 26 | } 27 | 28 | #toc li { 29 | padding: 5px 10px; 30 | } 31 | 32 | #toc a { 33 | text-decoration: none; 34 | display: block; 35 | } 36 | 37 | #toc .toc-h2 { 38 | padding-left: 10px; 39 | } 40 | 41 | #toc .toc-h3 { 42 | padding-left: 20px; 43 | } 44 | 45 | #toc .toc-active { 46 | background: #FFFF99; 47 | } 48 | -------------------------------------------------------------------------------- /visualization/influxdb-dashboard/public/scripts/flamegraph.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var spawn = require('child_process').spawnSync 3 | 4 | exports.getFlameGraph = function(metrics, callback) { 5 | if (Object.keys(metrics).length === 0) { 6 | callback('Unable to retrieve CPU metrics'); 7 | return; 8 | } 9 | var collapsedMetrics = metrics.filter(function(metric) { 10 | // This filters out the metrics representing the upper and lower bounds on depth of the metric hierarchy 11 | return metric.metric != ':' + metric.value; 12 | }).map(function(metric) { 13 | return metric.metric + " " + Math.round(metric.value); 14 | }); 15 | var flamegraphScriptPath = path.join(__dirname, 'flamegraph.pl') 16 | var child = spawn(flamegraphScriptPath, ['--title', 'Flame Graph', '--width', '1800'], {'input': collapsedMetrics.join('\n')}); 17 | callback(child.stdout) 18 | } 19 | -------------------------------------------------------------------------------- /visualization/influxdb-dashboard/public/css/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding-top: 70px; 3 | } 4 | @media (max-width: 2000px) { 5 | .navbar-header { 6 | float: none; 7 | } 8 | .navbar-left, 9 | .navbar-right { 10 | float: none !important; 11 | } 12 | .navbar-toggle { 13 | display: block; 14 | } 15 | .navbar-collapse { 16 | border-top: 1px solid transparent; 17 | box-shadow: inset 0 1px 0 rgba(255,255,255,0.1); 18 | } 19 | .navbar-fixed-top { 20 | top: 0; 21 | border-width: 0 0 1px; 22 | } 23 | .navbar-collapse.collapse { 24 | display: none !important; 25 | } 26 | .navbar-nav { 27 | float: none !important; 28 | margin-top: 7.5px; 29 | } 30 | .navbar-nav>li { 31 | float: none; 32 | } 33 | .navbar-nav>li>a { 34 | padding-top: 10px; 35 | padding-bottom: 10px; 36 | } 37 | .collapse.in { 38 | display: block !important; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/etsy/statsd/profiler/worker/ProfilerShutdownHookWorker.java: -------------------------------------------------------------------------------- 1 | package com.etsy.statsd.profiler.worker; 2 | 3 | import com.etsy.statsd.profiler.Profiler; 4 | 5 | import java.util.Collection; 6 | import java.util.concurrent.atomic.AtomicReference; 7 | 8 | /** 9 | * Worker thread for profiler shutdown hook 10 | * 11 | * @author Andrew Johnson 12 | */ 13 | public class ProfilerShutdownHookWorker implements Runnable { 14 | private final Collection profilers; 15 | private final AtomicReference isRunning; 16 | public ProfilerShutdownHookWorker(Collection profilers, AtomicReference isRunning) { 17 | this.profilers = profilers; 18 | this.isRunning = isRunning; 19 | } 20 | 21 | @Override 22 | public void run() { 23 | for (Profiler p : profilers) { 24 | p.flushData(); 25 | } 26 | 27 | isRunning.set(false); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /visualization/influxdb-dashboard/public/css/style.styl: -------------------------------------------------------------------------------- 1 | body { 2 | padding-top:70px; 3 | } 4 | 5 | 6 | @media (max-width: 2000px) { 7 | .navbar-header { 8 | float: none; 9 | } 10 | .navbar-left,.navbar-right { 11 | float: none !important; 12 | } 13 | .navbar-toggle { 14 | display: block; 15 | } 16 | .navbar-collapse { 17 | border-top: 1px solid transparent; 18 | box-shadow: inset 0 1px 0 rgba(255,255,255,0.1); 19 | } 20 | .navbar-fixed-top { 21 | top: 0; 22 | border-width: 0 0 1px; 23 | } 24 | .navbar-collapse.collapse { 25 | display: none!important; 26 | } 27 | .navbar-nav { 28 | float: none!important; 29 | margin-top: 7.5px; 30 | } 31 | .navbar-nav>li { 32 | float: none; 33 | } 34 | .navbar-nav>li>a { 35 | padding-top: 10px; 36 | padding-bottom: 10px; 37 | } 38 | .collapse.in{ 39 | display:block !important; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/test/java/com/etsy/statsd/profiler/worker/ProfilerWorkerThreadTest.java: -------------------------------------------------------------------------------- 1 | package com.etsy.statsd.profiler.worker; 2 | 3 | import com.etsy.statsd.profiler.Profiler; 4 | import com.etsy.statsd.profiler.profilers.MockProfiler1; 5 | import org.junit.Test; 6 | 7 | import java.util.HashSet; 8 | import java.util.LinkedList; 9 | import java.util.Set; 10 | 11 | import static org.junit.Assert.*; 12 | 13 | public class ProfilerWorkerThreadTest { 14 | @Test 15 | public void testRunnable() throws InterruptedException { 16 | Set output = new HashSet<>(); 17 | Profiler mockProfiler1 = new MockProfiler1(output); 18 | 19 | Thread t = new Thread(new ProfilerWorkerThread(mockProfiler1, new LinkedList())); 20 | t.run(); 21 | t.join(); 22 | 23 | Set expectedOutput = new HashSet<>(); 24 | expectedOutput.add(MockProfiler1.class.getSimpleName() + "-profile"); 25 | assertEquals(expectedOutput, output); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/test/java/com/etsy/statsd/profiler/profilers/MockReportingProfiler.java: -------------------------------------------------------------------------------- 1 | package com.etsy.statsd.profiler.profilers; 2 | 3 | import com.etsy.statsd.profiler.Arguments; 4 | import com.etsy.statsd.profiler.Profiler; 5 | import com.etsy.statsd.profiler.reporter.Reporter; 6 | 7 | import java.util.concurrent.TimeUnit; 8 | 9 | /** 10 | * Mock profiler for testing 11 | * 12 | * @author Andrew Johnson 13 | */ 14 | public class MockReportingProfiler extends Profiler { 15 | public MockReportingProfiler(Reporter reporter) { 16 | super(reporter, null); 17 | } 18 | 19 | @Override 20 | public void profile() { 21 | recordGaugeValue("profile", 1); 22 | } 23 | 24 | @Override 25 | public void flushData() { 26 | recordGaugeValue("flushData", 1); 27 | } 28 | 29 | @Override 30 | public long getPeriod() { 31 | return 0; 32 | } 33 | 34 | @Override 35 | public TimeUnit getTimeUnit() { 36 | return null; 37 | } 38 | 39 | @Override 40 | protected void handleArguments(Arguments arguments) { } 41 | } 42 | -------------------------------------------------------------------------------- /src/test/java/com/etsy/statsd/profiler/reporter/mock/BaseReporterTest.java: -------------------------------------------------------------------------------- 1 | package com.etsy.statsd.profiler.reporter.mock; 2 | 3 | import com.etsy.statsd.profiler.reporter.Reporter; 4 | import com.google.common.base.Function; 5 | import org.junit.Before; 6 | import org.mockito.InjectMocks; 7 | import org.mockito.MockitoAnnotations; 8 | import org.mockito.stubbing.Answer; 9 | 10 | /** 11 | * Base class for Reporter tests 12 | */ 13 | public abstract class BaseReporterTest> { 14 | @InjectMocks 15 | protected T reporter = constructReporter(); 16 | 17 | private Function testCaseFunction = new Function() { 18 | @Override 19 | public Void apply(Object[] input) { 20 | testCase(input); 21 | return null; 22 | } 23 | }; 24 | 25 | protected Answer answer = new ReporterAnswer(testCaseFunction); 26 | 27 | @Before 28 | public void setUp() { 29 | MockitoAnnotations.initMocks(this); 30 | } 31 | 32 | protected abstract T constructReporter(); 33 | 34 | protected abstract void testCase(Object[] args); 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014-2016 Etsy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /visualization/influxdb-dashboard/views/layout-render.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | block head 5 | block title 6 | title Profiler Dashboard 7 | block meta 8 | meta(name='description', content='') 9 | meta(name='keywords', content='') 10 | link(rel='stylesheet', href='/css/bootstrap.min.css') 11 | link(rel='stylesheet', href='/css/toc.css') 12 | link(rel='stylesheet', href='//cdnjs.cloudflare.com/ajax/libs/c3/0.4.10/c3.min.css') 13 | | 16 | script(src='//ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js') 17 | script(src='//cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js') 18 | script(src='//cdnjs.cloudflare.com/ajax/libs/c3/0.4.10/c3.min.js') 19 | script(src='//cdnjs.cloudflare.com/ajax/libs/URI.js/1.15.1/URI.min.js') 20 | script(src='//cdnjs.cloudflare.com/ajax/libs/URI.js/1.15.1/jquery.URI.min.js') 21 | script(src='/scripts/view-util.js') 22 | script(src='/scripts/toc.min.js') 23 | script(src='/scripts/jquery.sticky-kit.min.js') 24 | body 25 | include header-render 26 | 27 | block content 28 | 29 | include footer 30 | -------------------------------------------------------------------------------- /src/test/java/com/etsy/statsd/profiler/profilers/MockProfilerWithArguments.java: -------------------------------------------------------------------------------- 1 | package com.etsy.statsd.profiler.profilers; 2 | 3 | import com.etsy.statsd.profiler.Arguments; 4 | import com.etsy.statsd.profiler.Profiler; 5 | import com.etsy.statsd.profiler.reporter.Reporter; 6 | 7 | import java.util.concurrent.TimeUnit; 8 | 9 | /** 10 | * Mock profiler for testing 11 | * 12 | * @author Andrew Johnson 13 | */ 14 | public class MockProfilerWithArguments extends Profiler { 15 | public static final String FAKE_ARG = "fakeArg"; 16 | 17 | public String fake; 18 | 19 | public MockProfilerWithArguments(Reporter reporter, Arguments arguments) { 20 | super(reporter, arguments); 21 | } 22 | 23 | @Override 24 | public void profile() { } 25 | 26 | @Override 27 | public void flushData() { } 28 | 29 | @Override 30 | public long getPeriod() { 31 | return 0; 32 | } 33 | 34 | @Override 35 | public TimeUnit getTimeUnit() { 36 | return null; 37 | } 38 | 39 | @Override 40 | protected void handleArguments(Arguments arguments) { 41 | fake = arguments.remainingArgs.get(FAKE_ARG); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/test/java/com/etsy/statsd/profiler/reporter/StatsDReporterTest.java: -------------------------------------------------------------------------------- 1 | package com.etsy.statsd.profiler.reporter; 2 | 3 | import com.etsy.statsd.profiler.reporter.mock.BaseReporterTest; 4 | import com.etsy.statsd.profiler.util.MockArguments; 5 | import com.timgroup.statsd.StatsDClient; 6 | import org.junit.Test; 7 | import org.mockito.Matchers; 8 | import org.mockito.Mock; 9 | import org.mockito.Mockito; 10 | 11 | import static org.junit.Assert.assertEquals; 12 | 13 | public class StatsDReporterTest extends BaseReporterTest { 14 | @Mock 15 | private StatsDClient client; 16 | 17 | @Override 18 | protected StatsDReporter constructReporter() { 19 | return new StatsDReporter(MockArguments.BASIC); 20 | } 21 | 22 | @Override 23 | protected void testCase(Object[] args) { 24 | assertEquals(2, args.length); 25 | assertEquals("fake", args[0]); 26 | assertEquals(100L, args[1]); 27 | } 28 | 29 | @Test 30 | public void testRecordGaugeValue() { 31 | Mockito.doAnswer(answer).when(client).recordGaugeValue(Matchers.anyString(), Matchers.anyLong()); 32 | reporter.recordGaugeValue("fake", 100L); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribute 2 | These are the basic steps for contributing to statsd-jvm-profiler: 3 | 4 | 1. Fork from [the master branch](https://github.com/etsy/statsd-jvm-profiler) 5 | 2. Make your changes 6 | 3. Document new functionality as appropriate 7 | 4. Add new tests if possible 8 | 5. Run the test suite with `mvn test` 9 | 6. Push your changes to your fork. 10 | 7. Send a pull request! 11 | 12 | Every pull request will be built with [Travis CI](https://travis-ci.org/etsy/statsd-jvm-profiler). The CI build runs both unit tests and Checkstyle. 13 | 14 | # Contributors 15 | - Andrew Johnson [ajsquared](https://github.com/ajsquared) 16 | - Dan Osipov [danosipov](https://github.com/danosipov) 17 | - Slawek Puklo [spuklo](https://github.com/spuklo) 18 | - Andrew Stiegmann [stieg](https://github.com/stieg) 19 | - Joe Meissler [stickperson](https://github.com/stickperson) 20 | - Ben Darfler [bdarfler](https://github.com/bdarfler) 21 | - Ihor Bobak [ibobak](https://github.com/ibobak) 22 | - Jeff Fenchel [jfenc91](https://github.com/jfenc91) 23 | - Alejandro Rivera [AlejandroRivera](https://github.com/AlejandroRivera) 24 | - Mohamed Ezzat [m-ezzat](https://github.com/m-ezzat) 25 | - Ikariusrb [Ikariusrb](https://github.com/Ikariusrb) 26 | -------------------------------------------------------------------------------- /src/test/java/com/etsy/statsd/profiler/profilers/MockProfiler1.java: -------------------------------------------------------------------------------- 1 | package com.etsy.statsd.profiler.profilers; 2 | 3 | import com.etsy.statsd.profiler.Arguments; 4 | import com.etsy.statsd.profiler.Profiler; 5 | import com.etsy.statsd.profiler.reporter.MockReporter; 6 | 7 | import java.util.Set; 8 | import java.util.concurrent.TimeUnit; 9 | 10 | /** 11 | * Mock profiler for testing 12 | * 13 | * @author Andrew Johnson 14 | */ 15 | public class MockProfiler1 extends Profiler { 16 | private Set output; 17 | 18 | public MockProfiler1(Set output) { 19 | super(new MockReporter(), null); 20 | this.output = output; 21 | } 22 | 23 | @Override 24 | public void profile() { 25 | output.add(this.getClass().getSimpleName() + "-profile"); 26 | } 27 | 28 | @Override 29 | public void flushData() { 30 | output.add(this.getClass().getSimpleName() + "-flushData"); 31 | } 32 | 33 | @Override 34 | public long getPeriod() { 35 | return 1; 36 | } 37 | 38 | @Override 39 | public TimeUnit getTimeUnit() { 40 | return TimeUnit.MINUTES; 41 | } 42 | 43 | @Override 44 | protected void handleArguments(Arguments arguments) { } 45 | } 46 | -------------------------------------------------------------------------------- /src/test/java/com/etsy/statsd/profiler/profilers/MockProfiler2.java: -------------------------------------------------------------------------------- 1 | package com.etsy.statsd.profiler.profilers; 2 | 3 | import com.etsy.statsd.profiler.Arguments; 4 | import com.etsy.statsd.profiler.Profiler; 5 | import com.etsy.statsd.profiler.reporter.MockReporter; 6 | 7 | import java.util.Set; 8 | import java.util.concurrent.TimeUnit; 9 | 10 | /** 11 | * Mock profiler for testing 12 | * 13 | * @author Andrew Johnson 14 | */ 15 | public class MockProfiler2 extends Profiler { 16 | private Set output; 17 | 18 | public MockProfiler2(Set output) { 19 | super(new MockReporter(), null); 20 | this.output = output; 21 | } 22 | 23 | @Override 24 | public void profile() { 25 | output.add(this.getClass().getSimpleName() + "-profile"); 26 | } 27 | 28 | @Override 29 | public void flushData() { 30 | output.add(this.getClass().getSimpleName() + "-flushData"); 31 | } 32 | 33 | @Override 34 | public long getPeriod() { 35 | return 1; 36 | } 37 | 38 | @Override 39 | public TimeUnit getTimeUnit() { 40 | return TimeUnit.MINUTES; 41 | } 42 | 43 | @Override 44 | protected void handleArguments(Arguments arguments) { } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/com/etsy/statsd/profiler/util/Range.java: -------------------------------------------------------------------------------- 1 | package com.etsy.statsd.profiler.util; 2 | 3 | /** 4 | * Represents an immutable range of integers 5 | */ 6 | public class Range { 7 | private final int left; 8 | private final int right; 9 | 10 | public Range(int left, int right) { 11 | this.left = left; 12 | this.right = right; 13 | } 14 | 15 | public int getLeft() { 16 | return left; 17 | } 18 | 19 | public int getRight() { 20 | return right; 21 | } 22 | 23 | @Override 24 | public boolean equals(Object o) { 25 | if (this == o) { 26 | return true; 27 | } 28 | if (o == null || getClass() != o.getClass()) { 29 | return false; 30 | } 31 | 32 | Range range = (Range) o; 33 | 34 | return left == range.left && right == range.right; 35 | 36 | } 37 | 38 | @Override 39 | public int hashCode() { 40 | int result = left; 41 | result = 31 * result + right; 42 | return result; 43 | } 44 | 45 | @Override 46 | public String toString() { 47 | return "Range{" + 48 | "left=" + left + 49 | ", right=" + right + 50 | '}'; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/com/etsy/statsd/profiler/worker/ProfilerThreadFactory.java: -------------------------------------------------------------------------------- 1 | package com.etsy.statsd.profiler.worker; 2 | 3 | import java.util.concurrent.Executors; 4 | import java.util.concurrent.ThreadFactory; 5 | 6 | /** 7 | * ThreadFactory for the profiler threads 8 | * This factory prefixes the thread name with 'statsd-jvm-profiler' 9 | * This allows the profilers to identify other profiler threads 10 | * 11 | * @author Andrew Johnson 12 | */ 13 | public class ProfilerThreadFactory implements ThreadFactory { 14 | public static final String NAME_PREFIX = "statsd-jvm-profiler"; 15 | 16 | private final ThreadFactory defaultThreadFactory = Executors.defaultThreadFactory(); 17 | 18 | /** 19 | * Create a profiler thread with the name prefixed with 'statsd-jvm-profiler' 20 | * @param r A runnable to be executed by new thread instance 21 | * @return Constructed thread, or {@code null} if the request to 22 | * create a thread is rejected 23 | */ 24 | @Override 25 | public Thread newThread(Runnable r) { 26 | Thread t = defaultThreadFactory.newThread(r); 27 | if (t != null) { 28 | t.setName(String.format("%s-%s", NAME_PREFIX, t.getName())); 29 | } 30 | 31 | return t; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/etsy/statsd/profiler/worker/ProfilerWorkerThread.java: -------------------------------------------------------------------------------- 1 | package com.etsy.statsd.profiler.worker; 2 | 3 | import com.etsy.statsd.profiler.Profiler; 4 | 5 | import java.io.PrintWriter; 6 | import java.io.StringWriter; 7 | import java.util.LinkedList; 8 | import java.util.List; 9 | 10 | /** 11 | * Worker thread for executing a profiler 12 | * 13 | * @author Andrew Johnson 14 | */ 15 | public class ProfilerWorkerThread implements Runnable { 16 | private final Profiler profiler; 17 | private final List errors; 18 | 19 | public ProfilerWorkerThread(Profiler profiler, List errors) { 20 | this.profiler = profiler; 21 | this.errors = errors; 22 | } 23 | 24 | @Override 25 | public void run() { 26 | try { 27 | profiler.profile(); 28 | } catch (Exception e) { 29 | StringWriter sw = new StringWriter(); 30 | PrintWriter pw = new PrintWriter(sw); 31 | e.printStackTrace(pw); 32 | errors.add(String.format("Received an error running profiler: %s, error: %s", profiler.getClass().getName(), sw.toString())); 33 | if (errors.size() > 10) { 34 | ((LinkedList) errors).pollFirst(); 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/test/java/com/etsy/statsd/profiler/util/MockArguments.java: -------------------------------------------------------------------------------- 1 | package com.etsy.statsd.profiler.util; 2 | 3 | import com.etsy.statsd.profiler.Arguments; 4 | 5 | import java.util.Map; 6 | 7 | /** 8 | * Utility class to create mock arguments for testing 9 | */ 10 | public final class MockArguments { 11 | public static final Arguments BASIC = createArgs("localhost", 8888, "statsd-jvm-profiler", null); 12 | 13 | private MockArguments() { } 14 | 15 | /** 16 | * Create an Arguments instance for testing 17 | * 18 | * @param server The server argument 19 | * @param port The port argument 20 | * @param prefix The prefix argument 21 | * @param otherArgs Any additional arguments to include 22 | * @return An Arguments instance containing all the given arguments 23 | */ 24 | public static Arguments createArgs(String server, int port, String prefix, Map otherArgs) { 25 | String args = String.format("server=%s,port=%d,prefix=%s", server, port, prefix); 26 | if (otherArgs != null) { 27 | for (Map.Entry entry : otherArgs.entrySet()) { 28 | args += String.format(",%s=%s", entry.getKey(), entry.getValue()); 29 | } 30 | } 31 | 32 | return Arguments.parseArgs(args); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/etsy/statsd/profiler/util/TimeUtil.java: -------------------------------------------------------------------------------- 1 | package com.etsy.statsd.profiler.util; 2 | 3 | import java.util.concurrent.TimeUnit; 4 | 5 | /** 6 | * Utility class for working with time 7 | * 8 | * @author Andrew Johnson 9 | */ 10 | public final class TimeUtil { 11 | private TimeUtil() { } 12 | 13 | /** 14 | * Convert a reporting period into the time scale of a profiling period 15 | * 16 | * @param profilePeriod The profiling period 17 | * @param profileTimeUnit The TimeUnit for the profiling period 18 | * @param reportingPeriod The reporting period 19 | * @param reportingTimeUnit The TimeUnit for the reporting period 20 | * @return The reporting period scaled to the profiling period (i.e. suitable for use like x % convertReportingPeriod(...) == 0) 21 | */ 22 | public static long convertReportingPeriod(long profilePeriod, TimeUnit profileTimeUnit, long reportingPeriod, TimeUnit reportingTimeUnit) { 23 | long convertedReportingPeriod = profileTimeUnit.convert(reportingPeriod, reportingTimeUnit); 24 | 25 | // If we profile less frequently than we want report, returning 1 would indicate we should always report 26 | if (convertedReportingPeriod <= profilePeriod) { 27 | return 1; 28 | } 29 | 30 | return convertedReportingPeriod / profilePeriod; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/etsy/statsd/profiler/util/StackTraceFormatter.java: -------------------------------------------------------------------------------- 1 | package com.etsy.statsd.profiler.util; 2 | 3 | import com.google.common.base.Joiner; 4 | 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | 8 | /** 9 | * Utility class for formatting stack traces 10 | * 11 | * @author Andrew Johnson 12 | */ 13 | public final class StackTraceFormatter { 14 | private StackTraceFormatter() { } 15 | 16 | /** 17 | * Formats a StackTraceElement as a String, excluding the line number 18 | * 19 | * @param element The StackTraceElement to format 20 | * @return A String representing the given StackTraceElement 21 | */ 22 | public static String formatStackTraceElement(StackTraceElement element) { 23 | return String.format("%s-%s-%d", element.getClassName().replace(".", "-"), element.getMethodName(), element.getLineNumber()); 24 | } 25 | 26 | /** 27 | * Formats an entire stack trace as a String 28 | * 29 | * @param stack The stack trace to format 30 | * @return A String representing the given stack trace 31 | */ 32 | public static String formatStackTrace(StackTraceElement[] stack) { 33 | List lines = new ArrayList<>(); 34 | lines.add("cpu"); 35 | lines.add("trace"); 36 | for (StackTraceElement element : stack) { 37 | lines.add(formatStackTraceElement(element)); 38 | } 39 | 40 | return Joiner.on(".").join(lines); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /visualization/influxdb-dashboard/app.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var express = require('express') 7 | , routes = require('./routes') 8 | , http = require('http') 9 | , path = require('path') 10 | , bodyParser = require('body-parser') 11 | , favicon = require('serve-favicon') 12 | , logger = require('morgan') 13 | , methodOverride = require('method-override'); 14 | 15 | var app = express(); 16 | 17 | app.set('port', process.env.PORT || 3888); 18 | app.set('views', __dirname + '/views'); 19 | app.set('view engine', 'jade'); 20 | app.use(favicon(__dirname + '/public/images/favicon.ico')); 21 | app.use(logger('dev')); 22 | app.use(bodyParser.urlencoded({ extended: false })); 23 | app.use(methodOverride('_method')); 24 | app.use(require('stylus').middleware(__dirname + '/public')); 25 | app.use(express.static(path.join(__dirname, 'public'))); 26 | 27 | if (app.get('env') == 'development') { 28 | app.locals.pretty = true; 29 | } 30 | 31 | app.get('/', routes.index); 32 | 33 | app.get('/config', routes.config); 34 | app.get('/options', routes.options); 35 | app.get('/render', routes.render); 36 | app.get('/data/:user/:job/:flow/:stage/:phase/:jvm?/:metric', routes.data) 37 | 38 | app.get('/cpu/:user/:job/:flow/:stage/:phase/:jvm?/:prefix', routes.cpu); 39 | 40 | http.createServer(app).listen(app.get('port'), function(){ 41 | console.log("Express server listening on port " + app.get('port')); 42 | }); 43 | 44 | process.title = 'statsd-jvm-profiler-dash'; 45 | -------------------------------------------------------------------------------- /src/test/java/com/etsy/statsd/profiler/worker/ProfilerShutdownHookWorkerTest.java: -------------------------------------------------------------------------------- 1 | package com.etsy.statsd.profiler.worker; 2 | 3 | import com.etsy.statsd.profiler.Profiler; 4 | import com.etsy.statsd.profiler.profilers.MockProfiler1; 5 | import com.etsy.statsd.profiler.profilers.MockProfiler2; 6 | import org.junit.Test; 7 | 8 | import java.util.Arrays; 9 | import java.util.Collection; 10 | import java.util.HashSet; 11 | import java.util.Set; 12 | import java.util.concurrent.atomic.AtomicReference; 13 | 14 | import static org.junit.Assert.assertEquals; 15 | 16 | public class ProfilerShutdownHookWorkerTest { 17 | @Test 18 | public void testRunnable() throws InterruptedException { 19 | Set output = new HashSet<>(); 20 | Profiler mockProfiler1 = new MockProfiler1(output); 21 | Profiler mockProfiler2 = new MockProfiler2(output); 22 | Collection profilers = Arrays.asList(mockProfiler1, mockProfiler2); 23 | AtomicReference isRunning = new AtomicReference<>(true); 24 | 25 | Thread t = new Thread(new ProfilerShutdownHookWorker(profilers, isRunning)); 26 | t.run(); 27 | t.join(); 28 | 29 | Set expectedOutput = new HashSet<>(); 30 | expectedOutput.add(MockProfiler1.class.getSimpleName() + "-flushData"); 31 | expectedOutput.add(MockProfiler2.class.getSimpleName() + "-flushData"); 32 | assertEquals(expectedOutput, output); 33 | assertEquals(isRunning.get(), false); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /visualization/influxdb-dashboard/public/scripts/jquery.redirect.js: -------------------------------------------------------------------------------- 1 | /* jQuery POST/GET redirect method 2 | v.0.1 3 | modified by Miguel Galante,https://github.com/mgalante 4 | v.0.1 5 | made by Nemanja Avramovic, www.avramovic.info 6 | */ 7 | 8 | ;(function( $ ){ 9 | 10 | $.redirect = function( target, values, method ) { 11 | 12 | method = (method && method.toUpperCase() == 'GET') ? 'GET' : 'POST'; 13 | 14 | if (!values) 15 | { 16 | var obj = $.parse_url(target); 17 | target = obj.url; 18 | values = obj.params; 19 | } 20 | 21 | var form = $('
',{attr:{ 22 | method: method, 23 | action: target 24 | }}); 25 | 26 | for(var i in values) 27 | { 28 | $('',{ 29 | attr:{ 30 | type: 'hidden', 31 | name: i, 32 | value: values[i] 33 | } 34 | }).appendTo(form); 35 | 36 | } 37 | 38 | $('body').append(form); 39 | console.log(form); 40 | form.submit(); 41 | }; 42 | 43 | $.parse_url = function(url) 44 | { 45 | if (url.indexOf('?') == -1) 46 | return { url: url, params: {} } 47 | 48 | var parts = url.split('?'); 49 | var url = parts[0]; 50 | var query_string = parts[1]; 51 | 52 | var return_obj = {}; 53 | var elems = query_string.split('&'); 54 | 55 | var obj = {}; 56 | 57 | for(var i in elems) 58 | { 59 | var elem = elems[i]; 60 | var pair = elem.split('='); 61 | obj[pair[0]] = pair[1]; 62 | } 63 | 64 | return_obj.url = url; 65 | return_obj.params = obj; 66 | 67 | return return_obj; 68 | } 69 | })( jQuery ); -------------------------------------------------------------------------------- /src/test/java/com/etsy/statsd/profiler/util/CPUTracesTest.java: -------------------------------------------------------------------------------- 1 | package com.etsy.statsd.profiler.util; 2 | 3 | import org.junit.Before; 4 | import org.junit.Test; 5 | 6 | import java.util.HashMap; 7 | import java.util.Map; 8 | 9 | import static org.junit.Assert.assertEquals; 10 | 11 | public class CPUTracesTest { 12 | private CPUTraces traces; 13 | 14 | @Before 15 | public void setup() { 16 | traces = new CPUTraces(); 17 | } 18 | 19 | @Test 20 | public void testGetDataToFlush() { 21 | traces.increment("cpu.trace.key", 1); 22 | traces.increment("cpu.trace.key2", 3); 23 | 24 | Map expectedMap = new HashMap<>(); 25 | expectedMap.put("cpu.trace.key", 1L); 26 | expectedMap.put("cpu.trace.key2", 3L); 27 | 28 | assertEquals(expectedMap, traces.getDataToFlush()); 29 | 30 | traces.increment("cpu.trace.key3", 100); 31 | traces.increment("cpu.trace.key2", 3); 32 | 33 | expectedMap = new HashMap<>(); 34 | expectedMap.put("cpu.trace.key2", 3L); 35 | expectedMap.put("cpu.trace.key3", 100L); 36 | 37 | assertEquals(expectedMap, traces.getDataToFlush()); 38 | } 39 | 40 | @Test 41 | public void testGetBounds() { 42 | traces.increment("cpu.trace.a.b.c", 1); 43 | traces.increment("cpu.trace.a.b.c.d", 1); 44 | traces.increment("cpu.trace.a.b.c.d.e", 1); 45 | 46 | Range bounds = traces.getBounds(); 47 | assertEquals(3, bounds.getLeft()); 48 | assertEquals(5, bounds.getRight()); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/com/etsy/statsd/profiler/util/MapUtil.java: -------------------------------------------------------------------------------- 1 | package com.etsy.statsd.profiler.util; 2 | 3 | import java.util.Map; 4 | 5 | /** 6 | * Utility class for working with Maps 7 | * 8 | * @author Andrew Johnson 9 | */ 10 | public final class MapUtil { 11 | private MapUtil() { } 12 | 13 | /** 14 | * Set a new value in a map or increment an existing value 15 | * 16 | * @param map The map in which to modify the value 17 | * @param key The key for the map 18 | * @param inc The new value or increment for the given key 19 | */ 20 | public static void setOrIncrementMap(Map map, String key, Number inc) { 21 | Number val = map.get(key); 22 | if (val == null) { 23 | if (inc instanceof Double) { 24 | map.put(key, inc.doubleValue()); 25 | } else if (inc instanceof Long || inc instanceof Integer) { 26 | map.put(key, inc.longValue()); 27 | } else { 28 | throw new IllegalArgumentException("Unexpected Number type: " + inc.getClass().getSimpleName()); 29 | } 30 | } else { 31 | if (val instanceof Double) { 32 | map.put(key, val.doubleValue() + inc.doubleValue()); 33 | } else if (val instanceof Long || val instanceof Integer) { 34 | map.put(key, val.longValue() + inc.longValue()); 35 | } else { 36 | throw new IllegalArgumentException("Unexpected Number type: " + val.getClass().getSimpleName()); 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /visualization/influxdb-dashboard/public/scripts/view-util.js: -------------------------------------------------------------------------------- 1 | var ViewUtil = (function($) { 2 | var view = {}; 3 | 4 | view.getMetricsForPool = function(pool, metrics) { 5 | return metrics.map(function(metric) { 6 | return {metric: pool + '.' + metric.metric, alias: metric.alias }; 7 | }); 8 | } 9 | 10 | view.renderSelect = function(id, users) { 11 | var html = ''; 16 | $(id).hide().html(html).show(); 17 | } 18 | 19 | view.renderGraph = function(results, title, div, metrics) { 20 | if (Object.keys(results).length === 0) { 21 | $(div).hide().html('Unable to retrieve metrics').show(); 22 | return; 23 | } 24 | var data = metrics.map(function(metric) { 25 | var values = $.grep(results, function(e){ return e.metric == metric.metric; })[0]['values']; 26 | return [[metric.metric].concat(values.map(function(value) { 27 | return value.time; 28 | })), 29 | [metric.alias].concat(values.map(function(value) { 30 | return value.value; 31 | }))]; 32 | }); 33 | 34 | var columns = [].concat.apply([], data); 35 | var xs = {}; 36 | metrics.map(function(m) { 37 | xs[m.alias] = m.metric; 38 | }); 39 | c3.generate({ 40 | bindto: div, 41 | data: { 42 | xs: xs, 43 | columns: columns 44 | }, 45 | axis: { 46 | y: { 47 | tick: { 48 | format: d3.format('s') 49 | } 50 | } 51 | }, 52 | zoom: { 53 | enabled: true 54 | } 55 | }); 56 | } 57 | 58 | return view; 59 | 60 | }(jQuery)); 61 | -------------------------------------------------------------------------------- /src/test/java/com/etsy/statsd/profiler/ProfilerTest.java: -------------------------------------------------------------------------------- 1 | package com.etsy.statsd.profiler; 2 | 3 | import com.etsy.statsd.profiler.profilers.MockProfilerWithArguments; 4 | import com.etsy.statsd.profiler.reporter.MockReporter; 5 | import org.junit.Test; 6 | 7 | import java.util.concurrent.TimeUnit; 8 | 9 | import static org.junit.Assert.assertEquals; 10 | 11 | public class ProfilerTest { 12 | @Test 13 | public void testHandleArguments() { 14 | Arguments arguments = Arguments.parseArgs("server=hostname,port=1234,fakeArg=notreal"); 15 | MockProfilerWithArguments profiler = new MockProfilerWithArguments(new MockReporter(), arguments); 16 | assertEquals("notreal", profiler.fake); 17 | } 18 | 19 | @Test(expected = NullPointerException.class) 20 | public void testNullReporterArg() { 21 | new Profiler(null, null) { 22 | @Override 23 | public void profile() { } 24 | 25 | @Override 26 | public void flushData() { } 27 | 28 | @Override 29 | public long getPeriod() { 30 | return 0; 31 | } 32 | 33 | @Override 34 | public TimeUnit getTimeUnit() { 35 | return null; 36 | } 37 | 38 | @Override 39 | protected void handleArguments(Arguments arguments) { } 40 | }; 41 | } 42 | 43 | @Test 44 | public void testGetApplicationId() { 45 | String containerId = "container_1486158130664_0002_01_000157"; 46 | String applicationId = MockProfilerWithArguments.getApplicationId(containerId); 47 | assertEquals(applicationId, "application_1486158130664_0002"); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /visualization/influxdb-dashboard/views/render.jade: -------------------------------------------------------------------------------- 1 | extends layout-render 2 | 3 | block content 4 | div.container 5 | text.hidden#refresh #{refresh} 6 | text.hidden#prefix #{prefix} 7 | div.panel.panel-default 8 | div.panel-heading(style='text-align:center') 9 | h2 Memory 10 | div.panel-body#memory 11 | div(style='text-align:center') 12 | h3 Heap Usage 13 | div#heap 14 | div(style='text-align:center') 15 | h3 Non-Heap Usage 16 | div#nonheap 17 | div.panel.panel-default 18 | div.panel-heading(style='text-align:center') 19 | h2 Garbage Collection 20 | div.panel-body#gc 21 | div(style='text-align:center') 22 | h3 GC Count 23 | div#count 24 | div(style='text-align:center') 25 | h3 Total GC Time (ms) 26 | div#time 27 | div(style='text-align:center') 28 | h3 GC Time (ms) 29 | div#runtime 30 | div.panel.panel-default 31 | div.panel-heading(style='text-align:center') 32 | h2 Classes 33 | div.panel-body#loading 34 | div(style='text-align:center') 35 | h3 Class Loading 36 | div#classloading 37 | div.panel.panel-default 38 | div.panel-heading(style='text-align:center') 39 | h2 Memory Pools 40 | div.panel-body#pool 41 | div(style='text-align:center') 42 | h3 Eden 43 | div#eden 44 | div(style='text-align:center') 45 | h3 Old Generation 46 | div#oldgen 47 | div(style='text-align:center') 48 | h3 Survivor Space 49 | div#survivor 50 | div(style='text-align:center') 51 | h3 Code Cache 52 | div#codecache 53 | div(style='text-align:center') 54 | h3 Permgen 55 | div#permgen 56 | div.panel.panel-default 57 | div.panel-heading(style='text-align:center') 58 | h2 Finalization 59 | div.panel-body#finalization 60 | div(style='text-align:center') 61 | h3 Objects Pending Finalization 62 | div#finalize 63 | 64 | div.panel.panel-default#toc 65 | 66 | script(src='/scripts/render-client.js') 67 | -------------------------------------------------------------------------------- /visualization/influxdb-dashboard/public/scripts/index-client.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | var options = null; 3 | 4 | var getJobs = function() { 5 | var user = $('#users').val(); 6 | ViewUtil.renderSelect('#jobs', Object.keys(options[user])); 7 | $('#jobs').attr('name', 'job'); 8 | getFlows(); 9 | }; 10 | 11 | var getFlows = function() { 12 | var user = $('#users').val(); 13 | var job = $('#jobs').val(); 14 | ViewUtil.renderSelect('#flows', Object.keys(options[user][job])); 15 | $('#flows').attr('name', 'flow'); 16 | getStages(); 17 | }; 18 | 19 | var getStages = function() { 20 | var user = $('#users').val(); 21 | var job = $('#jobs').val(); 22 | var flow = $('#flows').val(); 23 | ViewUtil.renderSelect('#stages', Object.keys(options[user][job][flow])); 24 | $('#stages').attr('name', 'stage'); 25 | getPhases(); 26 | }; 27 | 28 | var getPhases = function() { 29 | var user = $('#users').val(); 30 | var job = $('#jobs').val(); 31 | var flow = $('#flows').val(); 32 | var stage = $('#stages').val(); 33 | ViewUtil.renderSelect('#phases', Object.keys(options[user][job][flow][stage])); 34 | $('#phases').attr('name', 'phase'); 35 | getJvms(); 36 | } 37 | 38 | var getJvms = function() { 39 | var user = $('#users').val(); 40 | var job = $('#jobs').val(); 41 | var flow = $('#flows').val(); 42 | var stage = $('#stages').val(); 43 | var phase = $('#phases').val(); 44 | ViewUtil.renderSelect('#jvms', options[user][job][flow][stage][phase]); 45 | $('#jvms').attr('name', 'jvm'); 46 | } 47 | 48 | var refresh = function(o) { 49 | options = o; 50 | ViewUtil.renderSelect('#users', Object.keys(options)); 51 | $('#users').attr('name', 'user'); 52 | 53 | getJobs(); 54 | } 55 | 56 | $.get('/options', refresh); 57 | 58 | $('#users').change(getJobs); 59 | $('#jobs').change(getFlows); 60 | $('#flows').change(getStages); 61 | $('#stages').change(getPhases); 62 | $('#phases').change(getJvms); 63 | }); 64 | -------------------------------------------------------------------------------- /src/test/java/com/etsy/statsd/profiler/reporter/MockReporter.java: -------------------------------------------------------------------------------- 1 | package com.etsy.statsd.profiler.reporter; 2 | 3 | import com.etsy.statsd.profiler.Arguments; 4 | import com.etsy.statsd.profiler.util.MapUtil; 5 | import com.etsy.statsd.profiler.util.MockArguments; 6 | 7 | import java.util.HashMap; 8 | import java.util.Map; 9 | 10 | /** 11 | * Mock reporter for testing 12 | * 13 | * @author Andrew Johnson 14 | */ 15 | public class MockReporter extends Reporter { 16 | private Map output; 17 | 18 | public MockReporter() { 19 | super(MockArguments.BASIC); 20 | output = new HashMap<>(); 21 | } 22 | 23 | @Override 24 | public void recordGaugeValue(String key, long value, String... tags) { 25 | MapUtil.setOrIncrementMap(output, key, value); 26 | } 27 | 28 | @Override 29 | public void recordGaugeValues(Map gauges, String... tags) { 30 | for (Map.Entry gauge : gauges.entrySet()) { 31 | if (gauge.getValue() instanceof Long) { 32 | recordGaugeValue(gauge.getKey(), gauge.getValue().longValue()); 33 | } else if (gauge.getValue() instanceof Double) { 34 | recordGaugeValue(gauge.getKey(), gauge.getValue().doubleValue()); 35 | } else { 36 | throw new IllegalArgumentException("Unexpected Number type: " + gauge.getValue().getClass().getSimpleName()); 37 | } 38 | } 39 | } 40 | 41 | @Override 42 | public void recordGaugeValue(String key, double value, String... tags) { 43 | MapUtil.setOrIncrementMap(output, key, value); 44 | } 45 | 46 | @Override 47 | protected String createClient(String server, int port, String prefix) { 48 | return ""; 49 | } 50 | 51 | @Override 52 | protected void handleArguments(Arguments arguments) { } 53 | 54 | public Map getOutput() { 55 | return output; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/test/java/com/etsy/statsd/profiler/util/StackTraceFormatterTest.java: -------------------------------------------------------------------------------- 1 | package com.etsy.statsd.profiler.util; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | public class StackTraceFormatterTest { 8 | @Test 9 | public void testFormatStackTraceElement() { 10 | StackTraceElement element = new StackTraceElement("com.etsy.statsd.profiler.util.StackTraceFormatter", 11 | "formatStackTraceElement", "StackTraceFormatter.java", 21); 12 | StackTraceElement elementNoFile = new StackTraceElement("com.etsy.statsd.profiler.util.StackTraceFormatter", 13 | "formatStackTraceElement", null, 21); 14 | 15 | String expected = "com-etsy-statsd-profiler-util-StackTraceFormatter-formatStackTraceElement-21"; 16 | 17 | assertEquals(expected, StackTraceFormatter.formatStackTraceElement(element)); 18 | assertEquals(expected, StackTraceFormatter.formatStackTraceElement(elementNoFile)); 19 | } 20 | 21 | @Test 22 | public void testFormatEmptyStackTrace() { 23 | StackTraceElement[] stack = new StackTraceElement[0]; 24 | 25 | assertEquals("cpu.trace", StackTraceFormatter.formatStackTrace(stack)); 26 | } 27 | 28 | @Test 29 | public void testFormatStackTrace() { 30 | StackTraceElement[] stack = new StackTraceElement[2]; 31 | stack[0] = new StackTraceElement("com.etsy.statsd.profiler.util.StackTraceFormatter", 32 | "formatStackTraceElement", "StackTraceFormatter.java", 21); 33 | stack[1] = new StackTraceElement("com.etsy.statsd.profiler.util.StackTraceFormatterTest", 34 | "testFormatStackTraceElement", "StackTraceFormatterTest.java", 17); 35 | 36 | String expected = "cpu.trace.com-etsy-statsd-profiler-util-StackTraceFormatter-formatStackTraceElement-21.com-etsy-statsd-profiler-util-StackTraceFormatterTest-testFormatStackTraceElement-17"; 37 | 38 | assertEquals(expected, StackTraceFormatter.formatStackTrace(stack)); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /visualization/influxdb-dashboard/routes/index.js: -------------------------------------------------------------------------------- 1 | var influx = require('../public/scripts/influxdb.js'); 2 | var flamegraph = require('../public/scripts/flamegraph.js'); 3 | var config = require('../public/scripts/config.js'); 4 | var prefix = null; 5 | var refresh = null 6 | config.getConfig(function(conf) { 7 | prefix = conf['prefix']; 8 | refresh = conf['refresh']; 9 | }) 10 | 11 | exports.index = function(req, res){ 12 | res.render('index'); 13 | }; 14 | 15 | exports.render = function(req, res){ 16 | res.render('render', {user: req.query['user'], job: req.query['job'], flow: req.query['flow'], stage: req.query['stage'], phase: req.query['phase'], jvmName: req.query['jvm'], 'prefix': prefix, 'refresh': refresh}); 17 | }; 18 | 19 | exports.config = function(req, res) { 20 | config.getConfig(function(conf) { 21 | res.json(conf); 22 | }); 23 | }; 24 | 25 | exports.options = function(req, res) { 26 | influx.getOptions(prefix, function(data) { res.json(data) }); 27 | }; 28 | 29 | exports.data = function(req, res) { 30 | var user = req.params['user']; 31 | var job = req.params['job']; 32 | var flow = req.params['flow']; 33 | var stage = req.params['stage']; 34 | var phase = req.params['phase']; 35 | var jvmName = req.params['jvm']; 36 | var metric = req.params['metric']; 37 | influx.getData(user, job, flow, stage, phase, jvmName, metric, function(data) { 38 | res.json(data); 39 | }); 40 | } 41 | 42 | exports.cpu = function(req, res) { 43 | var user = req.params['user']; 44 | var job = req.params['job']; 45 | var flow = req.params['flow']; 46 | var stage = req.params['stage']; 47 | var phase = req.params['phase']; 48 | var jvmName = req.params['jvm']; 49 | var prefix = req.params['prefix']; 50 | influx.getFlameGraphData(user, job, flow, stage, phase, jvmName, prefix, function(metrics) { 51 | flamegraph.getFlameGraph(metrics, function(data) { 52 | res.setHeader('content-type', 'image/svg+xml'); 53 | res.send(data); 54 | }) 55 | }); 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/com/etsy/statsd/profiler/util/CPUTraces.java: -------------------------------------------------------------------------------- 1 | package com.etsy.statsd.profiler.util; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | 6 | /** 7 | * Represents the state of the CPU profiler 8 | * 9 | * @author Andrew Johnson 10 | */ 11 | public class CPUTraces { 12 | private Map traces; 13 | private int max = Integer.MIN_VALUE; 14 | private int min = Integer.MAX_VALUE; 15 | 16 | public CPUTraces() { 17 | traces = new HashMap<>(); 18 | } 19 | 20 | /** 21 | * Increment the aggregate time for a trace 22 | * 23 | * @param traceKey The key for the trace 24 | * @param inc The value by which to increment the aggregate time for the trace 25 | */ 26 | public void increment(String traceKey, long inc) { 27 | MapUtil.setOrIncrementMap(traces, traceKey, inc); 28 | updateBounds(traceKey); 29 | } 30 | 31 | /** 32 | * Get data to be flushed from the state 33 | * It only returns traces that have been updated since the last flush 34 | * 35 | */ 36 | public Map getDataToFlush() { 37 | Map result = traces; 38 | traces = new HashMap<>(); 39 | return result; 40 | } 41 | 42 | /** 43 | * Get the bounds on the number of path components for the CPU trace metrics 44 | * 45 | * @return A Pair of integers, the left being the minimum number of components and the right being the maximum 46 | */ 47 | public Range getBounds() { 48 | return new Range(min, max); 49 | } 50 | 51 | private void updateBounds(String traceKey) { 52 | int numComponents = 1; 53 | int len = traceKey.length(); 54 | for (int i = 0; i < len; ++i) { 55 | if (traceKey.charAt(i) == '.') { 56 | numComponents++; 57 | } 58 | } 59 | // Account for the cpu.trace prefix 60 | max = Math.max(max, numComponents - 2); 61 | min = Math.min(min, numComponents - 2); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /visualization/influxdb-dashboard/README.md: -------------------------------------------------------------------------------- 1 | # statsd-jvm-profiler-dash 2 | A dashboard to visualize the metrics produced by 3 | [statsd-jvm-profiler](https://github.com/etsy/statsd-jvm-profiler). 4 | 5 | This dashboard only supports the InfluxDB backend for the profiler. 6 | InfluxDB 0.13 is required, older versions are not supported. 7 | 8 | ## Configuration 9 | Configuration is done with the dashboard-config.json file in the 10 | project root. An example (example-dashboard-config.json) is provided. 11 | You must provide values for all the keys in that example, which 12 | includes the information necessary to connect to InfluxDB and the base 13 | prefix for metrics. 14 | 15 | The `/config` endpoint will return the current configuration. 16 | 17 | ## Metric Structure 18 | This dashboards assumes that you have configured statsd-jvm-profiler to put these tags on the metrics it produces: 19 | `username`, `job`, `flow`, `stage`, `phase`. You can use the 20 | `tagMapping` and `prefix` arguments to do so. The 21 | [example FlowListener](https://github.com/etsy/statsd-jvm-profiler/blob/master/example/StatsDProfilerFlowListener.scala) 22 | produces metrics in this format. `` is configured in 23 | dashboard-config.json, but the available values for the others are 24 | automatically pulled from the metrics that exist in the configured 25 | InfluxDB database. 26 | 27 | ## Installation 28 | It is assumed that you already have InfluxDB configured and running 29 | before setting up this dashboard. 30 | 31 | 1. Clone repository. 32 | 2. Open a command prompt, navigate to the folder, and enter: npm install 33 | 3. Next, run the app by entering: node app 34 | 4. Browse to http://localhost:3888 35 | 36 | ## Usage 37 | The homepage allows selecting the username, job, run id, stage number, 38 | and phase (map or reduce) from the available values. Clicking the 39 | "Render" button will display visualizations of memory usage and GC 40 | statistics. A flame graph can also be generated from this page. 41 | 42 | ## Project Template 43 | Kory Becker http://www.primaryobjects.com/kory-becker.aspx 44 | -------------------------------------------------------------------------------- /src/main/java/com/etsy/statsd/profiler/server/ProfilerServer.java: -------------------------------------------------------------------------------- 1 | package com.etsy.statsd.profiler.server; 2 | 3 | import com.etsy.statsd.profiler.Profiler; 4 | import org.vertx.java.core.AsyncResult; 5 | import org.vertx.java.core.Handler; 6 | import org.vertx.java.core.Vertx; 7 | import org.vertx.java.core.VertxFactory; 8 | import org.vertx.java.core.http.HttpServer; 9 | 10 | import java.util.List; 11 | import java.util.Map; 12 | import java.util.concurrent.ScheduledFuture; 13 | import java.util.concurrent.atomic.AtomicReference; 14 | import java.util.logging.Logger; 15 | 16 | /** 17 | * Sets up a simple embedded HTTP server for interacting with the profiler while it runs 18 | * 19 | * @author Andrew Johnson 20 | */ 21 | public final class ProfilerServer { 22 | private static final Logger LOGGER = Logger.getLogger(ProfilerServer.class.getName()); 23 | private static final Vertx VERTX = VertxFactory.newVertx(); 24 | 25 | private ProfilerServer() { } 26 | 27 | /** 28 | * Start an embedded HTTP server 29 | * 30 | * @param activeProfilers The active profilers 31 | * @param port The port on which to bind the server 32 | */ 33 | public static void startServer(final Map> runningProfilers, final Map activeProfilers, final int port, final AtomicReference isRunning, final List errors) { 34 | final HttpServer server = VERTX.createHttpServer(); 35 | server.requestHandler(RequestHandler.getMatcher(runningProfilers, activeProfilers, isRunning, errors)); 36 | server.listen(port, new Handler>() { 37 | @Override 38 | public void handle(AsyncResult event) { 39 | if (event.failed()) { 40 | server.close(); 41 | startServer(runningProfilers, activeProfilers, port + 1, isRunning, errors); 42 | } else if (event.succeeded()) { 43 | LOGGER.info("Profiler server started on port " + port); 44 | } 45 | } 46 | }); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /visualization/README.md: -------------------------------------------------------------------------------- 1 | This directory contains utilities for visualizing the output of the profiler. Some example flame graphs are in the `example_flame_graphs` directory. 2 | 3 | ## influxdb-dashboard 4 | 5 | This is a simple dashboard for visualizing the metrics produced by the profiler using the InfluxDB backend. See the README in this directory for more information. 6 | 7 | ## graphite_dump.py 8 | 9 | This script will dump the output of the profiler from Graphite and format it in a manner suitable for use with [FlameGraph](https://github.com/brendangregg/FlameGraph). You must specify the Graphite host, the prefix of the metrics, and the start and end date for the period to be visualized. 10 | 11 | ### Usage 12 | graphite_dump.py takes the following options: 13 | 14 | Option | Meaning 15 | -------|-------- 16 | -o | Hostname where Graphite is running 17 | -s | Beginning of the time range to dump in the HH:MM_yyyymmdd format 18 | -e | End of the time range to dump in the HH:MM_yyyymmdd format 19 | -p | Prefix for the metrics to dump 20 | 21 | 22 | An example invocation would be 23 | ``` 24 | graphite_dump.py -o graphitehost -s 19:48_20141230 -e 19:50_20141230 -p statsd-jvm-profiler.cpu.trace 25 | ``` 26 | 27 | ## influxdb_dump.py 28 | 29 | This script will dump the output of the profiler from InfluxDB format it in a manner suitable for use with [FlameGraph](https://github.com/brendangregg/FlameGraph). 30 | 31 | It requires [influxdb-python](https://github.com/influxdb/influxdb-python) to be installed. 32 | 33 | ### Usage 34 | influxdb_dump.py takes the following options: 35 | 36 | Option | Meaning 37 | -------|-------- 38 | -o | Hostname where InfluxDB is running (required) 39 | -r | Port for the InfluxDB HTTP API (optional, defaults to 8086) 40 | -u | Username to use when connecting to InfluxDB (required) 41 | -p | Password to use when connection to InfluxDB (required) 42 | -d | Database containing the profiler metrics (required) 43 | -e | Prefix of metrics. This would be the same value as the `prefix` argument given to the profiler (required) 44 | -t | Tag mapping for metrics. This would be the same value as the `tagMapping` argument given to the profiler (optional, defaults to none). 45 | 46 | An example invocation would be: 47 | ``` 48 | influxdb_dump.py -o influxdbhost -u profiler -p password -d profiler -e bigdata.profiler.ajohnson.job1.flow1.stage1.phase1 -t SKIP.SKIP.username.job.flow.stage.phase 49 | ``` 50 | -------------------------------------------------------------------------------- /src/test/java/com/etsy/statsd/profiler/reporter/InfluxDBReporterTest.java: -------------------------------------------------------------------------------- 1 | package com.etsy.statsd.profiler.reporter; 2 | 3 | import com.etsy.statsd.profiler.Arguments; 4 | import com.etsy.statsd.profiler.reporter.mock.BaseReporterTest; 5 | import com.etsy.statsd.profiler.util.MockArguments; 6 | import com.etsy.statsd.profiler.util.TagUtil; 7 | import com.google.common.collect.ImmutableMap; 8 | import org.influxdb.InfluxDB; 9 | import org.influxdb.dto.BatchPoints; 10 | import org.influxdb.dto.Point; 11 | import org.junit.Test; 12 | import org.mockito.Matchers; 13 | import org.mockito.Mock; 14 | import org.mockito.Mockito; 15 | 16 | import static org.junit.Assert.assertEquals; 17 | import static org.junit.Assert.assertTrue; 18 | 19 | public class InfluxDBReporterTest extends BaseReporterTest { 20 | @Mock 21 | private InfluxDB client; 22 | 23 | @Override 24 | protected InfluxDBReporter constructReporter() { 25 | Arguments arguments = MockArguments.createArgs("localhost", 8888, "influxdb.reporter.test", 26 | ImmutableMap.of("username", "user", "password", "password", "database", "database")); 27 | return new InfluxDBReporter(arguments); 28 | } 29 | 30 | @Override 31 | protected void testCase(Object[] args) { 32 | assertEquals(1, args.length); 33 | 34 | BatchPoints actual = (BatchPoints) args[0]; 35 | 36 | Point expectedPoint = Point.measurement("fake") 37 | .field(InfluxDBReporter.VALUE_COLUMN, 100L) 38 | .tag(TagUtil.PREFIX_TAG, "influxdb.reporter.test") 39 | .build(); 40 | 41 | BatchPoints expected = BatchPoints.database("database").build(); 42 | expected.point(expectedPoint); 43 | 44 | assertEquals(expected.getDatabase(), actual.getDatabase()); 45 | assertEquals(expected.getPoints().size(), actual.getPoints().size()); 46 | 47 | Point actualPoint = actual.getPoints().get(0); 48 | 49 | // All the fields on Point are private 50 | assertTrue(actualPoint.lineProtocol().startsWith("fake")); 51 | assertTrue(actualPoint.lineProtocol().contains("value=100")); 52 | assertTrue(actualPoint.lineProtocol().contains("prefix=influxdb.reporter.test")); 53 | } 54 | 55 | @Test 56 | public void testRecordGaugeValue() { 57 | Mockito.doAnswer(answer).when(client).write(Matchers.any(BatchPoints.class)); 58 | reporter.recordGaugeValue("fake", 100L); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /visualization/influxdb-dashboard/public/scripts/toc.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * toc - jQuery Table of Contents Plugin 3 | * v0.3.2 4 | * http://projects.jga.me/toc/ 5 | * copyright Greg Allen 2014 6 | * MIT License 7 | */ 8 | !function(a){a.fn.smoothScroller=function(b){b=a.extend({},a.fn.smoothScroller.defaults,b);var c=a(this);return a(b.scrollEl).animate({scrollTop:c.offset().top-a(b.scrollEl).offset().top-b.offset},b.speed,b.ease,function(){var a=c.attr("id");a.length&&(history.pushState?history.pushState(null,null,"#"+a):document.location.hash=a),c.trigger("smoothScrollerComplete")}),this},a.fn.smoothScroller.defaults={speed:400,ease:"swing",scrollEl:"body,html",offset:0},a("body").on("click","[data-smoothscroller]",function(b){b.preventDefault();var c=a(this).attr("href");0===c.indexOf("#")&&a(c).smoothScroller()})}(jQuery),function(a){var b={};a.fn.toc=function(b){var c,d=this,e=a.extend({},jQuery.fn.toc.defaults,b),f=a(e.container),g=a(e.selectors,f),h=[],i=e.activeClass,j=function(b,c){if(e.smoothScrolling&&"function"==typeof e.smoothScrolling){b.preventDefault();var f=a(b.target).attr("href");e.smoothScrolling(f,e,c)}a("li",d).removeClass(i),a(b.target).parent().addClass(i)},k=function(){c&&clearTimeout(c),c=setTimeout(function(){for(var b,c=a(window).scrollTop(),f=Number.MAX_VALUE,g=0,j=0,k=h.length;k>j;j++){var l=Math.abs(h[j]-c);f>l&&(g=j,f=l)}a("li",d).removeClass(i),b=a("li:eq("+g+")",d).addClass(i),e.onHighlight(b)},50)};return e.highlightOnScroll&&(a(window).bind("scroll",k),k()),this.each(function(){var b=a(this),c=a(e.listType);g.each(function(d,f){var g=a(f);h.push(g.offset().top-e.highlightOffset);var i=e.anchorName(d,f,e.prefix);if(f.id!==i){a("").attr("id",i).insertBefore(g)}var l=a("").text(e.headerText(d,f,g)).attr("href","#"+i).bind("click",function(c){a(window).unbind("scroll",k),j(c,function(){a(window).bind("scroll",k)}),b.trigger("selected",a(this).attr("href"))}),m=a("
  • ").addClass(e.itemClass(d,f,g,e.prefix)).append(l);c.append(m)}),b.html(c)})},jQuery.fn.toc.defaults={container:"body",listType:"
      ",selectors:"h1,h2,h3",smoothScrolling:function(b,c,d){a(b).smoothScroller({offset:c.scrollToOffset}).on("smoothScrollerComplete",function(){d()})},scrollToOffset:0,prefix:"toc",activeClass:"toc-active",onHighlight:function(){},highlightOnScroll:!0,highlightOffset:100,anchorName:function(c,d,e){if(d.id.length)return d.id;var f=a(d).text().replace(/[^a-z0-9]/gi," ").replace(/\s+/g,"-").toLowerCase();if(b[f]){for(var g=2;b[f+g];)g++;f=f+"-"+g}return b[f]=!0,e+"-"+f},headerText:function(a,b,c){return c.text()},itemClass:function(a,b,c,d){return d+"-"+c[0].tagName.toLowerCase()}}}(jQuery); -------------------------------------------------------------------------------- /src/test/java/com/etsy/statsd/profiler/util/TagUtilTest.java: -------------------------------------------------------------------------------- 1 | package com.etsy.statsd.profiler.util; 2 | 3 | import com.google.common.collect.Maps; 4 | import org.junit.Rule; 5 | import org.junit.Test; 6 | import org.junit.rules.ExpectedException; 7 | 8 | import java.util.HashMap; 9 | import java.util.HashSet; 10 | import java.util.Map; 11 | import java.util.Set; 12 | 13 | import static org.junit.Assert.assertEquals; 14 | 15 | public class TagUtilTest { 16 | @Rule 17 | public ExpectedException expectedException = ExpectedException.none(); 18 | 19 | @Test 20 | public void testNoTagMapping() { 21 | String prefix = "i.am.a.prefix"; 22 | 23 | Map expected = Maps.newHashMap(); 24 | expected.put(TagUtil.PREFIX_TAG, prefix); 25 | 26 | assertEquals(expected, TagUtil.getTags(null, prefix, false)); 27 | } 28 | 29 | @Test 30 | public void testInvalidMapping() { 31 | String prefix = "one.two.three"; 32 | String tagMapping = "tagOne.tagTwo"; 33 | 34 | expectedException.expect(RuntimeException.class); 35 | TagUtil.getTags(tagMapping, prefix, false); 36 | } 37 | 38 | @Test 39 | public void testMapping() { 40 | String prefix = "one.two.three"; 41 | String tagMapping = "tagOne.tagTwo.tagThree"; 42 | 43 | Map expected = Maps.newHashMap(); 44 | expected.put("tagOne", "one"); 45 | expected.put("tagTwo", "two"); 46 | expected.put("tagThree", "three"); 47 | 48 | assertEquals(expected, TagUtil.getTags(tagMapping, prefix, false)); 49 | } 50 | 51 | @Test 52 | public void testGetGlobalTags() { 53 | Map globalTags = new HashMap<>(); 54 | TagUtil.getGlobalTags(globalTags); 55 | Set expectedKeys = new HashSet<>(); 56 | expectedKeys.add(TagUtil.PID_TAG); 57 | expectedKeys.add(TagUtil.HOSTNAME_TAG); 58 | expectedKeys.add(TagUtil.JVM_NAME_TAG); 59 | 60 | assertEquals(expectedKeys, globalTags.keySet()); 61 | } 62 | 63 | @Test 64 | public void testIncludeGlobalTags() { 65 | String prefix = "one.two.three"; 66 | String tagMapping = "tagOne.tagTwo.tagThree"; 67 | 68 | Map expected = Maps.newHashMap(); 69 | expected.put("tagOne", "one"); 70 | expected.put("tagTwo", "two"); 71 | expected.put("tagThree", "three"); 72 | Map globalTags = new HashMap<>(); 73 | TagUtil.getGlobalTags(globalTags); 74 | expected.putAll(globalTags); 75 | 76 | assertEquals(expected, TagUtil.getTags(tagMapping, prefix, true)); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/main/java/com/etsy/statsd/profiler/reporter/StatsDReporter.java: -------------------------------------------------------------------------------- 1 | package com.etsy.statsd.profiler.reporter; 2 | 3 | import com.etsy.statsd.profiler.Arguments; 4 | import com.timgroup.statsd.NonBlockingStatsDClient; 5 | import com.timgroup.statsd.StatsDClient; 6 | 7 | import java.util.Map; 8 | 9 | /** 10 | * Reporter that sends data to StatsD 11 | * 12 | * @author Andrew Johnson 13 | */ 14 | public class StatsDReporter extends Reporter { 15 | public StatsDReporter(Arguments arguments) { 16 | super(arguments); 17 | } 18 | 19 | /** 20 | * Record a gauge value in StatsD 21 | * 22 | * @param key The key for the gauge 23 | * @param value The value of the gauge 24 | */ 25 | @Override 26 | public void recordGaugeValue(String key, long value, String... tags) { 27 | client.recordGaugeValue(key, value, tags); 28 | } 29 | 30 | /** 31 | * @see #recordGaugeValue(String, long) 32 | */ 33 | @Override 34 | public void recordGaugeValue(String key, double value, String... tags) { 35 | client.recordGaugeValue(key, value, tags); 36 | } 37 | 38 | /** 39 | * Record multiple gauge values in StatsD 40 | * This simply loops over calling recordGaugeValue 41 | * 42 | * @param gauges A map of gauge names to values 43 | */ 44 | @Override 45 | public void recordGaugeValues(Map gauges, String... tags) { 46 | for (Map.Entry gauge : gauges.entrySet()) { 47 | if (gauge.getValue() instanceof Long) { 48 | client.recordGaugeValue(gauge.getKey(), gauge.getValue().longValue(), tags); 49 | } else if (gauge.getValue() instanceof Double) { 50 | client.recordGaugeValue(gauge.getKey(), gauge.getValue().doubleValue(), tags); 51 | } else { 52 | throw new IllegalArgumentException("Unexpected Number type: " + gauge.getValue().getClass().getSimpleName()); 53 | } 54 | } 55 | } 56 | 57 | /** 58 | * Construct a StatsD client 59 | * 60 | * @param server The hostname of the StatsD server 61 | * @param port The port on which StatsD is running 62 | * @param prefix The prefix for all metrics sent 63 | * @return A StatsD client 64 | */ 65 | @Override 66 | protected StatsDClient createClient(String server, int port, String prefix) { 67 | return new NonBlockingStatsDClient(prefix, server, port); 68 | } 69 | 70 | /** 71 | * Handle additional arguments 72 | * 73 | * @param arguments The arguments given to the profiler agent 74 | */ 75 | @Override 76 | protected void handleArguments(Arguments arguments) { } 77 | } 78 | -------------------------------------------------------------------------------- /src/main/java/com/etsy/statsd/profiler/reporter/Reporter.java: -------------------------------------------------------------------------------- 1 | package com.etsy.statsd.profiler.reporter; 2 | 3 | import com.etsy.statsd.profiler.Arguments; 4 | import com.google.common.base.Preconditions; 5 | 6 | import java.util.Map; 7 | 8 | /** 9 | * Interface for reporters 10 | * 11 | * @author Andrew Johnson 12 | */ 13 | public abstract class Reporter { 14 | public static final Class[] CONSTRUCTOR_PARAM_TYPES = new Class[]{Arguments.class}; 15 | 16 | /** 17 | * The underlying implementation for this reporter 18 | */ 19 | protected T client; 20 | 21 | public Reporter(Arguments arguments) { 22 | Preconditions.checkNotNull(arguments); 23 | handleArguments(arguments); 24 | client = createClient(arguments.server, arguments.port, arguments.metricsPrefix); 25 | } 26 | 27 | /** 28 | * Record a gauge value 29 | * 30 | * @param key The name of the gauge 31 | * @param value The value of the gauge 32 | */ 33 | public abstract void recordGaugeValue(String key, long value, String... tags); 34 | 35 | /** 36 | * @see #recordGaugeValue(String, long) 37 | */ 38 | public abstract void recordGaugeValue(String key, double value, String... tags); 39 | 40 | /** 41 | * Record multiple gauge values 42 | * This is useful for reporters that can send points in batch 43 | * 44 | * @param gauges A map of gauge names to values 45 | */ 46 | public abstract void recordGaugeValues(Map gauges, String... tags); 47 | 48 | /** 49 | * CPUTracingProfiler can emit some metrics that indicate the upper and lower bound on the length of stack traces 50 | * This is helpful for querying this data for some backends (such as Graphite) that do not have rich query languages 51 | * Reporters can override this to disable these metrics 52 | * 53 | * @return true if the bounds metrics should be emitted, false otherwise 54 | */ 55 | public boolean emitBounds() { 56 | return true; 57 | } 58 | 59 | /** 60 | * Construct the underlying client implementation for this reporter 61 | * 62 | * @param server The server to which to report data 63 | * @param port The port on which the server is running 64 | * @param prefix The prefix for metrics 65 | * @return An instance of T, the client implementation 66 | */ 67 | protected abstract T createClient(String server, int port, String prefix); 68 | 69 | /** 70 | * Handle any additional arguments necessary for this reporter 71 | * 72 | * @param arguments The arguments given to the profiler agent 73 | */ 74 | protected abstract void handleArguments(Arguments arguments); 75 | } 76 | -------------------------------------------------------------------------------- /visualization/influxdb-dashboard/public/scripts/jquery.sticky-kit.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | Sticky-kit v1.1.1 | WTFPL | Leaf Corcoran 2014 | http://leafo.net 3 | */ 4 | (function(){var k,e;k=this.jQuery||window.jQuery;e=k(window);k.fn.stick_in_parent=function(d){var v,y,n,p,h,C,s,G,q,H;null==d&&(d={});s=d.sticky_class;y=d.inner_scrolling;C=d.recalc_every;h=d.parent;p=d.offset_top;n=d.spacer;v=d.bottoming;null==p&&(p=0);null==h&&(h=void 0);null==y&&(y=!0);null==s&&(s="is_stuck");null==v&&(v=!0);G=function(a,d,q,z,D,t,r,E){var u,F,m,A,c,f,B,w,x,g,b;if(!a.data("sticky_kit")){a.data("sticky_kit",!0);f=a.parent();null!=h&&(f=f.closest(h));if(!f.length)throw"failed to find stick parent"; 5 | u=m=!1;(g=null!=n?n&&a.closest(n):k("
      "))&&g.css("position",a.css("position"));B=function(){var c,e,l;if(!E&&(c=parseInt(f.css("border-top-width"),10),e=parseInt(f.css("padding-top"),10),d=parseInt(f.css("padding-bottom"),10),q=f.offset().top+c+e,z=f.height(),m&&(u=m=!1,null==n&&(a.insertAfter(g),g.detach()),a.css({position:"",top:"",width:"",bottom:""}).removeClass(s),l=!0),D=a.offset().top-(parseInt(a.css("margin-top"),10)||0)-p,t=a.outerHeight(!0),r=a.css("float"),g&&g.css({width:a.outerWidth(!0), 6 | height:t,display:a.css("display"),"vertical-align":a.css("vertical-align"),"float":r}),l))return b()};B();if(t!==z)return A=void 0,c=p,x=C,b=function(){var b,k,l,h;if(!E&&(null!=x&&(--x,0>=x&&(x=C,B())),l=e.scrollTop(),null!=A&&(k=l-A),A=l,m?(v&&(h=l+t+c>z+q,u&&!h&&(u=!1,a.css({position:"fixed",bottom:"",top:c}).trigger("sticky_kit:unbottom"))),lb&&!u&&(c-=k,c=Math.max(b-t,c),c=Math.min(p,c),m&&a.css({top:c+"px"})))):l>D&&(m=!0,b={position:"fixed",top:c},b.width="border-box"===a.css("box-sizing")?a.outerWidth()+"px":a.width()+"px",a.css(b).addClass(s),null==n&&(a.after(g),"left"!==r&&"right"!==r||g.append(a)),a.trigger("sticky_kit:stick")),m&&v&&(null==h&&(h=l+t+c>z+q),!u&&h)))return u=!0,"static"===f.css("position")&&f.css({position:"relative"}),a.css({position:"absolute",bottom:d,top:"auto"}).trigger("sticky_kit:bottom")}, 8 | w=function(){B();return b()},F=function(){E=!0;e.off("touchmove",b);e.off("scroll",b);e.off("resize",w);k(document.body).off("sticky_kit:recalc",w);a.off("sticky_kit:detach",F);a.removeData("sticky_kit");a.css({position:"",bottom:"",top:"",width:""});f.position("position","");if(m)return null==n&&("left"!==r&&"right"!==r||a.insertAfter(g),g.remove()),a.removeClass(s)},e.on("touchmove",b),e.on("scroll",b),e.on("resize",w),k(document.body).on("sticky_kit:recalc",w),a.on("sticky_kit:detach",F),setTimeout(b, 9 | 0)}};q=0;for(H=this.length;q getGlobalTags(Map tags) { 26 | // Add the jvm name, pid, hostname as tags to help identify different processes 27 | final String jvmName = ManagementFactory.getRuntimeMXBean().getName(); 28 | tags.put(JVM_NAME_TAG, jvmName); 29 | int atIndex = jvmName.indexOf("@"); 30 | if (atIndex > 0) { 31 | tags.put(PID_TAG, jvmName.substring(0, atIndex)); 32 | tags.put(HOSTNAME_TAG, jvmName.substring(atIndex + 1)); 33 | } else { 34 | tags.put(PID_TAG, UNKNOWN); 35 | tags.put(HOSTNAME_TAG, UNKNOWN); 36 | } 37 | 38 | return tags; 39 | } 40 | 41 | /** 42 | * Gets all the tag values from the prefix and the tag mapping 43 | * 44 | * @param tagMapping The mapping of tag names from the metric prefix 45 | * @param prefix The metric prefix 46 | * @param includeGlobalTags Whether or not to include the global tags 47 | * @return A map of tag name to value 48 | */ 49 | public static Map getTags(String tagMapping, String prefix, boolean includeGlobalTags) { 50 | Map mapping = Maps.newHashMap(); 51 | if (tagMapping != null) { 52 | String[] tagNames = tagMapping.split("\\."); 53 | String[] prefixComponents = prefix.split("\\."); 54 | if (tagNames.length != prefixComponents.length) { 55 | throw new RuntimeException(String.format("Invalid tag mapping: %s", tagMapping)); 56 | } 57 | 58 | for (int i = 0; i < tagNames.length; i++) { 59 | String tag = tagNames[i]; 60 | String value = prefixComponents[i]; 61 | if (!tag.equals(SKIP_TAG)) { 62 | mapping.put(tag, value); 63 | } 64 | } 65 | } else { 66 | mapping.put(PREFIX_TAG, prefix); 67 | } 68 | 69 | if (includeGlobalTags) { 70 | mapping.putAll(getGlobalTags(mapping)); 71 | } 72 | 73 | return mapping; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/test/java/com/etsy/statsd/profiler/util/StackTraceFilterTest.java: -------------------------------------------------------------------------------- 1 | package com.etsy.statsd.profiler.util; 2 | 3 | import com.etsy.statsd.profiler.profilers.CPUTracingProfiler; 4 | import org.junit.BeforeClass; 5 | import org.junit.Test; 6 | 7 | import java.util.ArrayList; 8 | import java.util.Arrays; 9 | import java.util.Collections; 10 | import java.util.List; 11 | import java.util.regex.Pattern; 12 | 13 | import static org.junit.Assert.*; 14 | 15 | public class StackTraceFilterTest { 16 | private static StackTraceFilter filter; 17 | private static List includePackages; 18 | private static List excludedTraces; 19 | private static List includedTraces; 20 | private static List otherTraces; 21 | 22 | @BeforeClass 23 | public static void setup() { 24 | includePackages = Arrays.asList("com.etsy", "com.twitter.scalding"); 25 | filter = new StackTraceFilter(includePackages, CPUTracingProfiler.EXCLUDE_PACKAGES); 26 | excludedTraces = Arrays.asList("com-etsy-statsd-profiler-profiler-util-StackTraceFormatter-formatStackTraceElement", "com-timgroup-statsd-StatsDClient-send", "com-etsy-statsd-profiler-profiler-util-StackTraceFormatter-formatStackTraceElement.com-etsy-Foo-fooTest"); 27 | includedTraces = Collections.singletonList("com-etsy-foo-fooTest"); 28 | otherTraces = Collections.singletonList("com-google-guava-Foo-helloWorld"); 29 | } 30 | 31 | @Test 32 | public void testGetPackagePattern() { 33 | Pattern expected = Pattern.compile("(.*\\.|^)((com-etsy)|(com-twitter-scalding)).*"); 34 | assertEquals(expected.toString(), filter.getPackagePattern(includePackages, StackTraceFilter.MATCH_EVERYTHING).toString()); 35 | 36 | assertEquals(StackTraceFilter.MATCH_EVERYTHING.toString(), filter.getPackagePattern(null, StackTraceFilter.MATCH_EVERYTHING).toString()); 37 | assertEquals(StackTraceFilter.MATCH_EVERYTHING.toString(), filter.getPackagePattern(new ArrayList(), StackTraceFilter.MATCH_EVERYTHING).toString()); 38 | } 39 | 40 | @Test 41 | public void testExcludeMatches() { 42 | for (String e : excludedTraces) { 43 | assertTrue(e, filter.excludeMatches(e)); 44 | } 45 | 46 | for (String i : includedTraces) { 47 | assertFalse(i, filter.excludeMatches(i)); 48 | } 49 | 50 | for (String o : otherTraces) { 51 | assertFalse(o, filter.excludeMatches(o)); 52 | } 53 | } 54 | 55 | @Test 56 | public void testIncludeMatches() { 57 | for (String i : includedTraces) { 58 | assertTrue(i, filter.includeMatches(i)); 59 | } 60 | 61 | for (String o : otherTraces) { 62 | assertFalse(o, filter.includeMatches(o)); 63 | } 64 | } 65 | 66 | @Test 67 | public void testIncludeStackTrace() { 68 | for (String e : excludedTraces) { 69 | assertFalse(e, filter.includeStackTrace(e)); 70 | } 71 | 72 | for (String i : includedTraces) { 73 | assertTrue(i, filter.includeStackTrace(i)); 74 | } 75 | 76 | for (String o : otherTraces) { 77 | assertFalse(o, filter.includeStackTrace(o)); 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/main/java/com/etsy/statsd/profiler/util/ThreadDumper.java: -------------------------------------------------------------------------------- 1 | package com.etsy.statsd.profiler.util; 2 | 3 | import com.google.common.base.Predicate; 4 | import com.google.common.collect.Collections2; 5 | 6 | import java.lang.management.ManagementFactory; 7 | import java.lang.management.ThreadInfo; 8 | import java.lang.management.ThreadMXBean; 9 | import java.util.Arrays; 10 | import java.util.Collection; 11 | 12 | /** 13 | * Dumps current thread state 14 | * 15 | * @author Andrew Johnson 16 | */ 17 | public final class ThreadDumper { 18 | private static ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean(); 19 | 20 | private ThreadDumper() { } 21 | 22 | /** 23 | * Predicate to filter by thread state 24 | */ 25 | private static class ThreadStatePredicate implements Predicate { 26 | private final Thread.State state; 27 | 28 | ThreadStatePredicate(Thread.State state) { 29 | this.state = state; 30 | } 31 | 32 | @Override 33 | public boolean apply(ThreadInfo input) { 34 | return input.getThreadState() == state; 35 | } 36 | } 37 | 38 | /** 39 | * Dump state of all threads 40 | * 41 | * @param lockedMonitors If true, dump all locked monitors 42 | * @param lockedSynchronizers If true, dump all locked ownable synchronizers 43 | * @return A Collection of {@link ThreadInfo} for all live threads 44 | */ 45 | public static Collection getAllThreads(boolean lockedMonitors, boolean lockedSynchronizers) { 46 | return Arrays.asList(threadMXBean.dumpAllThreads(lockedMonitors, lockedSynchronizers)); 47 | } 48 | 49 | /** 50 | * Dump state of all threads with a given state 51 | * 52 | * @param lockedMonitors If true, dump all locked monitors 53 | * @param lockedSynchronizers If true, dump all locked ownable synchronizers 54 | * @param state The state in which a thread must be to be dumped 55 | * @return A Collection of {@link ThreadInfo} for all live threads in the given state 56 | */ 57 | public static Collection getAllThreadsInState(boolean lockedMonitors, boolean lockedSynchronizers, Thread.State state) { 58 | return Collections2.filter(getAllThreads(lockedMonitors, lockedSynchronizers), new ThreadStatePredicate(state)); 59 | } 60 | 61 | /** 62 | * Dump state of all threads with a given state that match a predicate 63 | * 64 | * @param lockedMonitors If true, dump all locked monitors 65 | * @param lockedSynchronizers If true, dump all locked ownable synchronizers 66 | * @param state The state in which a thread must be to be dumped 67 | * @param threadInfoPredicate Predicate to further filter the dumped threads 68 | * @return A Collection of {@link ThreadInfo} for all live threads in the given state that match the given predicate 69 | */ 70 | public static Collection filterAllThreadsInState(boolean lockedMonitors, boolean lockedSynchronizers, Thread.State state, Predicate threadInfoPredicate) { 71 | return Collections2.filter(getAllThreadsInState(lockedMonitors, lockedSynchronizers, state), threadInfoPredicate); 72 | } 73 | } 74 | 75 | -------------------------------------------------------------------------------- /visualization/graphite_dump.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from optparse import OptionParser 3 | import requests 4 | 5 | def get_arg_parser(): 6 | parser = OptionParser() 7 | parser.add_option('-o', '--host', dest='host', help='Hostname of graphite server', metavar='HOST') 8 | parser.add_option('-r', '--port', dest='port', help='Port for graphite server', metavar='PORT') 9 | parser.add_option('-p', '--prefix', dest='prefix', help='Prefix of metric for which to dump the tree', metavar='PREFIX') 10 | parser.add_option('-s', '--start', dest='start', help='Start date for query', metavar='DATE') 11 | parser.add_option('-e', '--end', dest='end', help='End date for query', metavar='DATE') 12 | 13 | return parser 14 | 15 | 16 | def get_children(host, prefix, min, max): 17 | url = 'http://%s/metrics/expand' % host 18 | leaves = [] 19 | for i in range(min, max + 1): 20 | params = {'query': '%s%s' % (prefix, '.*' * i), 'leavesOnly': '1'} 21 | json_url = requests.get(url, params=params) 22 | json_results = json_url.json() 23 | leaves.extend(json_results['results']) 24 | 25 | return leaves 26 | 27 | 28 | def get_bounds(host, prefix): 29 | params = {'query': '%s.*' % prefix, 'leavesOnly': '1'} 30 | url = 'http://%s/metrics/expand' % host 31 | json_url = requests.get(url, params=params) 32 | json_results = json_url.json() 33 | bounds = [] 34 | for bound in json_results['results']: 35 | boundresult = bound.replace(prefix + '.', '') 36 | if boundresult.isdigit(): 37 | bounds.append(int(boundresult)) 38 | 39 | return (min(bounds), max(bounds)) 40 | 41 | 42 | def get_max_metric(host, metric, start, end): 43 | params = {'target': 'keepLastValue(%s)' % metric, 'format': 'json', 'from': start, 'until': end} 44 | url = 'http://%s/render' % host 45 | json_url = requests.get(url, params=params) 46 | json_results = json_url.json() 47 | if not json_results: 48 | return None 49 | else: 50 | return max([point[0] for point in json_results[0]['datapoints']]) 51 | 52 | 53 | def get_tree(host, prefix, start, end): 54 | nodes_to_process = [prefix] 55 | (min, max) = get_bounds(host, prefix) 56 | leaves = get_children(host, prefix, min, max) 57 | 58 | results = {} 59 | for leaf in leaves: 60 | leafmetric = get_max_metric(host, leaf, start, end) 61 | if leafmetric is not None: 62 | results[leaf] = leafmetric 63 | 64 | return results 65 | 66 | 67 | def format_metric(metric, prefix): 68 | no_prefix = metric.replace(prefix + '.', '') 69 | frames = no_prefix.split('.') 70 | frames.reverse() 71 | return ';'.join(frames).replace('-', '.') 72 | 73 | 74 | def format_output(prefix, results): 75 | for metric, value in results.iteritems(): 76 | if value is not None: 77 | print '%s %d' % (format_metric(metric, prefix), value) 78 | 79 | 80 | if __name__ == '__main__': 81 | parser = get_arg_parser() 82 | args, _ = parser.parse_args() 83 | if not(args.host and args.prefix and args.start and args.end): 84 | parser.print_help() 85 | sys.exit(255) 86 | 87 | host = args.host 88 | if args.port is not None: 89 | host = '%s:%s' % (host, args.port) 90 | 91 | results = get_tree(host, args.prefix, args.start, args.end) 92 | format_output(args.prefix, results) 93 | -------------------------------------------------------------------------------- /src/main/java/com/etsy/statsd/profiler/profilers/CPULoadProfiler.java: -------------------------------------------------------------------------------- 1 | package com.etsy.statsd.profiler.profilers; 2 | 3 | import com.google.common.collect.ImmutableMap; 4 | 5 | import com.etsy.statsd.profiler.Arguments; 6 | import com.etsy.statsd.profiler.Profiler; 7 | import com.etsy.statsd.profiler.reporter.Reporter; 8 | 9 | import java.lang.management.ManagementFactory; 10 | import java.util.Map; 11 | import java.util.concurrent.TimeUnit; 12 | import javax.management.Attribute; 13 | import javax.management.AttributeList; 14 | import javax.management.InstanceNotFoundException; 15 | import javax.management.MBeanServer; 16 | import javax.management.MalformedObjectNameException; 17 | import javax.management.ObjectName; 18 | import javax.management.ReflectionException; 19 | 20 | /** 21 | * This profiler retrieves CPU values for the JVM and System from the "OperatingSystem" JMX Bean. 22 | *

      23 | * This profiler relies on a JMX bean that might not be available in all JVM implementations. 24 | * We know for sure it's available in Sun/Oracle's JRE 7+, but there are no guarantees it 25 | * will remain there for the foreseeable future. 26 | * 27 | * @see StackOverflow post 28 | * 29 | * @author Alejandro Rivera 30 | */ 31 | public class CPULoadProfiler extends Profiler { 32 | 33 | public static final long PERIOD = 10; 34 | private static final Map ATTRIBUTES_MAP = ImmutableMap.of("ProcessCpuLoad", "cpu.jvm", 35 | "SystemCpuLoad", "cpu.system"); 36 | 37 | private AttributeList list; 38 | 39 | public CPULoadProfiler(Reporter reporter, Arguments arguments) { 40 | super(reporter, arguments); 41 | try { 42 | MBeanServer mbs = ManagementFactory.getPlatformMBeanServer(); 43 | ObjectName os = ObjectName.getInstance("java.lang:type=OperatingSystem"); 44 | list = mbs.getAttributes(os, ATTRIBUTES_MAP.keySet().toArray(new String[ATTRIBUTES_MAP.size()])); 45 | } catch (InstanceNotFoundException | ReflectionException | MalformedObjectNameException e) { 46 | list = null; 47 | } 48 | 49 | } 50 | 51 | /** 52 | * Profile memory usage and GC statistics 53 | */ 54 | @Override 55 | public void profile() { 56 | recordStats(); 57 | } 58 | 59 | @Override 60 | public void flushData() { 61 | recordStats(); 62 | } 63 | 64 | @Override 65 | public long getPeriod() { 66 | return PERIOD; 67 | } 68 | 69 | @Override 70 | public TimeUnit getTimeUnit() { 71 | return TimeUnit.SECONDS; 72 | } 73 | 74 | @Override 75 | protected void handleArguments(Arguments arguments) { /* No arguments needed */ } 76 | 77 | /** 78 | * Records all memory statistics 79 | */ 80 | private void recordStats() { 81 | if (list == null) { 82 | return; 83 | } 84 | 85 | Attribute att; 86 | Double value; 87 | String metric; 88 | for (Object o : list) { 89 | att = (Attribute) o; 90 | value = (Double) att.getValue(); 91 | 92 | if (value == null || value == -1.0) { 93 | continue; 94 | } 95 | 96 | metric = ATTRIBUTES_MAP.get(att.getName()); 97 | if (metric == null) { 98 | continue; 99 | } 100 | 101 | value = ((int) (value * 1000)) / 10.0d; // 0-100 with 1-decimal precision 102 | recordGaugeValue(metric, value); 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /visualization/influxdb_dump.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from optparse import OptionParser 3 | from influxdb import InfluxDBClient 4 | import sys 5 | 6 | class InfluxDBDump: 7 | def __init__(self, host, port, username, password, database, prefix, tag_mapping): 8 | self.host = host 9 | self.port = port 10 | self.username = username 11 | self.password = password 12 | self.database = database 13 | self.prefix = prefix 14 | self.tag_mapping = tag_mapping 15 | self.client = InfluxDBClient(self.host, self.port, self.username, self.password, self.database) 16 | self.mapped_tags = self._construct_tag_mapping(prefix, tag_mapping) 17 | 18 | def run(self): 19 | clauses = ["%s ='%s'" % (tag, value) for (tag, value) in self.mapped_tags.iteritems()] 20 | query = 'select value from /^cpu.trace.*/ where %s' % " and ".join(clauses) 21 | metrics = self.client.query(query).raw['series'] 22 | for metric in metrics: 23 | name = self._format_metric_name(metric['name'], 'cpu.trace.') 24 | value = sum([v[1] for v in metric['values']]) 25 | if name != str(value): 26 | print '%s %d' % (name, value) 27 | 28 | def _format_metric_name(self, name, prefix): 29 | tokens = name.replace(prefix, '').split('.') 30 | reverse = reversed(tokens) 31 | line_numbers = [':'.join(r.rsplit('-', 1)) for r in reverse] 32 | return ';'.join(line_numbers).replace('-', '.') 33 | 34 | def _construct_tag_mapping(self, prefix, tag_mapping): 35 | mapped_tags = {} 36 | if tag_mapping: 37 | tag_names = tag_mapping.split('.') 38 | prefix_components = prefix.split('.') 39 | if len(tag_names) != len(prefix_components): 40 | raise Exception('Invalid tag mapping %s' % tag_mapping) 41 | 42 | zipped = zip(tag_names, prefix_components) 43 | for entry in zipped: 44 | if entry[0] != 'SKIP': 45 | mapped_tags[entry[0]] = entry[1] 46 | else: 47 | mapped_tags['prefix'] = prefix 48 | 49 | return mapped_tags 50 | 51 | 52 | def get_arg_parser(): 53 | parser = OptionParser() 54 | parser.add_option('-o', '--host', dest='host', help='Hostname of InfluxDB server', metavar='HOST') 55 | parser.add_option('-r', '--port', dest='port', help='Port for InfluxDB HTTP API (defaults to 8086)', metavar='PORT') 56 | parser.add_option('-u', '--username', dest='username', help='Username with which to connect to InfluxDB', metavar='USER') 57 | parser.add_option('-p', '--password', dest='password', help='Password with which to connect to InfluxDB', metavar='PASSWORD') 58 | parser.add_option('-d', '--database', dest='database', help='InfluxDB database which contains profiler data', metavar='DB') 59 | parser.add_option('-e', '--prefix', dest='prefix', help='Metric prefix', metavar='PREFIX') 60 | parser.add_option('-t', '--tag-mapping', dest='mapping', help='Tag mapping for metric prefix', metavar='MAPPING') 61 | 62 | return parser 63 | 64 | if __name__ == '__main__': 65 | parser = get_arg_parser() 66 | args, _ = parser.parse_args() 67 | if not(args.host and args.username and args.password and args.database and args.prefix): 68 | parser.print_help() 69 | sys.exit(255) 70 | port = args.port or 8086 71 | tag_mapping = args.mapping or None 72 | dumper = InfluxDBDump(args.host, port, args.username, args.password, args.database, args.prefix, tag_mapping) 73 | dumper.run() 74 | 75 | -------------------------------------------------------------------------------- /src/main/java/com/etsy/statsd/profiler/Profiler.java: -------------------------------------------------------------------------------- 1 | package com.etsy.statsd.profiler; 2 | 3 | import com.etsy.statsd.profiler.reporter.Reporter; 4 | import com.google.common.base.Preconditions; 5 | 6 | import java.util.Map; 7 | import java.util.concurrent.TimeUnit; 8 | 9 | /** 10 | * Interface for profilers 11 | * 12 | * @author Andrew Johnson 13 | */ 14 | public abstract class Profiler { 15 | public static final Class[] CONSTRUCTOR_PARAM_TYPES = new Class[]{Reporter.class, Arguments.class}; 16 | 17 | private final Reporter reporter; 18 | // CONTAINER_ID=container_1486158130664_0002_01_000157 19 | private String[] tags = {}; 20 | 21 | private long recordedStats = 0; 22 | public Profiler(Reporter reporter, Arguments arguments) { 23 | Preconditions.checkNotNull(reporter); 24 | this.reporter = reporter; 25 | handleArguments(arguments); 26 | 27 | String containerId = System.getenv("CONTAINER_ID"); 28 | if (containerId != null) { 29 | String applicationId = getApplicationId(containerId); 30 | tags = new String[]{"container_id:" + containerId, "application_id:" + applicationId}; 31 | } 32 | } 33 | 34 | public static String getApplicationId(String containerId) { 35 | String[] parts = containerId.replace("container_", "").split("_"); 36 | return "application_" + parts[0] + "_" + parts[1]; 37 | } 38 | 39 | /** 40 | * Perform profiling 41 | */ 42 | public abstract void profile(); 43 | 44 | /** 45 | * Hook to flush any remaining data cached by the profiler at JVM shutdown 46 | */ 47 | public abstract void flushData(); 48 | 49 | /** 50 | * Get the period to use for this profiler in the ScheduledExecutorService 51 | * 52 | * @return The ScheduledExecutorThread period for this profiler 53 | */ 54 | public abstract long getPeriod(); 55 | 56 | /** 57 | * Get the unit of time that corresponds to the period for this profiler 58 | * 59 | * @return A TimeUnit corresponding the the period for this profiler 60 | */ 61 | public abstract TimeUnit getTimeUnit(); 62 | 63 | /** 64 | * CPUTracingProfiler can emit some metrics that indicate the upper and lower bound on the length of stack traces 65 | * This is helpful for querying this data for some backends (such as Graphite) that do not have rich query languages 66 | * Reporters can override this to disable these metrics 67 | * 68 | * @return true if the bounds metrics should be emitted, false otherwise 69 | */ 70 | protected boolean emitBounds() { 71 | return reporter.emitBounds(); 72 | } 73 | 74 | /** 75 | * Handle any additional arguments necessary for this profiler 76 | * 77 | * @param arguments The arguments given to the profiler 78 | */ 79 | protected abstract void handleArguments(Arguments arguments); 80 | 81 | /** 82 | * Record a gauge value 83 | * 84 | * @param key The key for the gauge 85 | * @param value The value of the gauge 86 | */ 87 | protected void recordGaugeValue(String key, long value) { 88 | recordedStats++; 89 | reporter.recordGaugeValue(key, value); 90 | } 91 | 92 | /** 93 | * @see #recordGaugeValue(String, long) 94 | */ 95 | protected void recordGaugeValue(String key, double value) { 96 | recordedStats++; 97 | reporter.recordGaugeValue(key, value); 98 | } 99 | 100 | /** 101 | * Record multiple gauge values 102 | * This is useful for reporters that can send points in batch 103 | * 104 | * @param gauges A map of gauge names to values 105 | */ 106 | protected void recordGaugeValues(Map gauges) { 107 | recordedStats++; 108 | reporter.recordGaugeValues(gauges, tags); 109 | } 110 | 111 | public long getRecordedStats() { return recordedStats; } 112 | } 113 | -------------------------------------------------------------------------------- /example/StatsDProfilerFlowListener.scala: -------------------------------------------------------------------------------- 1 | package com.etsy.cascading.flow 2 | 3 | import java.util.Properties 4 | 5 | import cascading.flow.{Flow, FlowListener, FlowStep} 6 | import org.apache.hadoop.mapred.JobConf 7 | 8 | import scala.collection.JavaConversions._ 9 | 10 | /** 11 | * Flow listener for setting up JobConf to enable statsd-jvm-profiler 12 | */ 13 | class StatsDProfilerFlowListener extends FlowListener { 14 | val baseParamsFormat = "-javaagent:%s=server=%s,port=%s,prefix=bigdata.profiler.%s.%s.%s.%%s.%%s,packageWhitelist=%s,packageBlacklist=%s,username=%s,password=%s,database=%s,reporter=%s,tagMapping=%s" 15 | 16 | override def onStarting(flow: Flow[_]): Unit = { 17 | val profilerProps = loadProperties("statsd-jvm-profiler.properties") 18 | 19 | val jarPath = profilerProps.getProperty("jar.location") 20 | val host = profilerProps.getProperty("host") 21 | val port = profilerProps.getProperty("port") 22 | val userName = System.getProperty("user.name") 23 | val flowId = flow.getID 24 | val jobName = flow.getName.replace(".", "-") 25 | val reporter = profilerProps.getProperty("reporter") 26 | val packageBlacklist = profilerProps.getProperty("package.blacklist") 27 | val packageWhiteList = profilerProps.getProperty("package.whitelist") 28 | val influxdbUser = profilerProps.getProperty("influxdb.user") 29 | val influxdbPassword = profilerProps.getProperty("influxdb.password") 30 | val influxdbDatabase = profilerProps.getProperty("influxdb.database") 31 | val dashboardUrl = profilerProps.getProperty("dashboard.url") 32 | val tagMapping = profilerProps.getProperty("tagMapping") 33 | 34 | val baseParams = baseParamsFormat.format(jarPath, host, port, userName, jobName, flowId, packageWhiteList, packageBlacklist, influxdbUser, influxdbPassword, influxdbDatabase, reporter, tagMapping) 35 | 36 | flow.getFlowSteps.toList foreach { fs: FlowStep[_] => 37 | val stepNum = fs.getStepNum.toString 38 | val conf = fs.getConfig.asInstanceOf[JobConf] 39 | val numReduceTasks = conf.get("mapreduce.job.reduces") 40 | 41 | conf.setBoolean("mapreduce.task.profile", true) 42 | conf.set("mapreduce.task.profile.map.params", baseParams.format(stepNum, "map")) 43 | conf.set("mapreduce.task.profile.reduce.params", baseParams.format(stepNum, "reduce")) 44 | // In newer versions of Cascading/Scalding it seems to no longer be possible to retrieve the correct 45 | // number of map or reduce tasks from the flow. 46 | // As such we have to profile a predetermined task every time, rather than picking a random one 47 | conf.set("mapreduce.task.profile.maps", getTaskToProfile(stepNum, "map", conf)) 48 | conf.set("mapreduce.task.profile.reduces", getTaskToProfile(stepNum, "reduce", conf)) 49 | 50 | // If you use https://github.com/etsy/Sahale this will cause links to the profiler dashboard to appear in Sahale 51 | // for jobs that are profiled 52 | val additionalLinksOrig = conf.get("sahale.additional.links", "") 53 | val additionalLinks = numReduceTasks.toInt match { 54 | case i: Int if i <= 0 => "%s;Profiler Dashboard - Map|%s".format(additionalLinksOrig, dashboardUrl.format("map")) 55 | case _ => "%s;Profiler Dashboard - Map|%s;Profiler Dashboard - Reduce|%s".format(additionalLinksOrig, dashboardUrl.format("map"), dashboardUrl.format("reduce")) 56 | } 57 | conf.set("sahale.additional.links", additionalLinks) 58 | } 59 | } 60 | 61 | private def getTaskToProfile(stage: String, phase: String, conf: JobConf): String = { 62 | val prop = "profiler.stage%s.%s".format(stage, phase) 63 | conf.get(prop, "0") 64 | } 65 | 66 | private def loadProperties(resourceName: String): Properties = { 67 | val props = new Properties() 68 | props.load(Thread.currentThread.getContextClassLoader.getResourceAsStream(resourceName)) 69 | 70 | props 71 | } 72 | 73 | override def onThrowable(flow: Flow[_], t: Throwable): Boolean = false 74 | 75 | override def onStopping(flow: Flow[_]): Unit = () 76 | 77 | override def onCompleted(flow: Flow[_]): Unit = () 78 | } 79 | -------------------------------------------------------------------------------- /src/main/java/com/etsy/statsd/profiler/util/StackTraceFilter.java: -------------------------------------------------------------------------------- 1 | package com.etsy.statsd.profiler.util; 2 | 3 | import com.google.common.base.Function; 4 | import com.google.common.base.Joiner; 5 | import com.google.common.collect.Lists; 6 | 7 | import java.util.List; 8 | import java.util.regex.Matcher; 9 | import java.util.regex.Pattern; 10 | 11 | /** 12 | * Utility class for filtering stack traces 13 | * Assumes a string representation like that produced by @link{StackTraceFormatter} 14 | * 15 | * @author Andrew Johnson 16 | */ 17 | public class StackTraceFilter { 18 | public static final Pattern MATCH_EVERYTHING = Pattern.compile("^.*$"); 19 | public static final Pattern MATCH_NOTHING = Pattern.compile("$^"); 20 | 21 | private final Pattern includePattern; 22 | private final Pattern excludePattern; 23 | // This allows us to shortcut doing regex matching for performance 24 | private final boolean arePatternsDefault; 25 | 26 | public StackTraceFilter(List includePackages, List excludePackages) { 27 | includePattern = getPackagePattern(includePackages, MATCH_EVERYTHING); 28 | excludePattern = getPackagePattern(excludePackages, MATCH_NOTHING); 29 | arePatternsDefault = includePattern == MATCH_EVERYTHING && excludePattern == MATCH_NOTHING; 30 | } 31 | 32 | /** 33 | * Indicate if this stack trace should be included in the filter 34 | * Checks if the stack trace matches the include pattern and does not match the exclude pattern 35 | * 36 | * @param formattedStackTrace The stack trace to check for inclusion in the filter 37 | * @return True if it should be included, false otherwise 38 | */ 39 | public boolean includeStackTrace(String formattedStackTrace) { 40 | return arePatternsDefault || includeMatches(formattedStackTrace) && !excludeMatches(formattedStackTrace); 41 | } 42 | 43 | /** 44 | * Indicate if this stack trace matches one of the included packages 45 | * 46 | * @param formattedStackTrace The stack trace to check against the included packages 47 | * @return True if it matches an included package, false otherwise 48 | */ 49 | public boolean includeMatches(String formattedStackTrace) { 50 | return matches(includePattern, formattedStackTrace); 51 | } 52 | 53 | /** 54 | * Indicate if this stack trace matches one of the excluded packages 55 | * 56 | * @param formattedStackTrace The stack trace to check against the excluded packages 57 | * @return True if it matches an excluded package, false otherwise 58 | */ 59 | public boolean excludeMatches(String formattedStackTrace) { 60 | return matches(excludePattern, formattedStackTrace); 61 | } 62 | 63 | /** 64 | * Indicate if this stack trace matches the given pattern 65 | * 66 | * @param pattern The pattern to match against the stack trace 67 | * @param formattedStackTrace The stack trace to check against the pattern 68 | * @return True if the stack trace matches the pattern, false otherwise 69 | */ 70 | private static boolean matches(Pattern pattern, String formattedStackTrace) { 71 | Matcher matcher = pattern.matcher(formattedStackTrace); 72 | 73 | return matcher.matches(); 74 | } 75 | 76 | /** 77 | * Construct a Pattern that matches any of the given packages 78 | * 79 | * @param packageWhitelist The packages to match in this Pattern 80 | * @return A Pattern object that matches any of the given packages 81 | */ 82 | public Pattern getPackagePattern(List packageWhitelist, Pattern defaultPattern) { 83 | if (packageWhitelist == null || packageWhitelist.isEmpty()) { 84 | return defaultPattern; 85 | } 86 | else { 87 | return Pattern.compile(String.format("(.*\\.|^)(%s).*", 88 | Joiner.on("|").join(Lists.transform(packageWhitelist, new Function() { 89 | @Override 90 | public String apply(String s) { 91 | return String.format("(%s)", s.replace(".", "-")); 92 | } 93 | })))); 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /visualization/influxdb-dashboard/public/scripts/influxdb.js: -------------------------------------------------------------------------------- 1 | var influx = require('influx') 2 | var config = require('./config.js') 3 | 4 | var client = null; 5 | config.getConfig(function(conf) { 6 | client = influx(conf['influxdb']); 7 | }); 8 | 9 | // This measurement will always exist and can be used to query for valid tags 10 | var canaryMeasurement = "heap.total.max"; 11 | 12 | exports.getData = function(user, job, flow, stage, phase, jvmName, metric, callback) { 13 | var optionalJvmName = jvmName ? "' and jvmName = '" + jvmName + "'": "'"; 14 | var query = "select value from /^" + metric + ".*/ where username = '" + user + "' and job = '" + job + "' and flow = '" + flow + "' and stage = '" + stage + "' and phase = '" + phase + optionalJvmName; 15 | client.queryRaw(query, function(err, res) { 16 | var series = res[0].series; 17 | var results = {}; 18 | if (series) { 19 | results = series.map(function(series) { 20 | var name = series.name.replace(metric + '.', ''); 21 | var points = series.values.map(function(value) { 22 | return {time: new Date(value[0]).getTime(), value: value[1]}; 23 | }); 24 | 25 | return {metric: name, values: points}; 26 | }); 27 | } 28 | callback(results); 29 | }); 30 | } 31 | 32 | 33 | exports.getFlameGraphData = function(user, job, flow, stage, phase, jvmName, prefix, callback) { 34 | var optionalJvmName = jvmName ? "' and jvmName = '" + jvmName + "'": "'"; 35 | var query = "select value from /^" + prefix + ".*/ where username = '" + user + "' and job = '" + job + "' and flow = '" + flow + "' and stage = '" + stage + "' and phase = '" + phase + optionalJvmName; 36 | client.queryRaw(query, function(err, res) { 37 | var series = res[0].series; 38 | var results = {}; 39 | if (series) { 40 | results = series.map(function(series) { 41 | var name = formatMetric(series.name, prefix); 42 | var value = (null, series.values.map(function(value) { 43 | return value[1]; 44 | })).reduce(function(total, curr) { 45 | return total + curr; 46 | }, 0); 47 | 48 | return {metric: name, value: value}; 49 | }); 50 | } 51 | callback(results); 52 | }); 53 | } 54 | 55 | exports.getOptions = function(prefix, callback) { 56 | client.getSeries("heap.total.max", function(err, seriesNames) { 57 | var result = {}; 58 | if (seriesNames !== undefined) { 59 | var series = seriesNames[0] 60 | var columns = series.values.map(function(value) { 61 | var tokens = value[0].split(','); 62 | column = {} 63 | tokens.forEach(function(token) { 64 | if (token.indexOf("=") != -1) { 65 | var split = token.split("="); 66 | column[split[0]] = split[1]; 67 | } 68 | }) 69 | return column; 70 | }); 71 | 72 | columns.map(function(values) { 73 | var user = values['username']; 74 | var job = values['job']; 75 | var flow = values['flow']; 76 | var stage = values['stage']; 77 | var phase = values['phase']; 78 | var jvmName = values['jvmName']; 79 | 80 | var userVal = result[user] 81 | if (!userVal) { 82 | userVal = {}; 83 | } 84 | 85 | var jobVal = userVal[job]; 86 | if (!jobVal) { 87 | jobVal = {}; 88 | } 89 | 90 | var flowVal = jobVal[flow]; 91 | if(!flowVal) { 92 | flowVal = {}; 93 | } 94 | 95 | var stageVal = flowVal[stage]; 96 | if(!stageVal) { 97 | stageVal = {}; 98 | } 99 | 100 | 101 | var phaseVal = stageVal[phase]; 102 | if(!phaseVal) { 103 | phaseVal = []; 104 | } 105 | 106 | if (phaseVal.indexOf(jvmName) == -1) { 107 | phaseVal.push(jvmName); 108 | } 109 | 110 | stageVal[phase] = phaseVal; 111 | flowVal[stage] = stageVal; 112 | jobVal[flow] = flowVal; 113 | userVal[job] = jobVal; 114 | result[user] = userVal; 115 | }); 116 | callback(result); 117 | } 118 | }); 119 | } 120 | 121 | function formatMetric(metric, prefix) { 122 | var noPrefix = metric.replace(prefix, ''); 123 | var frames = noPrefix.split('.'); 124 | frames.splice(0, 1); 125 | frames.reverse(); 126 | var lineNumFrames = frames.map(function(frame) { 127 | var lineNumSepIndex = frame.lastIndexOf('-'); 128 | return frame.substring(0, lineNumSepIndex) + ':' + frame.substring(lineNumSepIndex+1); 129 | }); 130 | 131 | return lineNumFrames.join(';').replace(/-/g, '.'); 132 | } 133 | -------------------------------------------------------------------------------- /visualization/influxdb-dashboard/public/scripts/render-client.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | $("#toc").toc({ 3 | 'highlightOffset': 1, 4 | }); 5 | var params = URI(window.location.href).search(true); 6 | 7 | var user = params.user; 8 | var job = params.job; 9 | var flow = params.flow; 10 | var stage = params.stage; 11 | var phase = params.phase; 12 | var jvmName = params.jvm; 13 | var base = params.prefix || $('#prefix').text() || 'bigdata.profiler'; 14 | var refresh = params.refresh || $('#refresh').text() || 60; 15 | var optionalJvmName = jvmName && jvmName !== '' ? '/' + jvmName + '/' : '/'; 16 | 17 | var prefix = base + '.' + user + '.' + job + '.' + flow + '.' + stage + '.' + phase; 18 | var cpuPrefix = 'cpu.trace'; 19 | var heapPrefix = 'heap'; 20 | var nonHeapPrefix = 'nonheap'; 21 | var finalizePrefix = 'pending-finalization-count'; 22 | var gcPrefix = 'gc'; 23 | var classLoadingPrefix = '.*class-count'; 24 | 25 | var memoryMetrics = [{metric:'init', alias:'Initial'},{metric:'committed', alias:'Committed'},{metric:'max', alias:'Maximum'},{metric:'used', alias:'Used'}]; 26 | var finalizeMetrics = [{metric: finalizePrefix, alias: 'Objects Pending Finalization'}]; 27 | var gcCountMetrics = [{metric:'PS_MarkSweep.count', alias:'PS_MarkSweep'},{metric:'PS_Scavenge.count', alias:'PS_Scavenge'}]; 28 | var gcTimeMetrics = [{metric:'PS_MarkSweep.time', alias:'PS_MarkSweep'},{metric:'PS_Scavenge.time', alias:'PS_Scavenge'}]; 29 | var gcRuntimeMetrics = [{metric:'PS_MarkSweep.runtime', alias:'PS_MarkSweep'},{metric:'PS_Scavenge.runtime', alias:'PS_Scavenge'}]; 30 | var classLoadingMetrics = [{metric:'loaded-class-count', alias:'Classes Loaded'},{metric:'total-loaded-class-count', alias:'Total Classes Loaded'},{metric:'unloaded-class-count', alias:'Classes Unloaded'}]; 31 | var heapPools = [{pool:'ps-eden-space', selector:'#eden', title:'Eden'}, {pool:'ps-old-gen', selector:'#oldgen', title:'Old Generation'}, {pool:'ps-survivor-space', selector:'#survivor', title:'Survivor Space'}]; 32 | var nonHeapPools = [{pool:'code-cache', selector:'#codecache', title:'Code Cache'}, {pool:'ps-perm-gen', selector:'#permgen', title:'Permgen'}]; 33 | 34 | $("#toc ul").append('

    • Flame Graph
    • '); 35 | $('#toc').affix({ 36 | offset: { 37 | top: $('.navbar navbar-default').height() 38 | } 39 | }); 40 | 41 | var heapGet = $.get('/data/' + user + '/' + job + '/' + flow + '/' + stage + '/' + phase + optionalJvmName + heapPrefix); 42 | var nonHeapGet = $.get('/data/' + user + '/' + job + '/' + flow + '/' + stage + '/' + phase + optionalJvmName + nonHeapPrefix); 43 | var finalizeGet = $.get('/data/' + user + '/' + job + '/' + flow + '/' + stage + '/' + phase + optionalJvmName + finalizePrefix); 44 | var gcGet = $.get('/data/' + user + '/' + job + '/' + flow + '/' + stage + '/' + phase + optionalJvmName + gcPrefix); 45 | var classLoadingGet = $.get('/data/' + user + '/' + job + '/' + flow + '/' + stage + '/' + phase + optionalJvmName + classLoadingPrefix); 46 | 47 | $.when(heapGet).done(function() { 48 | var heapResults = heapGet['responseJSON']; 49 | ViewUtil.renderGraph(heapResults, 'Heap Usage', '#heap', ViewUtil.getMetricsForPool('total', memoryMetrics)); 50 | heapPools.forEach(function(pool) { 51 | ViewUtil.renderGraph(heapResults, pool.title, pool.selector, ViewUtil.getMetricsForPool(pool.pool, memoryMetrics)); 52 | }); 53 | }); 54 | 55 | $.when(nonHeapGet).done(function() { 56 | var nonHeapResults = nonHeapGet['responseJSON']; 57 | ViewUtil.renderGraph(nonHeapResults, 'Non-Heap Usage', '#nonheap', ViewUtil.getMetricsForPool('total', memoryMetrics)); 58 | 59 | nonHeapPools.forEach(function(pool) { 60 | ViewUtil.renderGraph(nonHeapResults, pool.title, pool.selector, ViewUtil.getMetricsForPool(pool.pool, memoryMetrics)); 61 | }); 62 | }); 63 | 64 | $.when(finalizeGet).done(function() { 65 | var finalizeResults = finalizeGet['responseJSON']; 66 | ViewUtil.renderGraph(finalizeResults, 'Objects Pending Finalization', '#finalize', finalizeMetrics); 67 | }); 68 | 69 | $.when(gcGet).done(function() { 70 | var gcResults = gcGet['responseJSON']; 71 | ViewUtil.renderGraph(gcResults, 'Garbage Collection', '#count', gcCountMetrics); 72 | ViewUtil.renderGraph(gcResults, 'Garbage Collection', '#time', gcTimeMetrics); 73 | ViewUtil.renderGraph(gcResults, 'Garbage Collection', '#runtime', gcRuntimeMetrics); 74 | }); 75 | 76 | $.when(classLoadingGet).done(function() { 77 | var classLoadingResults = classLoadingGet['responseJSON']; 78 | ViewUtil.renderGraph(classLoadingResults, 'Class Loading', '#classloading', classLoadingMetrics); 79 | }); 80 | 81 | if (refresh > 0) { 82 | setTimeout(function() { 83 | location.reload(); 84 | }, refresh * 1000); 85 | } 86 | }); 87 | -------------------------------------------------------------------------------- /src/main/java/com/etsy/statsd/profiler/reporter/InfluxDBReporter.java: -------------------------------------------------------------------------------- 1 | package com.etsy.statsd.profiler.reporter; 2 | 3 | import com.etsy.statsd.profiler.Arguments; 4 | import com.etsy.statsd.profiler.util.TagUtil; 5 | import com.google.common.base.Preconditions; 6 | import com.google.common.collect.ImmutableMap; 7 | import org.influxdb.InfluxDB; 8 | import org.influxdb.InfluxDBFactory; 9 | import org.influxdb.dto.BatchPoints; 10 | import org.influxdb.dto.Point; 11 | 12 | import java.util.Map; 13 | import java.util.concurrent.TimeUnit; 14 | 15 | /** 16 | * Reporter that sends data to InfluxDB 17 | * 18 | * @author Andrew Johnson 19 | */ 20 | public class InfluxDBReporter extends Reporter { 21 | public static final String VALUE_COLUMN = "value"; 22 | public static final String USERNAME_ARG = "username"; 23 | public static final String PASSWORD_ARG = "password"; 24 | public static final String DATABASE_ARG = "database"; 25 | public static final String TAG_MAPPING_ARG = "tagMapping"; 26 | 27 | private String username; 28 | private String password; 29 | private String database; 30 | private String tagMapping; 31 | private final Map tags; 32 | 33 | public InfluxDBReporter(Arguments arguments) { 34 | super(arguments); 35 | String prefix = arguments.metricsPrefix; 36 | // If we have a tag mapping it must match the number of components of the prefix 37 | Preconditions.checkArgument(tagMapping == null || tagMapping.split("\\.").length == prefix.split("\\.").length); 38 | tags = TagUtil.getTags(tagMapping, prefix, true); 39 | } 40 | 41 | /** 42 | * Record a gauge value in InfluxDB 43 | * 44 | * @param key The key for the gauge 45 | * @param value The value of the gauge 46 | */ 47 | @Override 48 | public void recordGaugeValue(String key, long value, String... tags) { 49 | Map gauges = ImmutableMap.of(key, value); 50 | recordGaugeValues(gauges); 51 | } 52 | 53 | /** 54 | * @see #recordGaugeValue(String, long, String...) 55 | */ 56 | @Override 57 | public void recordGaugeValue(String key, double value, String... tags) { 58 | Map gauges = ImmutableMap.of(key, value); 59 | recordGaugeValues(gauges); 60 | } 61 | 62 | /** 63 | * Record multiple gauge values in InfluxDB 64 | * 65 | * @param gauges A map of gauge names to values 66 | */ 67 | @Override 68 | public void recordGaugeValues(Map gauges, String... tags) { 69 | long time = System.currentTimeMillis(); 70 | BatchPoints batchPoints = BatchPoints.database(database).build(); 71 | for (Map.Entry gauge: gauges.entrySet()) { 72 | batchPoints.point(constructPoint(time, gauge.getKey(), gauge.getValue())); 73 | } 74 | client.write(batchPoints); 75 | } 76 | 77 | /** 78 | * InfluxDB has a rich query language and does not need the bounds metrics emitted by CPUTracingProfiler 79 | * As such we can disable emitting these metrics 80 | * 81 | * @return false 82 | */ 83 | @Override 84 | public boolean emitBounds() { 85 | return false; 86 | } 87 | 88 | /** 89 | * 90 | * @param server The server to which to report data 91 | * @param port The port on which the server is running 92 | * @param prefix The prefix for metrics 93 | * @return An InfluxDB client 94 | */ 95 | @Override 96 | protected InfluxDB createClient(String server, int port, String prefix) { 97 | return InfluxDBFactory.connect(String.format("http://%s:%d", server, port), username, password); 98 | } 99 | 100 | /** 101 | * Handle remaining arguments 102 | * 103 | * @param arguments The arguments given to the profiler agent 104 | */ 105 | @Override 106 | protected void handleArguments(Arguments arguments) { 107 | username = arguments.remainingArgs.get(USERNAME_ARG); 108 | password = arguments.remainingArgs.get(PASSWORD_ARG); 109 | database = arguments.remainingArgs.get(DATABASE_ARG); 110 | tagMapping = arguments.remainingArgs.get(TAG_MAPPING_ARG); 111 | 112 | Preconditions.checkNotNull(username); 113 | Preconditions.checkNotNull(password); 114 | Preconditions.checkNotNull(database); 115 | } 116 | 117 | private Point constructPoint(long time, String key, Number value) { 118 | Point.Builder builder = Point.measurement(key) 119 | .time(time, TimeUnit.MILLISECONDS) 120 | .field(VALUE_COLUMN, value); 121 | for (Map.Entry entry : tags.entrySet()) { 122 | builder = builder.tag(entry.getKey(), entry.getValue()); 123 | } 124 | 125 | return builder.build(); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/main/java/com/etsy/statsd/profiler/profilers/CPUTracingProfiler.java: -------------------------------------------------------------------------------- 1 | package com.etsy.statsd.profiler.profilers; 2 | 3 | import com.etsy.statsd.profiler.Arguments; 4 | import com.etsy.statsd.profiler.Profiler; 5 | import com.etsy.statsd.profiler.reporter.Reporter; 6 | import com.etsy.statsd.profiler.util.*; 7 | import com.etsy.statsd.profiler.worker.ProfilerThreadFactory; 8 | import com.google.common.base.Predicate; 9 | import com.google.common.collect.Iterables; 10 | import com.google.common.collect.Lists; 11 | 12 | import java.lang.management.ThreadInfo; 13 | import java.util.ArrayList; 14 | import java.util.Arrays; 15 | import java.util.Collection; 16 | import java.util.List; 17 | import java.util.concurrent.TimeUnit; 18 | 19 | /** 20 | * Profiles CPU time spent in each method 21 | * 22 | * @author Andrew Johnson 23 | */ 24 | public class CPUTracingProfiler extends Profiler { 25 | private static final String PACKAGE_WHITELIST_ARG = "packageWhitelist"; 26 | private static final String PACKAGE_BLACKLIST_ARG = "packageBlacklist"; 27 | 28 | public static final long REPORTING_PERIOD = 1; 29 | public static final long PERIOD = 10; 30 | public static final List EXCLUDE_PACKAGES = Arrays.asList("com.etsy.statsd.profiler", "com.timgroup.statsd"); 31 | 32 | private final CPUTraces traces; 33 | private long profileCount; 34 | private StackTraceFilter filter; 35 | private final long reportingFrequency; 36 | 37 | 38 | public CPUTracingProfiler(Reporter reporter, Arguments arguments) { 39 | super(reporter, arguments); 40 | traces = new CPUTraces(); 41 | profileCount = 0; 42 | reportingFrequency = TimeUtil.convertReportingPeriod(getPeriod(), getTimeUnit(), REPORTING_PERIOD, TimeUnit.SECONDS); 43 | } 44 | 45 | /** 46 | * Profile CPU time by method call 47 | */ 48 | @Override 49 | public void profile() { 50 | profileCount++; 51 | 52 | for (ThreadInfo thread : getAllRunnableThreads()) { 53 | // certain threads do not have stack traces 54 | if (thread.getStackTrace().length > 0) { 55 | String traceKey = StackTraceFormatter.formatStackTrace(thread.getStackTrace()); 56 | if (filter.includeStackTrace(traceKey)) { 57 | traces.increment(traceKey, 1); 58 | } 59 | } 60 | } 61 | 62 | // To keep from overwhelming StatsD, we only report statistics every second 63 | if (profileCount == reportingFrequency) { 64 | profileCount = 0; 65 | recordMethodCounts(); 66 | } 67 | } 68 | 69 | /** 70 | * Flush methodCounts data on shutdown 71 | */ 72 | @Override 73 | public void flushData() { 74 | recordMethodCounts(); 75 | // These bounds are recorded to help speed up generating flame graphs for certain backends 76 | if (emitBounds()) { 77 | Range bounds = traces.getBounds(); 78 | recordGaugeValue("cpu.trace." + bounds.getLeft(), bounds.getLeft()); 79 | recordGaugeValue("cpu.trace." + bounds.getRight(), bounds.getRight()); 80 | } 81 | } 82 | 83 | @Override 84 | public long getPeriod() { 85 | return PERIOD; 86 | } 87 | 88 | @Override 89 | public TimeUnit getTimeUnit() { 90 | return TimeUnit.MILLISECONDS; 91 | } 92 | 93 | @Override 94 | protected void handleArguments(Arguments arguments) { 95 | List packageWhitelist = parsePackageList(arguments.remainingArgs.get(PACKAGE_WHITELIST_ARG)); 96 | List packageBlacklist = parsePackageList(arguments.remainingArgs.get(PACKAGE_BLACKLIST_ARG)); 97 | filter = new StackTraceFilter(packageWhitelist, Lists.newArrayList(Iterables.concat(EXCLUDE_PACKAGES, packageBlacklist))); 98 | } 99 | 100 | /** 101 | * Parses a colon-delimited list of packages 102 | * 103 | * @param packages A string containing a colon-delimited list of packages 104 | * @return A List of packages 105 | */ 106 | private static List parsePackageList(String packages) { 107 | if (packages == null) { 108 | return new ArrayList<>(); 109 | } else { 110 | return Arrays.asList(packages.split(":")); 111 | } 112 | } 113 | 114 | /** 115 | * Records method CPU time in StatsD 116 | */ 117 | private void recordMethodCounts() { 118 | recordGaugeValues(traces.getDataToFlush()); 119 | } 120 | 121 | /** 122 | * Gets all runnable threads, excluding profiler threads 123 | * 124 | * @return A Collection representing current thread state 125 | */ 126 | private static Collection getAllRunnableThreads() { 127 | return ThreadDumper.filterAllThreadsInState(false, false, Thread.State.RUNNABLE, new Predicate() { 128 | @Override 129 | public boolean apply(ThreadInfo input) { 130 | return !input.getThreadName().startsWith(ProfilerThreadFactory.NAME_PREFIX); 131 | } 132 | }); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /checkstyle.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /src/test/java/com/etsy/statsd/profiler/server/ProfilerServerTest.java: -------------------------------------------------------------------------------- 1 | package com.etsy.statsd.profiler.server; 2 | 3 | import com.etsy.statsd.profiler.Profiler; 4 | import com.etsy.statsd.profiler.profilers.MockProfiler1; 5 | import com.etsy.statsd.profiler.profilers.MockProfiler2; 6 | import com.etsy.statsd.profiler.worker.ProfilerThreadFactory; 7 | import com.etsy.statsd.profiler.worker.ProfilerWorkerThread; 8 | import com.google.common.base.Joiner; 9 | import com.google.common.util.concurrent.MoreExecutors; 10 | import org.apache.http.HttpEntity; 11 | import org.apache.http.client.methods.CloseableHttpResponse; 12 | import org.apache.http.client.methods.HttpGet; 13 | import org.apache.http.client.methods.HttpRequestBase; 14 | import org.apache.http.impl.client.CloseableHttpClient; 15 | import org.apache.http.impl.client.HttpClients; 16 | import org.apache.http.util.EntityUtils; 17 | import org.junit.Before; 18 | import org.junit.Test; 19 | 20 | import java.io.IOException; 21 | import java.util.*; 22 | import java.util.concurrent.Executors; 23 | import java.util.concurrent.ScheduledExecutorService; 24 | import java.util.concurrent.ScheduledFuture; 25 | import java.util.concurrent.ScheduledThreadPoolExecutor; 26 | import java.util.concurrent.atomic.AtomicReference; 27 | 28 | import static org.junit.Assert.assertEquals; 29 | import static org.junit.Assert.assertNotNull; 30 | 31 | public class ProfilerServerTest { 32 | private Map activeProfilers; 33 | private int port; 34 | private AtomicReference isRunning; 35 | private List errors; 36 | private CloseableHttpClient client; 37 | 38 | @Before 39 | public void setup() throws IOException { 40 | MockProfiler1 profiler1 = new MockProfiler1(new HashSet()); 41 | MockProfiler2 profiler2 = new MockProfiler2(new HashSet()); 42 | 43 | activeProfilers = new HashMap<>(); 44 | activeProfilers.put("MockProfiler1", profiler1); 45 | activeProfilers.put("MockProfiler2", profiler2); 46 | 47 | port = 8080; 48 | 49 | isRunning = new AtomicReference<>(true); 50 | errors = new ArrayList<>(); 51 | errors.add("example error"); 52 | 53 | Map> runningProfilers = new HashMap<>(); 54 | ProfilerWorkerThread worker1 = new ProfilerWorkerThread(profiler1, errors); 55 | ProfilerWorkerThread worker2 = new ProfilerWorkerThread(profiler2, errors); 56 | ScheduledExecutorService scheduledExecutorService = MoreExecutors.getExitingScheduledExecutorService( 57 | (ScheduledThreadPoolExecutor) Executors.newScheduledThreadPool(2, new ProfilerThreadFactory())); 58 | ScheduledFuture future1 = scheduledExecutorService.scheduleAtFixedRate(worker1, 0, profiler1.getPeriod(), profiler1.getTimeUnit()); 59 | ScheduledFuture future2 = scheduledExecutorService.scheduleAtFixedRate(worker2, 0, profiler2.getPeriod(), profiler2.getTimeUnit()); 60 | runningProfilers.put("MockProfiler1", future1); 61 | runningProfilers.put("MockProfiler2", future2); 62 | 63 | ProfilerServer.startServer(runningProfilers, activeProfilers, port, isRunning, errors); 64 | client = HttpClients.createDefault(); 65 | } 66 | 67 | @Test 68 | public void testDisableProfiler() throws IOException { 69 | String profilerString = "MockProfiler1\nMockProfiler2"; 70 | httpRequestTest("profilers", profilerString); 71 | 72 | String profilerToDisable = "MockProfiler1"; 73 | httpRequestTest(String.format("disable/%s", profilerToDisable), String.format("Disabled profiler %s", profilerToDisable)); 74 | 75 | profilerString = "MockProfiler2"; 76 | httpRequestTest("profilers", profilerString); 77 | } 78 | 79 | @Test 80 | public void testProfilerStatus() throws IOException { 81 | String profilerName = "MockProfiler1"; 82 | long recordedStats = activeProfilers.get(profilerName).getRecordedStats(); 83 | httpRequestTest(String.format("status/profiler/%s", profilerName), String.format("Recorded stats %d\n", recordedStats)); 84 | } 85 | 86 | @Test 87 | public void testProfilers() throws IOException { 88 | String profilerString = "MockProfiler1\nMockProfiler2"; 89 | httpRequestTest("profilers", profilerString); 90 | } 91 | 92 | @Test 93 | public void testErrors() throws IOException { 94 | String errorString = Joiner.on("\n").join(errors); 95 | httpRequestTest("errors", String.format("Errors: %s", errorString)); 96 | } 97 | 98 | @Test 99 | public void testIsRunning() throws IOException { 100 | httpRequestTest("isRunning", String.format("isRunning: %s", isRunning.get().toString())); 101 | } 102 | 103 | private void httpRequestTest(String path, String expectedBody) throws IOException { 104 | HttpRequestBase get = new HttpGet(String.format("http://localhost:%d/%s", port, path)); 105 | CloseableHttpResponse response = client.execute(get); 106 | 107 | int statusCode = response.getStatusLine().getStatusCode(); 108 | assertEquals(200, statusCode); 109 | 110 | HttpEntity entity = response.getEntity(); 111 | assertNotNull(entity); 112 | String responseBody = EntityUtils.toString(entity); 113 | assertEquals(expectedBody, responseBody); 114 | 115 | response.close(); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/main/java/com/etsy/statsd/profiler/Agent.java: -------------------------------------------------------------------------------- 1 | package com.etsy.statsd.profiler; 2 | 3 | import com.etsy.statsd.profiler.reporter.Reporter; 4 | import com.etsy.statsd.profiler.server.ProfilerServer; 5 | import com.etsy.statsd.profiler.worker.ProfilerShutdownHookWorker; 6 | import com.etsy.statsd.profiler.worker.ProfilerThreadFactory; 7 | import com.etsy.statsd.profiler.worker.ProfilerWorkerThread; 8 | import com.google.common.util.concurrent.MoreExecutors; 9 | 10 | import java.lang.instrument.Instrumentation; 11 | import java.lang.reflect.Constructor; 12 | import java.lang.reflect.InvocationTargetException; 13 | import java.util.*; 14 | import java.util.concurrent.Executors; 15 | import java.util.concurrent.ScheduledExecutorService; 16 | import java.util.concurrent.ScheduledFuture; 17 | import java.util.concurrent.ScheduledThreadPoolExecutor; 18 | import java.util.concurrent.atomic.AtomicReference; 19 | 20 | /** 21 | * javaagent profiler using StatsD as a backend 22 | * 23 | * @author Andrew Johnson 24 | */ 25 | public final class Agent { 26 | public static final int EXECUTOR_DELAY = 0; 27 | 28 | static AtomicReference isRunning = new AtomicReference<>(true); 29 | static LinkedList errors = new LinkedList<>(); 30 | 31 | private Agent() { } 32 | 33 | public static void agentmain(final String args, final Instrumentation instrumentation) { 34 | premain(args, instrumentation); 35 | } 36 | 37 | /** 38 | * Start the profiler 39 | * 40 | * @param args Profiler arguments 41 | * @param instrumentation Instrumentation agent 42 | */ 43 | public static void premain(final String args, final Instrumentation instrumentation) { 44 | Arguments arguments = Arguments.parseArgs(args); 45 | 46 | Reporter reporter = instantiate(arguments.reporter, Reporter.CONSTRUCTOR_PARAM_TYPES, arguments); 47 | 48 | Collection profilers = new ArrayList<>(); 49 | for (Class profiler : arguments.profilers) { 50 | profilers.add(instantiate(profiler, Profiler.CONSTRUCTOR_PARAM_TYPES, reporter, arguments)); 51 | } 52 | 53 | scheduleProfilers(profilers, arguments); 54 | registerShutdownHook(profilers); 55 | } 56 | 57 | /** 58 | * Schedule profilers with a SchedulerExecutorService 59 | * 60 | * @param profilers Collection of profilers to schedule 61 | * @param arguments 62 | */ 63 | private static void scheduleProfilers(Collection profilers, Arguments arguments) { 64 | // We need to convert to an ExitingScheduledExecutorService so the JVM shuts down 65 | // when the main thread finishes 66 | ScheduledExecutorService scheduledExecutorService = MoreExecutors.getExitingScheduledExecutorService( 67 | (ScheduledThreadPoolExecutor) Executors.newScheduledThreadPool(profilers.size(), new ProfilerThreadFactory())); 68 | 69 | Map> runningProfilers = new HashMap<>(profilers.size()); 70 | Map activeProfilers = new HashMap<>(profilers.size()); 71 | for (Profiler profiler : profilers) { 72 | activeProfilers.put(profiler.getClass().getSimpleName(), profiler); 73 | ProfilerWorkerThread worker = new ProfilerWorkerThread(profiler, errors); 74 | ScheduledFuture future = scheduledExecutorService.scheduleAtFixedRate(worker, EXECUTOR_DELAY, profiler.getPeriod(), profiler.getTimeUnit()); 75 | runningProfilers.put(profiler.getClass().getSimpleName(), future); 76 | } 77 | 78 | if (arguments.httpServerEnabled) { 79 | ProfilerServer.startServer(runningProfilers, activeProfilers, arguments.httpPort, isRunning, errors); 80 | } 81 | } 82 | 83 | /** 84 | * Register a shutdown hook to flush profiler data to StatsD 85 | * 86 | * @param profilers The profilers to flush at shutdown 87 | */ 88 | private static void registerShutdownHook(Collection profilers) { 89 | Thread shutdownHook = new Thread(new ProfilerShutdownHookWorker(profilers, isRunning)); 90 | Runtime.getRuntime().addShutdownHook(shutdownHook); 91 | } 92 | 93 | /** 94 | * Uniformed handling of initialization exception 95 | * 96 | * @param clazz The class that could not be instantiated 97 | * @param cause The underlying exception 98 | */ 99 | private static void handleInitializationException(final Class clazz, final Exception cause) { 100 | throw new RuntimeException("Unable to instantiate " + clazz.getSimpleName(), cause); 101 | } 102 | 103 | /** 104 | * Instantiate an object 105 | * 106 | * @param clazz A Class representing the type of object to instantiate 107 | * @param parameterTypes The parameter types for the constructor 108 | * @param initArgs The values to pass to the constructor 109 | * @param The type of the object to instantiate 110 | * @return A new instance of type T 111 | */ 112 | private static T instantiate(final Class clazz, Class[] parameterTypes, Object... initArgs) { 113 | try { 114 | Constructor constructor = clazz.getConstructor(parameterTypes); 115 | return constructor.newInstance(initArgs); 116 | } catch (NoSuchMethodException | IllegalAccessException | InstantiationException | InvocationTargetException e) { 117 | handleInitializationException(clazz, e); 118 | } 119 | 120 | return null; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/main/java/com/etsy/statsd/profiler/Arguments.java: -------------------------------------------------------------------------------- 1 | package com.etsy.statsd.profiler; 2 | 3 | import com.etsy.statsd.profiler.profilers.CPUTracingProfiler; 4 | import com.etsy.statsd.profiler.profilers.MemoryProfiler; 5 | import com.etsy.statsd.profiler.reporter.Reporter; 6 | import com.etsy.statsd.profiler.reporter.StatsDReporter; 7 | import com.google.common.base.Optional; 8 | 9 | import java.util.*; 10 | 11 | /** 12 | * Represents arguments to the profiler 13 | * 14 | * @author Andrew Johnson 15 | */ 16 | public final class Arguments { 17 | private static final String SERVER = "server"; 18 | private static final String PORT = "port"; 19 | private static final String METRICS_PREFIX = "prefix"; 20 | private static final String PROFILERS = "profilers"; 21 | private static final String REPORTER = "reporter"; 22 | private static final String HTTP_PORT = "httpPort"; 23 | private static final String HTTP_SEVER_ENABLED = "httpServerEnabled"; 24 | 25 | private static final Collection REQUIRED = Arrays.asList(SERVER, PORT); 26 | 27 | public String server; 28 | public int port; 29 | public String metricsPrefix; 30 | public Set> profilers; 31 | public Map remainingArgs; 32 | public Class> reporter; 33 | public int httpPort; 34 | public boolean httpServerEnabled; 35 | 36 | private Arguments(Map parsedArgs) { 37 | server = parsedArgs.get(SERVER); 38 | port = Integer.parseInt(parsedArgs.get(PORT)); 39 | metricsPrefix = Optional.fromNullable(parsedArgs.get(METRICS_PREFIX)).or("statsd-jvm-profiler"); 40 | profilers = parseProfilerArg(parsedArgs.get(PROFILERS)); 41 | reporter = parserReporterArg(parsedArgs.get(REPORTER)); 42 | httpPort = Integer.parseInt(Optional.fromNullable(parsedArgs.get(HTTP_PORT)).or("5005")); 43 | httpServerEnabled = Boolean.parseBoolean(Optional.fromNullable(parsedArgs.get(HTTP_SEVER_ENABLED)).or("true")); 44 | 45 | parsedArgs.remove(SERVER); 46 | parsedArgs.remove(PORT); 47 | parsedArgs.remove(METRICS_PREFIX); 48 | parsedArgs.remove(PROFILERS); 49 | remainingArgs = parsedArgs; 50 | } 51 | 52 | /** 53 | * Parses arguments into an Arguments object 54 | * 55 | * @param args A String containing comma-delimited args in k=v form 56 | * @return An Arguments object representing the given arguments 57 | */ 58 | public static Arguments parseArgs(final String args) { 59 | Map parsed = new HashMap<>(); 60 | for (String argPair : args.split(",")) { 61 | String[] tokens = argPair.split("="); 62 | if (tokens.length != 2) { 63 | throw new IllegalArgumentException("statsd-jvm-profiler takes a comma-delimited list of arguments in k=v form"); 64 | } 65 | 66 | parsed.put(tokens[0], tokens[1]); 67 | } 68 | 69 | for (String requiredArg : REQUIRED) { 70 | if (!parsed.containsKey(requiredArg)) { 71 | throw new IllegalArgumentException(String.format("%s argument was not supplied", requiredArg)); 72 | } 73 | } 74 | 75 | return new Arguments(parsed); 76 | } 77 | 78 | @SuppressWarnings("unchecked") 79 | private static Class> parserReporterArg(String reporterArg) { 80 | if (reporterArg == null) { 81 | return StatsDReporter.class; 82 | } else { 83 | try { 84 | return (Class>) Class.forName(reporterArg); 85 | } catch (ClassNotFoundException e) { 86 | // This might indicate the package was left off, so we'll try with the default package 87 | try { 88 | return (Class>) Class.forName("com.etsy.statsd.profiler.reporter." + reporterArg); 89 | } catch (ClassNotFoundException inner) { 90 | throw new IllegalArgumentException("Reporter " + reporterArg + " not found", inner); 91 | } 92 | } 93 | } 94 | } 95 | 96 | @SuppressWarnings("unchecked") 97 | private static Set> parseProfilerArg(String profilerArg) { 98 | Set> parsedProfilers = new HashSet<>(); 99 | if (profilerArg == null) { 100 | parsedProfilers.add(CPUTracingProfiler.class); 101 | parsedProfilers.add(MemoryProfiler.class); 102 | } else { 103 | for (String p : profilerArg.split(":")) { 104 | try { 105 | parsedProfilers.add((Class) Class.forName(p)); 106 | } catch (ClassNotFoundException e) { 107 | // This might indicate the package was left off, so we'll try with the default package 108 | try { 109 | parsedProfilers.add((Class) Class.forName("com.etsy.statsd.profiler.profilers." + p)); 110 | } catch (ClassNotFoundException inner) { 111 | throw new IllegalArgumentException("Profiler " + p + " not found", inner); 112 | } 113 | } 114 | } 115 | } 116 | 117 | if (parsedProfilers.isEmpty()) { 118 | throw new IllegalArgumentException("At least one profiler must be specified"); 119 | } 120 | 121 | return parsedProfilers; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/test/java/com/etsy/statsd/profiler/ArgumentsTest.java: -------------------------------------------------------------------------------- 1 | package com.etsy.statsd.profiler; 2 | 3 | import com.etsy.statsd.profiler.profilers.CPUTracingProfiler; 4 | import com.etsy.statsd.profiler.profilers.MemoryProfiler; 5 | import com.etsy.statsd.profiler.reporter.InfluxDBReporter; 6 | import com.etsy.statsd.profiler.reporter.StatsDReporter; 7 | import org.junit.Rule; 8 | import org.junit.Test; 9 | import org.junit.rules.ExpectedException; 10 | 11 | import java.util.HashSet; 12 | import java.util.Set; 13 | 14 | import static org.junit.Assert.assertEquals; 15 | import static org.junit.Assert.assertFalse; 16 | import static org.junit.Assert.assertTrue; 17 | 18 | public class ArgumentsTest { 19 | @Rule 20 | public ExpectedException exception = ExpectedException.none(); 21 | 22 | @Test 23 | public void testInvalidArgument() { 24 | String args = "key=value,key2"; 25 | 26 | exception.expect(IllegalArgumentException.class); 27 | Arguments.parseArgs(args); 28 | } 29 | 30 | @Test 31 | public void testMissingRequiredArgument() { 32 | String args = "server=localhost,prefix=prefix"; 33 | 34 | exception.expect(IllegalArgumentException.class); 35 | Arguments.parseArgs(args); 36 | } 37 | 38 | @Test 39 | public void testNonNumericPort() { 40 | String args = "server=localhost,port=abcd"; 41 | 42 | exception.expect(NumberFormatException.class); 43 | Arguments.parseArgs(args); 44 | } 45 | 46 | @Test 47 | public void testNoOptionalArguments() { 48 | String args = "server=localhost,port=8125"; 49 | Arguments arguments = Arguments.parseArgs(args); 50 | 51 | assertEquals("localhost", arguments.server); 52 | assertEquals(8125, arguments.port); 53 | assertEquals("statsd-jvm-profiler", arguments.metricsPrefix); 54 | } 55 | 56 | @Test 57 | public void testOptionalArguments() { 58 | String args = "server=localhost,port=8125,prefix=i.am.a.prefix,packageWhitelist=com.etsy"; 59 | Arguments arguments = Arguments.parseArgs(args); 60 | 61 | assertEquals("localhost", arguments.server); 62 | assertEquals(8125, arguments.port); 63 | assertEquals("i.am.a.prefix", arguments.metricsPrefix); 64 | } 65 | 66 | @Test 67 | public void testDefaultProfilers() { 68 | String args = "server=localhost,port=8125"; 69 | Arguments arguments = Arguments.parseArgs(args); 70 | 71 | Set> expected = new HashSet<>(); 72 | expected.add(CPUTracingProfiler.class); 73 | expected.add(MemoryProfiler.class); 74 | 75 | assertEquals(expected, arguments.profilers); 76 | } 77 | 78 | @Test 79 | public void testProfilerWithPackage() { 80 | String args = "server=localhost,port=8125,profilers=com.etsy.statsd.profiler.profilers.CPUTracingProfiler"; 81 | Arguments arguments = Arguments.parseArgs(args); 82 | 83 | Set> expected = new HashSet<>(); 84 | expected.add(CPUTracingProfiler.class); 85 | 86 | assertEquals(expected, arguments.profilers); 87 | } 88 | 89 | @Test 90 | public void testProfilerWithoutPackage() { 91 | String args = "server=localhost,port=8125,profilers=MemoryProfiler"; 92 | Arguments arguments = Arguments.parseArgs(args); 93 | 94 | Set> expected = new HashSet<>(); 95 | expected.add(MemoryProfiler.class); 96 | 97 | assertEquals(expected, arguments.profilers); 98 | } 99 | 100 | @Test 101 | public void testMultipleProfilers() { 102 | String args = "server=localhost,port=8125,profilers=CPUTracingProfiler:MemoryProfiler"; 103 | Arguments arguments = Arguments.parseArgs(args); 104 | 105 | Set> expected = new HashSet<>(); 106 | expected.add(CPUTracingProfiler.class); 107 | expected.add(MemoryProfiler.class); 108 | 109 | assertEquals(expected, arguments.profilers); 110 | } 111 | 112 | @Test 113 | public void testProfilerNotFound() { 114 | String args = "server=localhost,port=8125,profilers=FakeProfiler"; 115 | 116 | exception.expect(IllegalArgumentException.class); 117 | Arguments.parseArgs(args); 118 | } 119 | 120 | @Test 121 | public void testReporterWithoutPackage() { 122 | String args = "server=localhost,port=8125,reporter=InfluxDBReporter"; 123 | Arguments arguments = Arguments.parseArgs(args); 124 | 125 | assertEquals(InfluxDBReporter.class, arguments.reporter); 126 | } 127 | 128 | @Test 129 | public void testReporterWithPackage() { 130 | String args = "server=localhost,port=8125,reporter=com.etsy.statsd.profiler.reporter.InfluxDBReporter"; 131 | Arguments arguments = Arguments.parseArgs(args); 132 | 133 | assertEquals(InfluxDBReporter.class, arguments.reporter); 134 | } 135 | 136 | @Test(expected = IllegalArgumentException.class) 137 | public void testReporterNotFound() { 138 | String args = "server=localhost,port=8125,reporter=NotRealReporter"; 139 | Arguments.parseArgs(args); 140 | } 141 | 142 | @Test 143 | public void testDefaultReporter() { 144 | String args = "server=localhost,port=8125"; 145 | Arguments arguments = Arguments.parseArgs(args); 146 | 147 | assertEquals(StatsDReporter.class, arguments.reporter); 148 | } 149 | 150 | @Test 151 | public void testHttpServerEnabledByDefault() throws Exception { 152 | String args = "server=localhost,port=8125"; 153 | Arguments arguments = Arguments.parseArgs(args); 154 | 155 | assertTrue(arguments.httpServerEnabled); 156 | } 157 | 158 | @Test 159 | public void testHttpServerDisabled() throws Exception { 160 | String args = "server=localhost,port=8125,httpServerEnabled=false"; 161 | Arguments arguments = Arguments.parseArgs(args); 162 | 163 | assertFalse(arguments.httpServerEnabled); 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/main/java/com/etsy/statsd/profiler/server/RequestHandler.java: -------------------------------------------------------------------------------- 1 | package com.etsy.statsd.profiler.server; 2 | 3 | import com.etsy.statsd.profiler.Profiler; 4 | import com.google.common.base.Function; 5 | import com.google.common.base.Joiner; 6 | import com.google.common.base.Predicate; 7 | import com.google.common.collect.Collections2; 8 | import org.vertx.java.core.Handler; 9 | import org.vertx.java.core.http.HttpServerRequest; 10 | import org.vertx.java.core.http.RouteMatcher; 11 | 12 | import java.util.*; 13 | import java.util.concurrent.ScheduledFuture; 14 | import java.util.concurrent.atomic.AtomicReference; 15 | 16 | /** 17 | * Handler for HTTP requests to the profiler server 18 | * 19 | * @author Andrew Johnson 20 | */ 21 | public final class RequestHandler { 22 | private RequestHandler() { } 23 | 24 | /** 25 | * Construct a RouteMatcher for the supported routes 26 | * 27 | * @param activeProfilers The active profilers 28 | * @return A RouteMatcher that matches all supported routes 29 | */ 30 | public static RouteMatcher getMatcher(final Map> runningProfilers, Map activeProfilers, AtomicReference isRunning, List errors) { 31 | RouteMatcher matcher = new RouteMatcher(); 32 | matcher.get("/profilers", RequestHandler.handleGetProfilers(runningProfilers)); 33 | matcher.get("/disable/:profiler", RequestHandler.handleDisableProfiler(runningProfilers)); 34 | matcher.get("/status/profiler/:profiler", RequestHandler.handleProfilerStatus(activeProfilers)); 35 | matcher.get("/errors", RequestHandler.handleErrorMessages(errors)); 36 | matcher.get("/isRunning", RequestHandler.isRunning(isRunning)); 37 | return matcher; 38 | } 39 | 40 | /** 41 | * Handle a GET to /isRunning 42 | * 43 | * @return A Handler that returns all running profilers 44 | */ 45 | public static Handler isRunning(final AtomicReference isRunning) { 46 | return new Handler() { 47 | @Override 48 | public void handle(HttpServerRequest httpServerRequest) { 49 | httpServerRequest.response().end(String.format("isRunning: %b", isRunning.get())); 50 | } 51 | }; 52 | } 53 | 54 | /** 55 | * Handle a GET to /profilers 56 | * 57 | * @return A Handler that handles a request to the /profilers endpoint 58 | */ 59 | public static Handler handleGetProfilers(final Map> runningProfilers) { 60 | return new Handler() { 61 | @Override 62 | public void handle(HttpServerRequest httpServerRequest) { 63 | httpServerRequest.response().end(Joiner.on("\n").join(getEnabledProfilers(runningProfilers))); 64 | } 65 | }; 66 | } 67 | 68 | /** 69 | * Handle a GET to /errors 70 | * 71 | * @return The last 10 error stacktraces 72 | */ 73 | public static Handler handleErrorMessages(final List errors) { 74 | return new Handler() { 75 | @Override 76 | public void handle(HttpServerRequest httpServerRequest) { 77 | httpServerRequest.response().end("Errors: " + Joiner.on("\n").join(errors)); 78 | } 79 | }; 80 | } 81 | 82 | /** 83 | * Handle a GET to /disable/:profiler 84 | * 85 | * @param activeProfilers The active profilers 86 | * @return A Handler that handles a request to the /disable/:profiler endpoint 87 | */ 88 | public static Handler handleDisableProfiler(final Map> activeProfilers) { 89 | return new Handler() { 90 | @Override 91 | public void handle(HttpServerRequest httpServerRequest) { 92 | String profilerToDisable = httpServerRequest.params().get("profiler"); 93 | ScheduledFuture future = activeProfilers.get(profilerToDisable); 94 | future.cancel(false); 95 | httpServerRequest.response().end(String.format("Disabled profiler %s", profilerToDisable)); 96 | } 97 | }; 98 | } 99 | 100 | /** 101 | * Handle a GET to /status/profiler/:profiler 102 | * 103 | * @param activeProfilers The active profilers 104 | * @return A Handler that handles a request to the /disable/:profiler endpoint 105 | */ 106 | public static Handler handleProfilerStatus(final Map activeProfilers) { 107 | return new Handler() { 108 | @Override 109 | public void handle(HttpServerRequest httpServerRequest) { 110 | String profilerName = httpServerRequest.params().get("profiler"); 111 | Profiler profiler = activeProfilers.get(profilerName); 112 | httpServerRequest.response().end(String.format("Recorded stats %d\n", profiler.getRecordedStats())); 113 | } 114 | }; 115 | } 116 | 117 | /** 118 | * Get all enabled profilers 119 | * @param activeProfilers The active profilers 120 | * @return A sorted List containing the names of profilers that are currently running 121 | */ 122 | private static List getEnabledProfilers(final Map> activeProfilers) { 123 | Collection profilers = Collections2.transform(Collections2.filter(activeProfilers.entrySet(), new Predicate>>() { 124 | @Override 125 | public boolean apply(Map.Entry> input) { 126 | return !input.getValue().isDone(); 127 | } 128 | }), new Function>, String>() { 129 | @Override 130 | public String apply(Map.Entry> input) { 131 | return input.getKey(); 132 | } 133 | }); 134 | 135 | List result = new ArrayList<>(profilers); 136 | Collections.sort(result); 137 | return result; 138 | 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/main/java/com/etsy/statsd/profiler/profilers/MemoryProfiler.java: -------------------------------------------------------------------------------- 1 | package com.etsy.statsd.profiler.profilers; 2 | 3 | import com.etsy.statsd.profiler.Arguments; 4 | import com.etsy.statsd.profiler.Profiler; 5 | import com.etsy.statsd.profiler.reporter.Reporter; 6 | import com.google.common.collect.Maps; 7 | 8 | import java.io.BufferedReader; 9 | import java.io.IOException; 10 | import java.io.InputStreamReader; 11 | import java.lang.management.*; 12 | import java.util.HashMap; 13 | import java.util.List; 14 | import java.util.Map; 15 | import java.util.concurrent.TimeUnit; 16 | import java.util.concurrent.atomic.AtomicLong; 17 | import java.util.logging.Logger; 18 | 19 | /** 20 | * Profiles memory usage and GC statistics 21 | * 22 | * @author Andrew Johnson 23 | */ 24 | public class MemoryProfiler extends Profiler { 25 | private static final Logger LOGGER = Logger.getLogger(MemoryProfiler.class.getName()); 26 | 27 | private static final long PERIOD = 10; 28 | 29 | private final MemoryMXBean memoryMXBean; 30 | private final List gcMXBeans; 31 | private final HashMap gcTimes = new HashMap<>(); 32 | private final ClassLoadingMXBean classLoadingMXBean; 33 | private final List memoryPoolMXBeans; 34 | private final Integer pid; 35 | 36 | public MemoryProfiler(Reporter reporter, Arguments arguments) { 37 | super(reporter, arguments); 38 | memoryMXBean = ManagementFactory.getMemoryMXBean(); 39 | gcMXBeans = ManagementFactory.getGarbageCollectorMXBeans(); 40 | classLoadingMXBean = ManagementFactory.getClassLoadingMXBean(); 41 | memoryPoolMXBeans = ManagementFactory.getMemoryPoolMXBeans(); 42 | 43 | for (GarbageCollectorMXBean b : gcMXBeans) { 44 | gcTimes.put(b, new AtomicLong()); 45 | } 46 | 47 | String processName = ManagementFactory.getRuntimeMXBean().getName(); 48 | pid = Integer.parseInt(processName.split("@")[0]); 49 | LOGGER.info("Process id: " + pid); 50 | } 51 | 52 | /** 53 | * Profile memory usage and GC statistics 54 | */ 55 | @Override 56 | public void profile() { 57 | recordStats(); 58 | } 59 | 60 | @Override 61 | public void flushData() { 62 | recordStats(); 63 | } 64 | 65 | @Override 66 | public long getPeriod() { 67 | return PERIOD; 68 | } 69 | 70 | @Override 71 | public TimeUnit getTimeUnit() { 72 | return TimeUnit.SECONDS; 73 | } 74 | 75 | @Override 76 | protected void handleArguments(Arguments arguments) { /* No arguments needed */ } 77 | 78 | /** 79 | * Records all memory statistics 80 | */ 81 | private void recordStats() { 82 | long finalizationPendingCount = memoryMXBean.getObjectPendingFinalizationCount(); 83 | MemoryUsage heap = memoryMXBean.getHeapMemoryUsage(); 84 | MemoryUsage nonHeap = memoryMXBean.getNonHeapMemoryUsage(); 85 | Map metrics = Maps.newHashMap(); 86 | 87 | metrics.put("pending-finalization-count", finalizationPendingCount); 88 | recordMemoryUsage("heap.total", heap, metrics); 89 | recordMemoryUsage("nonheap.total", nonHeap, metrics); 90 | 91 | for (GarbageCollectorMXBean gcMXBean : gcMXBeans) { 92 | String gcName = gcMXBean.getName().replace(" ", "_"); 93 | metrics.put("gc." + gcName + ".count", gcMXBean.getCollectionCount()); 94 | 95 | final long time = gcMXBean.getCollectionTime(); 96 | final long prevTime = gcTimes.get(gcMXBean).get(); 97 | final long runtime = time - prevTime; 98 | 99 | metrics.put("gc." + gcName + ".time", time); 100 | metrics.put("gc." + gcName + ".runtime", runtime); 101 | 102 | if (runtime > 0) { 103 | gcTimes.get(gcMXBean).set(time); 104 | } 105 | } 106 | 107 | long loadedClassCount = classLoadingMXBean.getLoadedClassCount(); 108 | long totalLoadedClassCount = classLoadingMXBean.getTotalLoadedClassCount(); 109 | long unloadedClassCount = classLoadingMXBean.getUnloadedClassCount(); 110 | 111 | metrics.put("loaded-class-count", loadedClassCount); 112 | metrics.put("total-loaded-class-count", totalLoadedClassCount); 113 | metrics.put("unloaded-class-count", unloadedClassCount); 114 | 115 | for (MemoryPoolMXBean memoryPoolMXBean: memoryPoolMXBeans) { 116 | String type = poolTypeToMetricName(memoryPoolMXBean.getType()); 117 | String name = poolNameToMetricName(memoryPoolMXBean.getName()); 118 | String prefix = type + '.' + name; 119 | MemoryUsage usage = memoryPoolMXBean.getUsage(); 120 | 121 | recordMemoryUsage(prefix, usage, metrics); 122 | } 123 | 124 | try { 125 | Process process = Runtime.getRuntime().exec("cat /proc/" + pid + "/status"); 126 | process.waitFor(); 127 | BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); 128 | String line; 129 | while ((line = reader.readLine()) != null) { 130 | if (line.startsWith("VmRSS:")) { 131 | metrics.put("process.rss", getRssMemory(line)); 132 | break; 133 | } 134 | } 135 | } catch (IOException | InterruptedException e) { 136 | LOGGER.severe(e.getMessage()); 137 | } 138 | 139 | recordGaugeValues(metrics); 140 | } 141 | 142 | static long getRssMemory(String line) { 143 | return 1024 * Long.parseLong( 144 | line.replace("VmRSS:", "").replace(" kB", "").replace(" ", "").replace("\t", "") 145 | ); 146 | } 147 | 148 | /** 149 | * Records memory usage 150 | * 151 | * @param prefix The prefix to use for this object 152 | * @param memory The MemoryUsage object containing the memory usage info 153 | */ 154 | private static void recordMemoryUsage(String prefix, MemoryUsage memory, Map metrics) { 155 | metrics.put(prefix + ".init", memory.getInit()); 156 | metrics.put(prefix + ".used", memory.getUsed()); 157 | metrics.put(prefix + ".committed", memory.getCommitted()); 158 | metrics.put(prefix + ".max", memory.getMax()); 159 | } 160 | 161 | /** 162 | * Formats a MemoryType into a valid metric name 163 | * 164 | * @param memoryType a MemoryType 165 | * @return a valid metric name 166 | */ 167 | private static String poolTypeToMetricName(MemoryType memoryType) { 168 | switch (memoryType) { 169 | case HEAP: 170 | return "heap"; 171 | case NON_HEAP: 172 | return "nonheap"; 173 | default: 174 | return "unknown"; 175 | } 176 | } 177 | 178 | /** 179 | * Formats a pool name into a valid metric name 180 | * 181 | * @param poolName a pool name 182 | * @return a valid metric name 183 | */ 184 | private static String poolNameToMetricName(String poolName) { 185 | return poolName.toLowerCase().replaceAll("\\s+", "-"); 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | 5 | com.datadoghq 6 | spark-jvm-profiler 7 | 1.0.0-SNAPSHOT 8 | jar 9 | 10 | spark-jvm-profiler 11 | Simple JVM profiler using StatsD 12 | https://github.com/datadoghq/spark-jvm-profiler 13 | 14 | 15 | 16 | MIT License 17 | http://opensource.org/licenses/MIT 18 | repo 19 | 20 | 21 | 22 | 23 | git@github.com:datadog/spark-jvm-profiler.git 24 | scm:git:git@github.com:datadog/spark-jvm-profiler.git 25 | 26 | 27 | 28 | 29 | ajsquared 30 | Andrew Johnson 31 | github.com/ajsquared 32 | 33 | 34 | buryat 35 | Vadim Semenov 36 | github.com/buryat 37 | 38 | 39 | 40 | 41 | UTF-8 42 | 43 | 44 | 45 | 46 | ossrh 47 | https://oss.sonatype.org/content/repositories/snapshots 48 | 49 | 50 | 51 | 52 | 53 | sonatype 54 | https://oss.sonatype.org/content/repositories/releases 55 | 56 | 57 | 58 | 59 | 60 | com.datadoghq 61 | java-dogstatsd-client 62 | 2.6.1 63 | 64 | 65 | com.google.guava 66 | guava 67 | 18.0 68 | 69 | 70 | org.influxdb 71 | influxdb-java 72 | 2.0 73 | 74 | 75 | io.vertx 76 | vertx-core 77 | 2.1.5 78 | 79 | 80 | junit 81 | junit 82 | 4.11 83 | test 84 | 85 | 86 | org.mockito 87 | mockito-core 88 | 1.10.19 89 | test 90 | 91 | 92 | org.apache.httpcomponents 93 | httpclient 94 | 4.5.2 95 | test 96 | 97 | 98 | 99 | 100 | 101 | release 102 | 103 | 104 | 105 | org.apache.maven.plugins 106 | maven-gpg-plugin 107 | 1.5 108 | 109 | 110 | sign-artifacts 111 | verify 112 | 113 | sign 114 | 115 | 116 | 117 | 118 | true 119 | 120 | 121 | 122 | org.apache.maven.plugins 123 | maven-source-plugin 124 | 2.4 125 | 126 | 127 | attach-sources 128 | 129 | jar-no-fork 130 | 131 | 132 | 133 | 134 | 135 | org.apache.maven.plugins 136 | maven-javadoc-plugin 137 | 2.10.3 138 | 139 | 140 | attach-javadocs 141 | 142 | jar 143 | 144 | 145 | 146 | 147 | 148 | org.sonatype.plugins 149 | nexus-staging-maven-plugin 150 | 1.6.3 151 | true 152 | 153 | ossrh 154 | https://oss.sonatype.org/ 155 | true 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | org.apache.maven.plugins 167 | maven-compiler-plugin 168 | 3.3 169 | 170 | 1.7 171 | 1.7 172 | 173 | 174 | 175 | org.apache.maven.plugins 176 | maven-jar-plugin 177 | 2.6 178 | 179 | 180 | 181 | com.etsy.statsd.profiler.Agent 182 | com.etsy.statsd.profiler.Agent 183 | 184 | 185 | 186 | 187 | 188 | org.apache.maven.plugins 189 | maven-shade-plugin 190 | 2.4.1 191 | 192 | 193 | package 194 | 195 | shade 196 | 197 | 198 | true 199 | jar-with-dependencies 200 | 201 | 202 | 203 | 204 | 205 | org.apache.maven.plugins 206 | maven-checkstyle-plugin 207 | 2.17 208 | 209 | checkstyle.xml 210 | false 211 | true 212 | 213 | 214 | 215 | com.puppycrawl.tools 216 | checkstyle 217 | 6.14.1 218 | 219 | 220 | 221 | 222 | checkstyle 223 | test 224 | 225 | check 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # spark-jvm-profiler [![Build Status](https://travis-ci.org/DataDog/spark-jvm-profiler.svg)](https://travis-ci.org/DataDog/spark-jvm-profiler) 2 | 3 | spark-jvm-profiler is a JVM agent profiler that sends profiling data to StatsD. Inspired by [riemann-jvm-profiler](https://github.com/riemann/riemann-jvm-profiler), it was primarily built for profiling Hadoop jobs, but can be used with any JVM process. 4 | 5 | Read [the blog post](https://codeascraft.com/2015/01/14/introducing-statsd-jvm-profiler-a-jvm-profiler-for-hadoop/) that introduced statsd-jvm-profiler on [Code as Craft](https://codeascraft.com/), Etsy's engineering blog. 6 | 7 | Also check out [the blog post](https://codeascraft.com/2015/05/12/four-months-of-statsd-jvm-profiler-a-retrospective/) reflecting on the experience of open-sourcing the project. 8 | 9 | ## Mailing List 10 | There is a mailing list for this project at https://groups.google.com/forum/#!forum/statsd-jvm-profiler. If you have questions or suggestions for the project send them here! 11 | 12 | ## Installation 13 | 14 | You will need the statsd-jvm-profiler JAR on the machine where the JVM will be running. If you are profiling Hadoop jobs, that means the JAR will need to be on all of the datanodes. 15 | 16 | The JAR can be built with `mvn package`. You will need a relatively recent Maven (at least Maven 3). 17 | 18 | statsd-jvm-profiler is available in Maven Central: 19 | ```xml 20 | 21 | com.etsy 22 | statsd-jvm-profiler 23 | 2.0.0 24 | 25 | ``` 26 | 27 | If you would like an uberjar containing all of the dependencies instead of the standard JAR, use the `jar-with-dependencies` classifier: 28 | ```xml 29 | 30 | com.etsy 31 | statsd-jvm-profiler 32 | 2.0.0 33 | jar-with-dependencies 34 | 35 | ``` 36 | 37 | ## Usage 38 | 39 | The profiler is enabled using the JVM's `-javaagent` argument. You are required to specify at least the StatsD host and port number to use. You can also specify the prefix for metrics and a whitelist of packages to be included in the CPU profiling. Arguments can be specified like so: 40 | ``` 41 | -javaagent:/usr/etsy/statsd-jvm-profiler/statsd-jvm-profiler.jar=server=hostname,port=num 42 | ``` 43 | 44 | You should use the uberjar when starting the profiler in this manner so that all the profiler's dependencies are available. 45 | 46 | The profiler can also be loaded dynamically (after the JVM has already started), but this technique requires relying on Sun's `tools.jar`, meaning it's an implementation-specific solution that might not work for all JVMs. For more information see the [Dynamic Loading section](#dynamic-loading-of-agent). 47 | 48 | An example of setting up Cascading/Scalding jobs to use the profiler can be found in the `example` directory. 49 | 50 | ### Global Options 51 | 52 | Name | Meaning 53 | ---------------- | ------- 54 | server | The hostname to which the reporter should send data (required) 55 | port | The port number for the server to which the reporter should send data (required) 56 | prefix | The prefix for metrics (optional, defaults to statsd-jvm-profiler) 57 | packageWhitelist | Colon-delimited whitelist for packages to include (optional, defaults to include everything) 58 | packageBlacklist | Colon-delimited whitelist for packages to exclude (optional, defaults to exclude nothing) 59 | profilers | Colon-delimited list of profiler class names (optional, defaults to `CPUTracingProfiler` and `MemoryProfiler`) 60 | reporter | Class name of the reporter to use (optional, defaults to StatsDReporter) 61 | httpServerEnabled| Determines if the embedded HTTP server should be started. (optional, defaults to `true`) 62 | httpPort | The port on which to bind the embedded HTTP server (optional, defaults to 5005). If this port is already in use, the next free port will be taken. 63 | 64 | ### Embedded HTTP Server 65 | statsd-jvm-profiler embeds an HTTP server to support simple interactions with the profiler while it is in operation. 66 | You can configure the port on which this server runs with the `httpPort` option. 67 | You can disable it altogether using the `httpServerEnabled=false` argument. 68 | 69 | Endpoint | Usage 70 | --------------- | ----- 71 | /profilers | List the currently enabled profilers 72 | /isRunning | List the running profilers. This should be the same as /profilers. 73 | /disable/:profiler | Disable the profiler specified by `:profiler`. The name must match what is returned by `/profilers`. 74 | /errors | List the past 10 errors from the running profilers and reporters. 75 | /status/profiler/:profiler | Displays a status message with the number of recorded stats for the requested profiler. 76 | 77 | ### Reporters 78 | statsd-jvm-profiler supports multiple backends. StatsD is the default, but InfluxDB is also supported. You can select the backend to use by passing the `reporter` argument to the profiler; `StatsDReporter` and `InfluxDBReporter` are the supported values. 79 | 80 | Some reporters may require additional arguments. 81 | 82 | #### StatsDReporter 83 | This reporter does not have any additional arguments. 84 | 85 | #### InfluxDBReporter 86 | 87 | Name | Meaning 88 | ----------- | ------- 89 | username | The username with which to connect to InfluxDB (required) 90 | password | The password with which to connect to InfluxDB (required) 91 | database | The database to which to write metrics (required) 92 | tagMapping | A mapping of tag names from the metric prefix (optional, defaults to no mapping) 93 | 94 | ##### Tag Mapping 95 | InfluxDB 0.9 supports tagging measurements and querying based on those tags. statsd-jvm-profilers uses these tags to support richer querying of the produced data. For compatibility with other metric backends, the tags are extracted from the metric prefix. 96 | 97 | If the `tagMapping` argument is not defined, only the `prefix` tag will be added, with the value of the entire prefix. 98 | 99 | `tagMapping` should be a period-delimited set of tag names. It must have the same number of components as `prefix`, or else an exception would be thrown. Each component of `tagMapping` is the name of the tag. The component in the corresponding position of `prefix` will be the value. 100 | 101 | If you do not want to include a component of `prefix` as a tag, use the special name `SKIP` in `tagMapping` for that position. 102 | 103 | ## Profilers 104 | 105 | `statsd-jvm-profiler` offers 3 profilers: `MemoryProfiler`, `CPUTracingProfiler` and `CPULoadProfiler`. 106 | 107 | The metrics for all these profilers will prefixed with the value from the `prefix` argument or it's default value: `statsd-jvm-profiler`. 108 | 109 | You can enable specific profilers through the `profilers` argument like so: 110 | 1. Memory metrics only: `profilers=MemoryProfiler` 111 | 2. CPU Tracing metrics only: `profilers=CPUTracingProfiler` 112 | 3. JVM/System CPU load metrics only: `profilers=CPULoadProfiler` 113 | 114 | Default value: `profilers=MemoryProfiler:CPUTracingProfiler` 115 | 116 | ### Garbage Collector and Memory Profiler: `MemoryProfiler` 117 | This profiler will record: 118 | 119 | 1. Heap and non-heap memory usage 120 | 2. Number of GC pauses and GC time 121 | 122 | Assuming you use the default prefix of `statsd-jvm-profiler`, 123 | the memory usage metrics will be under `statsd-jvm-profiler.heap` and `statsd-jvm-profiler.nonheap`, 124 | the GC metrics will be under `statsd-jvm-profiler.gc`. 125 | 126 | Memory and GC metrics are reported once every 10 seconds. 127 | 128 | ### CPU Tracing Profiler: `CPUTracingProfiler` 129 | This profiler records the time spent in each function across all Threads. 130 | 131 | Assuming you use the default prefix of `statsd-jvm-profiler`, the the CPU time metrics will be under `statsd-jvm-profiler.cpu.trace`. 132 | 133 | The CPU time is sampled every millisecond, but only reported every 10 seconds. 134 | The CPU time metrics represent the total time spent in that function. 135 | 136 | Profiling a long-running process or a lot of processes simultaneously will produce a lot of data, so be careful with the 137 | capacity of your StatsD instance. The `packageWhitelist` and `packageBlacklist` arguments can be used to limit the number 138 | of functions that are reported. Any function whose stack trace contains a function in one of the whitelisted packages will be included. 139 | 140 | The `visualization` directory contains some utilities for visualizing the output of this profiler. 141 | 142 | ### JVM And System CPU Load Profiler: `CPULoadProfiler` 143 | 144 | This profiler will record the JVM's and the overall system's CPU load, if the JVM is capable of providing this information. 145 | 146 | Assuming you use the default prefix of `statsd-jvm-profiler`, the JVM CPU load metrics will be under `statsd-jvm-profiler.cpu.jvm`, 147 | and the System CPU load wil be under `statsd-jvm-profiler.cpu.system`. 148 | 149 | The reported metrics will be percentages in the range of [0, 100] with 1 decimal precision. 150 | 151 | CPU load metrics are sampled and reported once every 10 seconds. 152 | 153 | Important notes: 154 | * This Profiler is not enabled by default. To enable use the argument `profilers=CPULoadProfiler` 155 | * This Profiler relies on Sun/Oracle-specific JVM implementations that offer a JMX bean that might not be available in other JVMs. 156 | Even if you are using the right JVM, there's no guarantee this JMX bean will remain there in the future. 157 | * The minimum required JVM version that offers support for this is for Java 7. 158 | * See [com.sun.management.OperatingSystemMXBean](https://docs.oracle.com/javase/7/docs/jre/api/management/extension/com/sun/management/OperatingSystemMXBean.html#getProcessCpuLoad()) 159 | for more information. 160 | * If the JVM doesn't support the required operations, the metrics above won't be reported at all. 161 | 162 | ## Dynamic Loading of Agent 163 | 164 | 1. Make sure you have the `tools.jar` available in your classpath during compilation and runtime. This JAR is usually found in the JAVA_HOME directory under the `/lib` folder for Oracle Java installations. 165 | 2. Make sure the `jvm-profiler` JAR is available during runtime. 166 | 3. During your application boostrap process, do the following: 167 | 168 | ```scala 169 | val jarPath: String = s"$ABSOLUTE_PATH_TO/com.etsy.statsd-jvm-profiler-$VERSION.jar" 170 | val agentArgs: String = s"server=$SERVER,port=$PORT" 171 | attachJvmAgent(jarPath, agentArgs) 172 | 173 | def attachJvmAgent(profilerJarPath: String, agentArgs: String): Unit = { 174 | val nameOfRunningVM: String = java.lang.management.ManagementFactory.getRuntimeMXBean.getName 175 | val p: Integer = nameOfRunningVM.indexOf('@') 176 | val pid: String = nameOfRunningVM.substring(0, p) 177 | 178 | try { 179 | val vm: com.sun.tools.attach.VirtualMachine = com.sun.tools.attach.VirtualMachine.attach(pid) 180 | vm.loadAgent(profilerJarPath, agentArgs) 181 | vm.detach() 182 | LOGGER.info("Dynamically loaded StatsD JVM Profiler Agent..."); 183 | } catch { 184 | case e: Exception => LOGGER.warn(s"Could not dynamically load StatsD JVM Profiler Agent ($profilerJarPath)", e); 185 | } 186 | } 187 | ``` 188 | 189 | 190 | ## Contributing 191 | Contributions are highly encouraged! Check out [the contribution guidlines](https://github.com/etsy/statsd-jvm-profiler/blob/master/CONTRIBUTING.md). 192 | 193 | Any ideas you have are welcome, but check out [some ideas](https://github.com/etsy/statsd-jvm-profiler/wiki/Contribution-Ideas) for contributions. 194 | -------------------------------------------------------------------------------- /visualization/influxdb-dashboard/public/css/bootstrap-theme.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.3.5 (http://getbootstrap.com) 3 | * Copyright 2011-2015 Twitter, Inc. 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */.btn-danger,.btn-default,.btn-info,.btn-primary,.btn-success,.btn-warning{text-shadow:0 -1px 0 rgba(0,0,0,.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 1px rgba(0,0,0,.075)}.btn-danger.active,.btn-danger:active,.btn-default.active,.btn-default:active,.btn-info.active,.btn-info:active,.btn-primary.active,.btn-primary:active,.btn-success.active,.btn-success:active,.btn-warning.active,.btn-warning:active{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn-danger.disabled,.btn-danger[disabled],.btn-default.disabled,.btn-default[disabled],.btn-info.disabled,.btn-info[disabled],.btn-primary.disabled,.btn-primary[disabled],.btn-success.disabled,.btn-success[disabled],.btn-warning.disabled,.btn-warning[disabled],fieldset[disabled] .btn-danger,fieldset[disabled] .btn-default,fieldset[disabled] .btn-info,fieldset[disabled] .btn-primary,fieldset[disabled] .btn-success,fieldset[disabled] .btn-warning{-webkit-box-shadow:none;box-shadow:none}.btn-danger .badge,.btn-default .badge,.btn-info .badge,.btn-primary .badge,.btn-success .badge,.btn-warning .badge{text-shadow:none}.btn.active,.btn:active{background-image:none}.btn-default{text-shadow:0 1px 0 #fff;background-image:-webkit-linear-gradient(top,#fff 0,#e0e0e0 100%);background-image:-o-linear-gradient(top,#fff 0,#e0e0e0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#e0e0e0));background-image:linear-gradient(to bottom,#fff 0,#e0e0e0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#dbdbdb;border-color:#ccc}.btn-default:focus,.btn-default:hover{background-color:#e0e0e0;background-position:0 -15px}.btn-default.active,.btn-default:active{background-color:#e0e0e0;border-color:#dbdbdb}.btn-default.disabled,.btn-default.disabled.active,.btn-default.disabled.focus,.btn-default.disabled:active,.btn-default.disabled:focus,.btn-default.disabled:hover,.btn-default[disabled],.btn-default[disabled].active,.btn-default[disabled].focus,.btn-default[disabled]:active,.btn-default[disabled]:focus,.btn-default[disabled]:hover,fieldset[disabled] .btn-default,fieldset[disabled] .btn-default.active,fieldset[disabled] .btn-default.focus,fieldset[disabled] .btn-default:active,fieldset[disabled] .btn-default:focus,fieldset[disabled] .btn-default:hover{background-color:#e0e0e0;background-image:none}.btn-primary{background-image:-webkit-linear-gradient(top,#337ab7 0,#265a88 100%);background-image:-o-linear-gradient(top,#337ab7 0,#265a88 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#265a88));background-image:linear-gradient(to bottom,#337ab7 0,#265a88 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff265a88', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#245580}.btn-primary:focus,.btn-primary:hover{background-color:#265a88;background-position:0 -15px}.btn-primary.active,.btn-primary:active{background-color:#265a88;border-color:#245580}.btn-primary.disabled,.btn-primary.disabled.active,.btn-primary.disabled.focus,.btn-primary.disabled:active,.btn-primary.disabled:focus,.btn-primary.disabled:hover,.btn-primary[disabled],.btn-primary[disabled].active,.btn-primary[disabled].focus,.btn-primary[disabled]:active,.btn-primary[disabled]:focus,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary,fieldset[disabled] .btn-primary.active,fieldset[disabled] .btn-primary.focus,fieldset[disabled] .btn-primary:active,fieldset[disabled] .btn-primary:focus,fieldset[disabled] .btn-primary:hover{background-color:#265a88;background-image:none}.btn-success{background-image:-webkit-linear-gradient(top,#5cb85c 0,#419641 100%);background-image:-o-linear-gradient(top,#5cb85c 0,#419641 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5cb85c),to(#419641));background-image:linear-gradient(to bottom,#5cb85c 0,#419641 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#3e8f3e}.btn-success:focus,.btn-success:hover{background-color:#419641;background-position:0 -15px}.btn-success.active,.btn-success:active{background-color:#419641;border-color:#3e8f3e}.btn-success.disabled,.btn-success.disabled.active,.btn-success.disabled.focus,.btn-success.disabled:active,.btn-success.disabled:focus,.btn-success.disabled:hover,.btn-success[disabled],.btn-success[disabled].active,.btn-success[disabled].focus,.btn-success[disabled]:active,.btn-success[disabled]:focus,.btn-success[disabled]:hover,fieldset[disabled] .btn-success,fieldset[disabled] .btn-success.active,fieldset[disabled] .btn-success.focus,fieldset[disabled] .btn-success:active,fieldset[disabled] .btn-success:focus,fieldset[disabled] .btn-success:hover{background-color:#419641;background-image:none}.btn-info{background-image:-webkit-linear-gradient(top,#5bc0de 0,#2aabd2 100%);background-image:-o-linear-gradient(top,#5bc0de 0,#2aabd2 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5bc0de),to(#2aabd2));background-image:linear-gradient(to bottom,#5bc0de 0,#2aabd2 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#28a4c9}.btn-info:focus,.btn-info:hover{background-color:#2aabd2;background-position:0 -15px}.btn-info.active,.btn-info:active{background-color:#2aabd2;border-color:#28a4c9}.btn-info.disabled,.btn-info.disabled.active,.btn-info.disabled.focus,.btn-info.disabled:active,.btn-info.disabled:focus,.btn-info.disabled:hover,.btn-info[disabled],.btn-info[disabled].active,.btn-info[disabled].focus,.btn-info[disabled]:active,.btn-info[disabled]:focus,.btn-info[disabled]:hover,fieldset[disabled] .btn-info,fieldset[disabled] .btn-info.active,fieldset[disabled] .btn-info.focus,fieldset[disabled] .btn-info:active,fieldset[disabled] .btn-info:focus,fieldset[disabled] .btn-info:hover{background-color:#2aabd2;background-image:none}.btn-warning{background-image:-webkit-linear-gradient(top,#f0ad4e 0,#eb9316 100%);background-image:-o-linear-gradient(top,#f0ad4e 0,#eb9316 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f0ad4e),to(#eb9316));background-image:linear-gradient(to bottom,#f0ad4e 0,#eb9316 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#e38d13}.btn-warning:focus,.btn-warning:hover{background-color:#eb9316;background-position:0 -15px}.btn-warning.active,.btn-warning:active{background-color:#eb9316;border-color:#e38d13}.btn-warning.disabled,.btn-warning.disabled.active,.btn-warning.disabled.focus,.btn-warning.disabled:active,.btn-warning.disabled:focus,.btn-warning.disabled:hover,.btn-warning[disabled],.btn-warning[disabled].active,.btn-warning[disabled].focus,.btn-warning[disabled]:active,.btn-warning[disabled]:focus,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning,fieldset[disabled] .btn-warning.active,fieldset[disabled] .btn-warning.focus,fieldset[disabled] .btn-warning:active,fieldset[disabled] .btn-warning:focus,fieldset[disabled] .btn-warning:hover{background-color:#eb9316;background-image:none}.btn-danger{background-image:-webkit-linear-gradient(top,#d9534f 0,#c12e2a 100%);background-image:-o-linear-gradient(top,#d9534f 0,#c12e2a 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9534f),to(#c12e2a));background-image:linear-gradient(to bottom,#d9534f 0,#c12e2a 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#b92c28}.btn-danger:focus,.btn-danger:hover{background-color:#c12e2a;background-position:0 -15px}.btn-danger.active,.btn-danger:active{background-color:#c12e2a;border-color:#b92c28}.btn-danger.disabled,.btn-danger.disabled.active,.btn-danger.disabled.focus,.btn-danger.disabled:active,.btn-danger.disabled:focus,.btn-danger.disabled:hover,.btn-danger[disabled],.btn-danger[disabled].active,.btn-danger[disabled].focus,.btn-danger[disabled]:active,.btn-danger[disabled]:focus,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger,fieldset[disabled] .btn-danger.active,fieldset[disabled] .btn-danger.focus,fieldset[disabled] .btn-danger:active,fieldset[disabled] .btn-danger:focus,fieldset[disabled] .btn-danger:hover{background-color:#c12e2a;background-image:none}.img-thumbnail,.thumbnail{-webkit-box-shadow:0 1px 2px rgba(0,0,0,.075);box-shadow:0 1px 2px rgba(0,0,0,.075)}.dropdown-menu>li>a:focus,.dropdown-menu>li>a:hover{background-color:#e8e8e8;background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-o-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#e8e8e8));background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);background-repeat:repeat-x}.dropdown-menu>.active>a,.dropdown-menu>.active>a:focus,.dropdown-menu>.active>a:hover{background-color:#2e6da4;background-image:-webkit-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2e6da4));background-image:linear-gradient(to bottom,#337ab7 0,#2e6da4 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);background-repeat:repeat-x}.navbar-default{background-image:-webkit-linear-gradient(top,#fff 0,#f8f8f8 100%);background-image:-o-linear-gradient(top,#fff 0,#f8f8f8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#f8f8f8));background-image:linear-gradient(to bottom,#fff 0,#f8f8f8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-radius:4px;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 5px rgba(0,0,0,.075);box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 5px rgba(0,0,0,.075)}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.open>a{background-image:-webkit-linear-gradient(top,#dbdbdb 0,#e2e2e2 100%);background-image:-o-linear-gradient(top,#dbdbdb 0,#e2e2e2 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dbdbdb),to(#e2e2e2));background-image:linear-gradient(to bottom,#dbdbdb 0,#e2e2e2 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdbdbdb', endColorstr='#ffe2e2e2', GradientType=0);background-repeat:repeat-x;-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,.075);box-shadow:inset 0 3px 9px rgba(0,0,0,.075)}.navbar-brand,.navbar-nav>li>a{text-shadow:0 1px 0 rgba(255,255,255,.25)}.navbar-inverse{background-image:-webkit-linear-gradient(top,#3c3c3c 0,#222 100%);background-image:-o-linear-gradient(top,#3c3c3c 0,#222 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#3c3c3c),to(#222));background-image:linear-gradient(to bottom,#3c3c3c 0,#222 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-radius:4px}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.open>a{background-image:-webkit-linear-gradient(top,#080808 0,#0f0f0f 100%);background-image:-o-linear-gradient(top,#080808 0,#0f0f0f 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#080808),to(#0f0f0f));background-image:linear-gradient(to bottom,#080808 0,#0f0f0f 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff080808', endColorstr='#ff0f0f0f', GradientType=0);background-repeat:repeat-x;-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,.25);box-shadow:inset 0 3px 9px rgba(0,0,0,.25)}.navbar-inverse .navbar-brand,.navbar-inverse .navbar-nav>li>a{text-shadow:0 -1px 0 rgba(0,0,0,.25)}.navbar-fixed-bottom,.navbar-fixed-top,.navbar-static-top{border-radius:0}@media (max-width:767px){.navbar .navbar-nav .open .dropdown-menu>.active>a,.navbar .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar .navbar-nav .open .dropdown-menu>.active>a:hover{color:#fff;background-image:-webkit-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2e6da4));background-image:linear-gradient(to bottom,#337ab7 0,#2e6da4 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);background-repeat:repeat-x}}.alert{text-shadow:0 1px 0 rgba(255,255,255,.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 2px rgba(0,0,0,.05);box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 2px rgba(0,0,0,.05)}.alert-success{background-image:-webkit-linear-gradient(top,#dff0d8 0,#c8e5bc 100%);background-image:-o-linear-gradient(top,#dff0d8 0,#c8e5bc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dff0d8),to(#c8e5bc));background-image:linear-gradient(to bottom,#dff0d8 0,#c8e5bc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0);background-repeat:repeat-x;border-color:#b2dba1}.alert-info{background-image:-webkit-linear-gradient(top,#d9edf7 0,#b9def0 100%);background-image:-o-linear-gradient(top,#d9edf7 0,#b9def0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9edf7),to(#b9def0));background-image:linear-gradient(to bottom,#d9edf7 0,#b9def0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0);background-repeat:repeat-x;border-color:#9acfea}.alert-warning{background-image:-webkit-linear-gradient(top,#fcf8e3 0,#f8efc0 100%);background-image:-o-linear-gradient(top,#fcf8e3 0,#f8efc0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fcf8e3),to(#f8efc0));background-image:linear-gradient(to bottom,#fcf8e3 0,#f8efc0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0);background-repeat:repeat-x;border-color:#f5e79e}.alert-danger{background-image:-webkit-linear-gradient(top,#f2dede 0,#e7c3c3 100%);background-image:-o-linear-gradient(top,#f2dede 0,#e7c3c3 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f2dede),to(#e7c3c3));background-image:linear-gradient(to bottom,#f2dede 0,#e7c3c3 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0);background-repeat:repeat-x;border-color:#dca7a7}.progress{background-image:-webkit-linear-gradient(top,#ebebeb 0,#f5f5f5 100%);background-image:-o-linear-gradient(top,#ebebeb 0,#f5f5f5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#ebebeb),to(#f5f5f5));background-image:linear-gradient(to bottom,#ebebeb 0,#f5f5f5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0);background-repeat:repeat-x}.progress-bar{background-image:-webkit-linear-gradient(top,#337ab7 0,#286090 100%);background-image:-o-linear-gradient(top,#337ab7 0,#286090 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#286090));background-image:linear-gradient(to bottom,#337ab7 0,#286090 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff286090', GradientType=0);background-repeat:repeat-x}.progress-bar-success{background-image:-webkit-linear-gradient(top,#5cb85c 0,#449d44 100%);background-image:-o-linear-gradient(top,#5cb85c 0,#449d44 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5cb85c),to(#449d44));background-image:linear-gradient(to bottom,#5cb85c 0,#449d44 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0);background-repeat:repeat-x}.progress-bar-info{background-image:-webkit-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:-o-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5bc0de),to(#31b0d5));background-image:linear-gradient(to bottom,#5bc0de 0,#31b0d5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0);background-repeat:repeat-x}.progress-bar-warning{background-image:-webkit-linear-gradient(top,#f0ad4e 0,#ec971f 100%);background-image:-o-linear-gradient(top,#f0ad4e 0,#ec971f 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f0ad4e),to(#ec971f));background-image:linear-gradient(to bottom,#f0ad4e 0,#ec971f 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0);background-repeat:repeat-x}.progress-bar-danger{background-image:-webkit-linear-gradient(top,#d9534f 0,#c9302c 100%);background-image:-o-linear-gradient(top,#d9534f 0,#c9302c 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9534f),to(#c9302c));background-image:linear-gradient(to bottom,#d9534f 0,#c9302c 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0);background-repeat:repeat-x}.progress-bar-striped{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.list-group{border-radius:4px;-webkit-box-shadow:0 1px 2px rgba(0,0,0,.075);box-shadow:0 1px 2px rgba(0,0,0,.075)}.list-group-item.active,.list-group-item.active:focus,.list-group-item.active:hover{text-shadow:0 -1px 0 #286090;background-image:-webkit-linear-gradient(top,#337ab7 0,#2b669a 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2b669a 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2b669a));background-image:linear-gradient(to bottom,#337ab7 0,#2b669a 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2b669a', GradientType=0);background-repeat:repeat-x;border-color:#2b669a}.list-group-item.active .badge,.list-group-item.active:focus .badge,.list-group-item.active:hover .badge{text-shadow:none}.panel{-webkit-box-shadow:0 1px 2px rgba(0,0,0,.05);box-shadow:0 1px 2px rgba(0,0,0,.05)}.panel-default>.panel-heading{background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-o-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#e8e8e8));background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);background-repeat:repeat-x}.panel-primary>.panel-heading{background-image:-webkit-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2e6da4));background-image:linear-gradient(to bottom,#337ab7 0,#2e6da4 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);background-repeat:repeat-x}.panel-success>.panel-heading{background-image:-webkit-linear-gradient(top,#dff0d8 0,#d0e9c6 100%);background-image:-o-linear-gradient(top,#dff0d8 0,#d0e9c6 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dff0d8),to(#d0e9c6));background-image:linear-gradient(to bottom,#dff0d8 0,#d0e9c6 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0);background-repeat:repeat-x}.panel-info>.panel-heading{background-image:-webkit-linear-gradient(top,#d9edf7 0,#c4e3f3 100%);background-image:-o-linear-gradient(top,#d9edf7 0,#c4e3f3 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9edf7),to(#c4e3f3));background-image:linear-gradient(to bottom,#d9edf7 0,#c4e3f3 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0);background-repeat:repeat-x}.panel-warning>.panel-heading{background-image:-webkit-linear-gradient(top,#fcf8e3 0,#faf2cc 100%);background-image:-o-linear-gradient(top,#fcf8e3 0,#faf2cc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fcf8e3),to(#faf2cc));background-image:linear-gradient(to bottom,#fcf8e3 0,#faf2cc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0);background-repeat:repeat-x}.panel-danger>.panel-heading{background-image:-webkit-linear-gradient(top,#f2dede 0,#ebcccc 100%);background-image:-o-linear-gradient(top,#f2dede 0,#ebcccc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f2dede),to(#ebcccc));background-image:linear-gradient(to bottom,#f2dede 0,#ebcccc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0);background-repeat:repeat-x}.well{background-image:-webkit-linear-gradient(top,#e8e8e8 0,#f5f5f5 100%);background-image:-o-linear-gradient(top,#e8e8e8 0,#f5f5f5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#e8e8e8),to(#f5f5f5));background-image:linear-gradient(to bottom,#e8e8e8 0,#f5f5f5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0);background-repeat:repeat-x;border-color:#dcdcdc;-webkit-box-shadow:inset 0 1px 3px rgba(0,0,0,.05),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 3px rgba(0,0,0,.05),0 1px 0 rgba(255,255,255,.1)} --------------------------------------------------------------------------------