├── .circleci └── config.yml ├── .gitignore ├── Dockerfile ├── LICENSE.txt ├── README.md ├── agent ├── pom.xml └── src │ ├── main │ └── java │ │ └── com │ │ └── zegelin │ │ ├── agent │ │ └── AgentArgumentParser.java │ │ └── cassandra │ │ └── exporter │ │ ├── Agent.java │ │ ├── InternalMetadataFactory.java │ │ ├── MBeanServerInterceptorHarvester.java │ │ └── collector │ │ └── InternalGossiperMBeanMetricFamilyCollector.java │ └── test │ └── java │ └── com │ └── zegelin │ └── cassandra │ └── exporter │ └── cli │ └── HarvesterOptionsTest.java ├── bin └── generate_cert_for_test.sh ├── common ├── pom.xml └── src │ ├── main │ ├── java │ │ ├── com │ │ │ └── zegelin │ │ │ │ ├── cassandra │ │ │ │ └── exporter │ │ │ │ │ ├── CassandraMetricsUtilities.java │ │ │ │ │ ├── CassandraObjectNames.java │ │ │ │ │ ├── CollectorFunctions.java │ │ │ │ │ ├── FactoriesSupplier.java │ │ │ │ │ ├── Harvester.java │ │ │ │ │ ├── LabelEnum.java │ │ │ │ │ ├── MBeanGroupMetricFamilyCollector.java │ │ │ │ │ ├── MetadataFactory.java │ │ │ │ │ ├── MetricValueConversionFunctions.java │ │ │ │ │ ├── SamplingCounting.java │ │ │ │ │ ├── cli │ │ │ │ │ ├── HarvesterOptions.java │ │ │ │ │ └── HttpServerOptions.java │ │ │ │ │ ├── collector │ │ │ │ │ ├── CachingCollector.java │ │ │ │ │ ├── FailureDetectorMBeanMetricFamilyCollector.java │ │ │ │ │ ├── GossiperMBeanMetricFamilyCollector.java │ │ │ │ │ ├── LatencyMetricGroupSummaryCollector.java │ │ │ │ │ ├── StorageServiceMBeanMetricFamilyCollector.java │ │ │ │ │ ├── dynamic │ │ │ │ │ │ └── FunctionalMetricFamilyCollector.java │ │ │ │ │ └── jvm │ │ │ │ │ │ ├── BufferPoolMXBeanMetricFamilyCollector.java │ │ │ │ │ │ ├── GarbageCollectorMXBeanMetricFamilyCollector.java │ │ │ │ │ │ ├── MemoryPoolMXBeanMetricFamilyCollector.java │ │ │ │ │ │ ├── OperatingSystemMXBeanMetricFamilyCollector.java │ │ │ │ │ │ └── ThreadMXBeanMetricFamilyCollector.java │ │ │ │ │ └── netty │ │ │ │ │ ├── HttpHandler.java │ │ │ │ │ ├── Server.java │ │ │ │ │ ├── SuppressingExceptionHandler.java │ │ │ │ │ └── ssl │ │ │ │ │ ├── ClientAuthentication.java │ │ │ │ │ ├── ReloadWatcher.java │ │ │ │ │ ├── SslContextFactory.java │ │ │ │ │ ├── SslImplementation.java │ │ │ │ │ ├── SslMode.java │ │ │ │ │ ├── SslSupport.java │ │ │ │ │ ├── SuppressingSslExceptionHandler.java │ │ │ │ │ └── UnexpectedSslExceptionHandler.java │ │ │ │ ├── function │ │ │ │ └── FloatFloatFunction.java │ │ │ │ ├── jmx │ │ │ │ ├── DelegatingMBeanServerInterceptor.java │ │ │ │ ├── NamedObject.java │ │ │ │ └── ObjectNames.java │ │ │ │ ├── netty │ │ │ │ ├── Floats.java │ │ │ │ └── Resources.java │ │ │ │ ├── picocli │ │ │ │ ├── InetSocketAddressTypeConverter.java │ │ │ │ └── JMXServiceURLTypeConverter.java │ │ │ │ └── prometheus │ │ │ │ ├── domain │ │ │ │ ├── CounterMetricFamily.java │ │ │ │ ├── GaugeMetricFamily.java │ │ │ │ ├── HistogramMetricFamily.java │ │ │ │ ├── Interval.java │ │ │ │ ├── Labels.java │ │ │ │ ├── Metric.java │ │ │ │ ├── MetricFamily.java │ │ │ │ ├── MetricFamilyVisitor.java │ │ │ │ ├── NumericMetric.java │ │ │ │ ├── SummaryMetricFamily.java │ │ │ │ └── UntypedMetricFamily.java │ │ │ │ └── exposition │ │ │ │ ├── ExpositionSink.java │ │ │ │ ├── FormattedByteChannel.java │ │ │ │ ├── FormattedExposition.java │ │ │ │ ├── NettyExpositionSink.java │ │ │ │ ├── NioExpositionSink.java │ │ │ │ ├── json │ │ │ │ ├── JsonFormatExposition.java │ │ │ │ ├── JsonFragment.java │ │ │ │ └── JsonToken.java │ │ │ │ └── text │ │ │ │ ├── TextFormatExposition.java │ │ │ │ ├── TextFormatLabels.java │ │ │ │ └── TextFormatMetricFamilyWriter.java │ │ └── info │ │ │ └── adams │ │ │ └── ryu │ │ │ ├── RoundingMode.java │ │ │ └── RyuFloat.java │ └── resources │ │ └── com │ │ └── zegelin │ │ ├── cassandra │ │ └── exporter │ │ │ └── netty │ │ │ └── root.html │ │ └── prometheus │ │ └── exposition │ │ └── text │ │ └── banner.txt │ └── test │ ├── java │ ├── com │ │ └── zegelin │ │ │ ├── cassandra │ │ │ └── exporter │ │ │ │ └── netty │ │ │ │ ├── TestSuppressingExceptionHandler.java │ │ │ │ └── ssl │ │ │ │ ├── TestReloadWatcher.java │ │ │ │ ├── TestSslContextFactory.java │ │ │ │ ├── TestSslSupport.java │ │ │ │ ├── TestSuppressingSslExceptionHandler.java │ │ │ │ └── TestUnexpectedSslExceptionHandler.java │ │ │ └── prometheus │ │ │ ├── domain │ │ │ └── TestLabels.java │ │ │ └── exposition │ │ │ ├── TestFormattedByteChannel.java │ │ │ ├── TestNettyExpositionSink.java │ │ │ └── TestNioExpositionSink.java │ └── info │ │ └── adams │ │ └── ryu │ │ └── TestRyuFloat.java │ └── resources │ └── cert │ ├── cert.pem │ ├── key.pem │ ├── protected-key.pass │ └── protected-key.pem ├── doc └── benchmark-results.png ├── entrypoint.sh ├── github-metric-help.py ├── grafana └── instaclustr │ ├── cluster-overview.json │ ├── node-overview.json │ └── table-details.json ├── install-ccm.sh ├── pom.xml ├── standalone ├── pom.xml └── src │ └── main │ └── java │ ├── com │ └── zegelin │ │ └── cassandra │ │ └── exporter │ │ ├── Application.java │ │ ├── JMXHarvester.java │ │ ├── RemoteMetadataFactory.java │ │ └── collector │ │ └── RemoteGossiperMBeanMetricFamilyCollector.java │ └── org │ └── apache │ └── cassandra │ ├── gms │ ├── FailureDetectorMBean.java │ └── GossiperMBean.java │ ├── locator │ └── EndpointSnitchInfoMBean.java │ ├── metrics │ └── CassandraMetricsRegistry.java │ ├── package-info.java │ ├── service │ └── StorageServiceMBean.java │ └── utils │ └── EstimatedHistogram.java └── test ├── lib ├── ccm.py ├── click_helpers.py ├── dump.py ├── dump_tests.py ├── experiment.py ├── jar_utils.py ├── net.py ├── path_utils.py ├── prometheus.py ├── prometheus_tests.py └── schema.py ├── old ├── capture_dump.py ├── create_demo_cluster.py ├── debug_agent.py ├── e2e_test.py ├── e2e_test_tests.py ├── metric_dump_tool.py └── metric_dump_tool_tests.py ├── pyproject.toml ├── schema.yaml ├── setup.py ├── test_tool.py └── tools └── dump.py /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | - image: circleci/openjdk:8-jdk 6 | 7 | working_directory: ~/cassandra-operator 8 | 9 | environment: 10 | MAVEN_OPTS: -Xmx3200m 11 | 12 | steps: 13 | - checkout 14 | 15 | - restore_cache: 16 | keys: 17 | - m2-{{ checksum "pom.xml" }} 18 | - m2- 19 | 20 | - run: mvn -DoutputDirectory=/tmp/artifacts package 21 | 22 | - save_cache: 23 | paths: 24 | - ~/.m2 25 | key: m2-{{ checksum "pom.xml" }} 26 | 27 | - persist_to_workspace: 28 | root: /tmp/artifacts 29 | paths: 30 | - "*.jar" 31 | 32 | publish-github-release: 33 | docker: 34 | - image: circleci/golang:1.13 35 | steps: 36 | - attach_workspace: 37 | at: ./artifacts 38 | - run: 39 | name: "Publish Release on GitHub" 40 | command: | 41 | set -xue 42 | go get github.com/tcnksm/ghr 43 | ghr -t ${GITHUB_TOKEN} -u ${CIRCLE_PROJECT_USERNAME} -r ${CIRCLE_PROJECT_REPONAME} -c ${CIRCLE_SHA1} -delete ${CIRCLE_TAG} ./artifacts/ 44 | 45 | 46 | workflows: 47 | version: 2 48 | main: 49 | jobs: 50 | - build: 51 | filters: 52 | tags: 53 | only: /^v\d+\.\d+\.\d+$/ 54 | - publish-github-release: 55 | requires: 56 | - build 57 | filters: 58 | branches: 59 | ignore: /.*/ 60 | tags: 61 | only: /^v\d+\.\d+\.\d+$/ -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.iml 3 | .DS_Store 4 | 5 | target/ 6 | out/ 7 | 8 | dependency-reduced-pom.xml 9 | 10 | *.releaseBackup 11 | release.properties 12 | 13 | *.srl 14 | test/build 15 | *.egg-info 16 | __pycache__ 17 | 18 | bin/ 19 | lib/ 20 | pyvenv.cfg 21 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM openjdk:11-jre-slim-buster 2 | ARG EXPORTER_VERSION=0.9.10 3 | RUN mkdir -p /opt/cassandra_exporter 4 | ADD https://github.com/instaclustr/cassandra-exporter/releases/download/v${EXPORTER_VERSION}/cassandra-exporter-standalone-${EXPORTER_VERSION}.jar /opt/cassandra_exporter/cassandra_exporter.jar 5 | COPY ./entrypoint.sh /opt/cassandra_exporter/entrypoint.sh 6 | RUN chmod g+wrX,o+rX -R /opt/cassandra_exporter 7 | CMD sh /opt/cassandra_exporter/entrypoint.sh -------------------------------------------------------------------------------- /agent/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | 6 | com.zegelin.cassandra-exporter 7 | exporter-parent 8 | 0.9.15-SNAPSHOT 9 | 10 | 11 | agent 12 | 0.9.15-SNAPSHOT 13 | 14 | Cassandra Exporter Agent 15 | 16 | 17 | 18 | com.zegelin.cassandra-exporter 19 | common 20 | 21 | 22 | 23 | org.apache.cassandra 24 | cassandra-all 25 | provided 26 | 27 | 28 | org.testng 29 | testng 30 | 6.14.3 31 | test 32 | 33 | 34 | 35 | 36 | 37 | 38 | org.apache.maven.plugins 39 | maven-shade-plugin 40 | 41 | cassandra-exporter-agent-${project.version} 42 | 43 | 44 | 45 | *:* 46 | 47 | META-INF/*.SF 48 | META-INF/*.DSA 49 | META-INF/*.RSA 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | com.zegelin.cassandra.exporter.Agent 59 | ${maven.compiler.source} 60 | ${maven.compiler.target} 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | package 69 | 70 | shade 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /agent/src/main/java/com/zegelin/agent/AgentArgumentParser.java: -------------------------------------------------------------------------------- 1 | package com.zegelin.agent; 2 | 3 | import com.google.common.collect.ImmutableList; 4 | 5 | import java.util.Deque; 6 | import java.util.LinkedList; 7 | import java.util.List; 8 | 9 | public final class AgentArgumentParser { 10 | AgentArgumentParser() {} 11 | 12 | enum State { 13 | ARGUMENT, 14 | QUOTED, 15 | ESCAPED 16 | } 17 | 18 | public static List parseArguments(final String argumentsString) { 19 | if (argumentsString == null) { 20 | return ImmutableList.of(); 21 | } 22 | 23 | final Deque stateStack = new LinkedList<>(); 24 | final ImmutableList.Builder arguments = ImmutableList.builder(); 25 | final StringBuilder currentArgument = new StringBuilder(); 26 | 27 | stateStack.push(State.ARGUMENT); 28 | 29 | final char[] chars = (argumentsString + '\0').toCharArray(); 30 | 31 | for (final char c : chars) { 32 | assert stateStack.peek() != null; 33 | 34 | switch (stateStack.peek()) { 35 | case ARGUMENT: 36 | if (c == ' ' || c == ',' || c == '\0') { 37 | if (currentArgument.length() > 0) { 38 | arguments.add(currentArgument.toString()); 39 | } 40 | 41 | currentArgument.setLength(0); 42 | continue; 43 | } 44 | 45 | if (c == '\\') { 46 | stateStack.push(State.ESCAPED); 47 | continue; 48 | } 49 | 50 | if (c == '"') { 51 | stateStack.push(State.QUOTED); 52 | continue; 53 | } 54 | 55 | break; 56 | 57 | case QUOTED: 58 | if (c == '"') { 59 | stateStack.pop(); 60 | continue; 61 | } 62 | 63 | if (c == '\\') { 64 | stateStack.push(State.ESCAPED); 65 | continue; 66 | } 67 | 68 | break; 69 | 70 | case ESCAPED: 71 | stateStack.pop(); 72 | 73 | break; 74 | 75 | default: 76 | throw new IllegalStateException(); 77 | } 78 | 79 | currentArgument.append(c); 80 | } 81 | 82 | if (stateStack.peek() != State.ARGUMENT) { 83 | throw new IllegalStateException(String.format("Argument %s is invalid.", currentArgument)); 84 | } 85 | 86 | return arguments.build(); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /agent/src/main/java/com/zegelin/cassandra/exporter/Agent.java: -------------------------------------------------------------------------------- 1 | package com.zegelin.cassandra.exporter; 2 | 3 | import java.lang.instrument.Instrumentation; 4 | import java.util.List; 5 | import java.util.concurrent.Callable; 6 | 7 | import com.sun.jmx.mbeanserver.JmxMBeanServerBuilder; 8 | import com.zegelin.agent.AgentArgumentParser; 9 | import com.zegelin.cassandra.exporter.cli.HarvesterOptions; 10 | import com.zegelin.cassandra.exporter.cli.HttpServerOptions; 11 | import com.zegelin.cassandra.exporter.netty.Server; 12 | import picocli.CommandLine; 13 | import picocli.CommandLine.Command; 14 | import picocli.CommandLine.Mixin; 15 | 16 | @Command(name = "cassandra-exporter-agent", mixinStandardHelpOptions = true, sortOptions = false) 17 | public class Agent implements Callable { 18 | 19 | @Mixin 20 | private HarvesterOptions harvesterOptions; 21 | 22 | @Mixin 23 | private HttpServerOptions httpServerOptions; 24 | 25 | @Override 26 | public Void call() throws Exception { 27 | System.setProperty("javax.management.builder.initial", JmxMBeanServerBuilder.class.getCanonicalName()); 28 | 29 | final MBeanServerInterceptorHarvester harvester = new MBeanServerInterceptorHarvester(harvesterOptions); 30 | 31 | final Server server = Server.start(harvester, httpServerOptions); 32 | 33 | Runtime.getRuntime().addShutdownHook(new Thread(() -> { 34 | try { 35 | server.stop().sync(); 36 | 37 | } catch (final InterruptedException e) { 38 | throw new RuntimeException(e); 39 | } 40 | })); 41 | 42 | return null; 43 | } 44 | 45 | public static void premain(final String agentArgs, final Instrumentation instrumentation) { 46 | final List arguments = AgentArgumentParser.parseArguments(agentArgs); 47 | 48 | final CommandLine commandLine = new CommandLine(new Agent()); 49 | 50 | commandLine.setCaseInsensitiveEnumValuesAllowed(true); 51 | 52 | commandLine.parseWithHandlers( 53 | new CommandLine.RunLast(), 54 | CommandLine.defaultExceptionHandler().andExit(1), 55 | arguments.toArray(new String[]{}) 56 | ); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /agent/src/main/java/com/zegelin/cassandra/exporter/InternalMetadataFactory.java: -------------------------------------------------------------------------------- 1 | package com.zegelin.cassandra.exporter; 2 | 3 | import org.apache.cassandra.config.DatabaseDescriptor; 4 | import org.apache.cassandra.cql3.statements.schema.IndexTarget; 5 | import org.apache.cassandra.locator.IEndpointSnitch; 6 | import org.apache.cassandra.locator.InetAddressAndPort; 7 | import org.apache.cassandra.utils.FBUtilities; 8 | import org.apache.cassandra.schema.Schema; 9 | 10 | import java.net.InetAddress; 11 | import java.util.Optional; 12 | import java.util.Set; 13 | 14 | public class InternalMetadataFactory extends MetadataFactory { 15 | 16 | private static Optional getTableMetaData(final String keyspaceName, final String tableName) { 17 | return Optional.ofNullable(Schema.instance.getTableMetadata(keyspaceName, tableName)); 18 | } 19 | 20 | private static Optional getIndexMetadata(final String keyspaceName, final String indexName) { 21 | return Optional.ofNullable(Schema.instance.getIndexTableMetadataRef(keyspaceName, indexName)); 22 | } 23 | 24 | @Override 25 | public Optional indexMetadata(final String keyspaceName, final String tableName, final String indexName) { 26 | return getIndexMetadata(keyspaceName, indexName) 27 | .flatMap(tableMetadata -> tableMetadata.get().indexes.get(indexName)) 28 | .map(indexMetadata -> { 29 | final IndexMetadata.IndexType indexType = IndexMetadata.IndexType.valueOf(indexMetadata.kind.name()); 30 | final Optional className = Optional.ofNullable(indexMetadata.options.get(IndexTarget.CUSTOM_INDEX_OPTION_NAME)); 31 | return new IndexMetadata() { 32 | @Override 33 | public IndexType indexType() { 34 | return indexType; 35 | } 36 | 37 | @Override 38 | public Optional customClassName() { 39 | return className; 40 | } 41 | }; 42 | }); 43 | } 44 | 45 | @Override 46 | public Optional tableOrViewMetadata(final String keyspaceName, final String tableOrViewName) { 47 | return getTableMetaData(keyspaceName, tableOrViewName) 48 | .map(m -> new TableMetadata() { 49 | @Override 50 | public String compactionStrategyClassName() { 51 | return m.params.compaction.klass().getCanonicalName(); 52 | } 53 | 54 | @Override 55 | public boolean isView() { 56 | return m.isView(); 57 | } 58 | }); 59 | } 60 | 61 | @Override 62 | public Set keyspaces() { 63 | return Schema.instance.getKeyspaces(); 64 | } 65 | 66 | @Override 67 | public Optional endpointMetadata(final InetAddress endpoint) { 68 | final IEndpointSnitch endpointSnitch = DatabaseDescriptor.getEndpointSnitch(); 69 | 70 | return Optional.of(new EndpointMetadata() { 71 | @Override 72 | public String dataCenter() { 73 | return endpointSnitch.getDatacenter(InetAddressAndPort.getByAddress(endpoint)); 74 | } 75 | 76 | @Override 77 | public String rack() { 78 | return endpointSnitch.getRack(InetAddressAndPort.getByAddress(endpoint)); 79 | } 80 | }); 81 | } 82 | 83 | @Override 84 | public String clusterName() { 85 | return DatabaseDescriptor.getClusterName(); 86 | } 87 | 88 | @Override 89 | public InetAddress localBroadcastAddress() { 90 | return FBUtilities.getBroadcastAddressAndPort().getAddress(); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /agent/src/main/java/com/zegelin/cassandra/exporter/MBeanServerInterceptorHarvester.java: -------------------------------------------------------------------------------- 1 | package com.zegelin.cassandra.exporter; 2 | 3 | import com.sun.jmx.mbeanserver.JmxMBeanServer; 4 | import com.zegelin.cassandra.exporter.collector.InternalGossiperMBeanMetricFamilyCollector; 5 | import com.zegelin.jmx.DelegatingMBeanServerInterceptor; 6 | import com.zegelin.cassandra.exporter.cli.HarvesterOptions; 7 | 8 | import javax.management.*; 9 | import java.lang.management.ManagementFactory; 10 | 11 | class MBeanServerInterceptorHarvester extends Harvester { 12 | class MBeanServerInterceptor extends DelegatingMBeanServerInterceptor { 13 | MBeanServerInterceptor(final MBeanServer delegate) { 14 | super(delegate); 15 | } 16 | 17 | @Override 18 | public ObjectInstance registerMBean(final Object object, ObjectName name) throws InstanceAlreadyExistsException, MBeanRegistrationException, NotCompliantMBeanException { 19 | // delegate first so that any exceptions (such as InstanceAlreadyExistsException) will throw first before additional processing occurs. 20 | final ObjectInstance objectInstance = super.registerMBean(object, name); 21 | 22 | // a MBean can provide its own name upon registration 23 | name = objectInstance.getObjectName(); 24 | 25 | MBeanServerInterceptorHarvester.this.registerMBean(object, name); 26 | 27 | return objectInstance; 28 | } 29 | 30 | @Override 31 | public void unregisterMBean(final ObjectName mBeanName) throws InstanceNotFoundException, MBeanRegistrationException { 32 | try { 33 | MBeanServerInterceptorHarvester.this.unregisterMBean(mBeanName); 34 | 35 | } finally { 36 | super.unregisterMBean(mBeanName); 37 | } 38 | } 39 | } 40 | 41 | MBeanServerInterceptorHarvester(final HarvesterOptions options) { 42 | this(new InternalMetadataFactory(), options); 43 | } 44 | 45 | private MBeanServerInterceptorHarvester(final MetadataFactory metadataFactory, final HarvesterOptions options) { 46 | super(metadataFactory, options); 47 | 48 | registerPlatformMXBeans(); 49 | 50 | installMBeanServerInterceptor(); 51 | 52 | addCollectorFactory(InternalGossiperMBeanMetricFamilyCollector.factory(metadataFactory)); 53 | } 54 | 55 | 56 | private void registerPlatformMXBeans() { 57 | // the platform MXBeans get registered right at JVM startup, before the agent gets a chance to 58 | // install the interceptor. 59 | // instead, directly register the MXBeans here... 60 | ManagementFactory.getPlatformManagementInterfaces().stream() 61 | .flatMap(i -> ManagementFactory.getPlatformMXBeans(i).stream()) 62 | .distinct() 63 | .forEach(mxBean -> registerMBean(mxBean, mxBean.getObjectName())); 64 | } 65 | 66 | private void installMBeanServerInterceptor() { 67 | final JmxMBeanServer mBeanServer = (JmxMBeanServer) ManagementFactory.getPlatformMBeanServer(); 68 | 69 | final MBeanServerInterceptor interceptor = new MBeanServerInterceptor(mBeanServer.getMBeanServerInterceptor()); 70 | 71 | mBeanServer.setMBeanServerInterceptor(interceptor); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /agent/src/main/java/com/zegelin/cassandra/exporter/collector/InternalGossiperMBeanMetricFamilyCollector.java: -------------------------------------------------------------------------------- 1 | package com.zegelin.cassandra.exporter.collector; 2 | 3 | import com.google.common.collect.ImmutableSet; 4 | import com.zegelin.cassandra.exporter.MetadataFactory; 5 | import com.zegelin.prometheus.domain.Labels; 6 | import com.zegelin.prometheus.domain.NumericMetric; 7 | import org.apache.cassandra.gms.EndpointState; 8 | import org.apache.cassandra.gms.Gossiper; 9 | import org.apache.cassandra.locator.InetAddressAndPort; 10 | 11 | import java.net.InetAddress; 12 | import java.util.Map; 13 | import java.util.Set; 14 | import java.util.stream.Stream; 15 | 16 | import static com.zegelin.cassandra.exporter.CassandraObjectNames.GOSSIPER_MBEAN_NAME; 17 | import static com.zegelin.cassandra.exporter.MetricValueConversionFunctions.millisecondsToSeconds; 18 | 19 | public class InternalGossiperMBeanMetricFamilyCollector extends GossiperMBeanMetricFamilyCollector { 20 | public static Factory factory(final MetadataFactory metadataFactory) { 21 | return mBean -> { 22 | if (!GOSSIPER_MBEAN_NAME.apply(mBean.name)) 23 | return null; 24 | 25 | return new InternalGossiperMBeanMetricFamilyCollector((Gossiper) mBean.object, metadataFactory); 26 | }; 27 | }; 28 | 29 | private final Gossiper gossiper; 30 | private final MetadataFactory metadataFactory; 31 | 32 | private InternalGossiperMBeanMetricFamilyCollector(final Gossiper gossiper, final MetadataFactory metadataFactory) { 33 | this.gossiper = gossiper; 34 | this.metadataFactory = metadataFactory; 35 | } 36 | 37 | @Override 38 | protected void collect(final Stream.Builder generationNumberMetrics, final Stream.Builder downtimeMetrics, final Stream.Builder activeMetrics) { 39 | for (InetAddressAndPort endpoint : gossiper.getEndpoints()) { 40 | final InetAddress endpointAddress = endpoint.getAddress(); 41 | final EndpointState state = gossiper.getEndpointStateForEndpoint(endpoint); 42 | 43 | final Labels labels = metadataFactory.endpointLabels(endpointAddress); 44 | 45 | generationNumberMetrics.add(new NumericMetric(labels, gossiper.getCurrentGenerationNumber(endpoint))); 46 | downtimeMetrics.add(new NumericMetric(labels, millisecondsToSeconds(gossiper.getEndpointDowntime(endpoint)))); 47 | activeMetrics.add(new NumericMetric(labels, state.isAlive() ? 1 : 0)); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /agent/src/test/java/com/zegelin/cassandra/exporter/cli/HarvesterOptionsTest.java: -------------------------------------------------------------------------------- 1 | package com.zegelin.cassandra.exporter.cli; 2 | 3 | import com.google.common.collect.ImmutableSet; 4 | import com.zegelin.cassandra.exporter.Harvester; 5 | import org.testng.annotations.Test; 6 | 7 | import java.io.IOException; 8 | import java.nio.file.Files; 9 | import java.nio.file.Path; 10 | import java.util.Set; 11 | import java.util.stream.Collectors; 12 | 13 | import static org.testng.Assert.*; 14 | 15 | public class HarvesterOptionsTest { 16 | 17 | static Set exclusionStrings = ImmutableSet.of("test_collector", "test:mbean=foo"); 18 | static Set exclusions = exclusionStrings.stream() 19 | .map(Harvester.Exclusion::create) 20 | .collect(Collectors.toSet()); 21 | 22 | @org.testng.annotations.Test 23 | public void testSetExclusions() { 24 | final HarvesterOptions harvesterOptions = new HarvesterOptions(); 25 | 26 | harvesterOptions.setExclusions(exclusionStrings); 27 | 28 | assertEquals(harvesterOptions.exclusions, exclusions); 29 | } 30 | 31 | @Test 32 | public void testSetExclusionsFromFile() throws IOException { 33 | final Path tempFile = Files.createTempFile(null, null); 34 | 35 | Files.write(tempFile, exclusionStrings); 36 | 37 | final HarvesterOptions harvesterOptions = new HarvesterOptions(); 38 | 39 | harvesterOptions.setExclusions(ImmutableSet.of(String.format("@%s", tempFile))); 40 | 41 | assertEquals(harvesterOptions.exclusions, exclusions); 42 | } 43 | } -------------------------------------------------------------------------------- /bin/generate_cert_for_test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | RESOURCE_PATH="common/src/test/resources" 4 | 5 | # Generate a private key and store it both unecrypted and encrypted (password protected) 6 | # Create a self-signed certificate for the key 7 | mkdir -p ${RESOURCE_PATH}/cert 8 | rm -f ${RESOURCE_PATH}/cert/* 9 | openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -outform PEM -out ${RESOURCE_PATH}/cert/key.pem 10 | echo -n "password" > ${RESOURCE_PATH}/cert/protected-key.pass 11 | openssl pkcs8 -topk8 -v1 PBE-SHA1-RC4-128 -in ${RESOURCE_PATH}/cert/key.pem -out ${RESOURCE_PATH}/cert/protected-key.pem -passout file:${RESOURCE_PATH}/cert/protected-key.pass 12 | openssl req -x509 -new -key ${RESOURCE_PATH}/cert/key.pem -sha256 -days 10000 -out ${RESOURCE_PATH}/cert/cert.pem -subj '/CN=localhost/O=Example Company/C=SE' -nodes 13 | -------------------------------------------------------------------------------- /common/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | 6 | com.zegelin.cassandra-exporter 7 | exporter-parent 8 | 0.9.15-SNAPSHOT 9 | 10 | 11 | common 12 | 0.9.15-SNAPSHOT 13 | 14 | Cassandra Exporter Common 15 | 16 | 17 | 3.6.1 18 | 19 | 20 | 21 | 22 | 23 | io.netty 24 | netty-all 25 | 4.1.58.Final 26 | provided 27 | 28 | 29 | 30 | org.apache.cassandra 31 | cassandra-all 32 | provided 33 | 34 | 35 | 36 | info.picocli 37 | picocli 38 | ${version.picocli} 39 | compile 40 | 41 | 42 | 43 | org.testng 44 | testng 45 | 6.14.3 46 | test 47 | 48 | 49 | 50 | org.mockito 51 | mockito-core 52 | 2.28.2 53 | test 54 | 55 | 56 | 57 | org.assertj 58 | assertj-core 59 | 3.12.0 60 | test 61 | 62 | 63 | 64 | io.netty 65 | netty-tcnative-boringssl-static 66 | 2.0.36.Final 67 | test 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /common/src/main/java/com/zegelin/cassandra/exporter/CassandraObjectNames.java: -------------------------------------------------------------------------------- 1 | package com.zegelin.cassandra.exporter; 2 | 3 | import com.zegelin.jmx.ObjectNames; 4 | 5 | import javax.management.ObjectName; 6 | 7 | public final class CassandraObjectNames { 8 | public static final ObjectName GOSSIPER_MBEAN_NAME = ObjectNames.create("org.apache.cassandra.net:type=Gossiper"); 9 | public static final ObjectName FAILURE_DETECTOR_MBEAN_NAME = ObjectNames.create("org.apache.cassandra.net:type=FailureDetector"); 10 | public static final ObjectName ENDPOINT_SNITCH_INFO_MBEAN_NAME = ObjectNames.create("org.apache.cassandra.db:type=EndpointSnitchInfo"); 11 | public static final ObjectName STORAGE_SERVICE_MBEAN_NAME = ObjectNames.create("org.apache.cassandra.db:type=StorageService"); 12 | 13 | private CassandraObjectNames() {} 14 | } 15 | -------------------------------------------------------------------------------- /common/src/main/java/com/zegelin/cassandra/exporter/LabelEnum.java: -------------------------------------------------------------------------------- 1 | package com.zegelin.cassandra.exporter; 2 | 3 | import com.google.common.collect.ImmutableMap; 4 | 5 | import java.util.Map; 6 | import java.util.Set; 7 | import java.util.function.Supplier; 8 | 9 | public interface LabelEnum { 10 | String labelName(); 11 | 12 | static void addIfEnabled(final LabelEnum e, final Set enabledLabels, final ImmutableMap.Builder mapBuilder, final Supplier valueSupplier) { 13 | if (enabledLabels.contains(e)) { 14 | mapBuilder.put(e.labelName(), valueSupplier.get()); 15 | } 16 | } 17 | 18 | static void addIfEnabled(final LabelEnum e, final Set enabledLabels, final Map map, final Supplier valueSupplier) { 19 | if (enabledLabels.contains(e)) { 20 | map.put(e.labelName(), valueSupplier.get()); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /common/src/main/java/com/zegelin/cassandra/exporter/MBeanGroupMetricFamilyCollector.java: -------------------------------------------------------------------------------- 1 | package com.zegelin.cassandra.exporter; 2 | 3 | import com.zegelin.jmx.NamedObject; 4 | import com.zegelin.prometheus.domain.MetricFamily; 5 | 6 | import javax.management.ObjectName; 7 | import java.util.stream.Stream; 8 | 9 | public abstract class MBeanGroupMetricFamilyCollector { 10 | /** 11 | * @return the name of the collector. Collectors with the same name will be merged together {@see merge}. 12 | */ 13 | public String name() { 14 | return this.getClass().getCanonicalName(); 15 | } 16 | 17 | /** 18 | * Merge two {@link MBeanGroupMetricFamilyCollector}s together. 19 | * 20 | * @param other The other {@link MBeanGroupMetricFamilyCollector} to merge with. 21 | * @return a new {@link MBeanGroupMetricFamilyCollector} that is the combinator of this {@link MBeanGroupMetricFamilyCollector} and {@param other} 22 | */ 23 | public MBeanGroupMetricFamilyCollector merge(final MBeanGroupMetricFamilyCollector other) { 24 | throw new IllegalStateException(String.format("Merging of %s and %s not implemented.", this, other)); 25 | } 26 | 27 | /** 28 | * @return a new MBeanGroupMetricFamilyCollector with the named MBean removed, or null if the collector is empty. 29 | */ 30 | public MBeanGroupMetricFamilyCollector removeMBean(final ObjectName mBeanName) { 31 | return null; 32 | } 33 | 34 | /** 35 | * @return a {@link Stream} of {@link MetricFamily}s that contain the metrics collected by this collector. 36 | */ 37 | public abstract Stream collect(); 38 | 39 | 40 | protected interface Factory { 41 | /** 42 | * Create a {@link MBeanGroupMetricFamilyCollector} for the given MBean, or null if this collector 43 | * doesn't support the given MBean. 44 | * 45 | * @return the MBeanGroupMetricFamilyCollector for the given MBean, or null 46 | */ 47 | MBeanGroupMetricFamilyCollector createCollector(final NamedObject mBean); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /common/src/main/java/com/zegelin/cassandra/exporter/MetadataFactory.java: -------------------------------------------------------------------------------- 1 | package com.zegelin.cassandra.exporter; 2 | 3 | import com.google.common.cache.CacheBuilder; 4 | import com.google.common.cache.CacheLoader; 5 | import com.google.common.cache.LoadingCache; 6 | import com.google.common.collect.ImmutableMap; 7 | import com.google.common.net.InetAddresses; 8 | import com.zegelin.prometheus.domain.Labels; 9 | 10 | import java.net.InetAddress; 11 | import java.util.Optional; 12 | import java.util.Set; 13 | import java.util.concurrent.TimeUnit; 14 | 15 | public abstract class MetadataFactory { 16 | 17 | interface IndexMetadata { 18 | enum IndexType { 19 | KEYS, 20 | CUSTOM, 21 | COMPOSITES 22 | } 23 | 24 | IndexType indexType(); 25 | 26 | Optional customClassName(); 27 | } 28 | 29 | interface TableMetadata { 30 | String compactionStrategyClassName(); 31 | 32 | boolean isView(); 33 | } 34 | 35 | interface EndpointMetadata { 36 | String dataCenter(); 37 | String rack(); 38 | } 39 | 40 | private final LoadingCache endpointLabelsCache = CacheBuilder.newBuilder() 41 | .expireAfterWrite(1, TimeUnit.MINUTES) 42 | .build(new CacheLoader() { 43 | @Override 44 | public Labels load(final InetAddress key) { 45 | final ImmutableMap.Builder labelsBuilder = ImmutableMap.builder(); 46 | 47 | labelsBuilder.put("endpoint", InetAddresses.toAddrString(key)); 48 | 49 | endpointMetadata(key).ifPresent(metadata -> { 50 | labelsBuilder.put("endpoint_datacenter", metadata.dataCenter()); 51 | labelsBuilder.put("endpoint_rack", metadata.rack()); 52 | }); 53 | 54 | return new Labels(labelsBuilder.build()); 55 | } 56 | }); 57 | 58 | public abstract Optional indexMetadata(final String keyspaceName, final String tableName, final String indexName); 59 | 60 | public abstract Optional tableOrViewMetadata(final String keyspaceName, final String tableOrViewName); 61 | 62 | public abstract Set keyspaces(); 63 | 64 | public abstract Optional endpointMetadata(final InetAddress endpoint); 65 | 66 | public Labels endpointLabels(final InetAddress endpoint) { 67 | return endpointLabelsCache.getUnchecked(endpoint); 68 | } 69 | 70 | public Labels endpointLabels(final String endpoint) { 71 | return endpointLabels(InetAddresses.forString(endpoint)); 72 | } 73 | 74 | public abstract String clusterName(); 75 | 76 | public abstract InetAddress localBroadcastAddress(); 77 | } 78 | -------------------------------------------------------------------------------- /common/src/main/java/com/zegelin/cassandra/exporter/MetricValueConversionFunctions.java: -------------------------------------------------------------------------------- 1 | package com.zegelin.cassandra.exporter; 2 | 3 | import java.util.concurrent.TimeUnit; 4 | 5 | public final class MetricValueConversionFunctions { 6 | private MetricValueConversionFunctions() {} 7 | 8 | public static float neg1ToNaN(final float f) { 9 | return (f == -1 ? Float.NaN : f); 10 | } 11 | 12 | public static float percentToRatio(final float f) { 13 | return f / 100.f; 14 | } 15 | 16 | 17 | private static float MILLISECONDS_PER_SECOND = TimeUnit.SECONDS.toMillis(1); 18 | private static float MICROSECONDS_PER_SECOND = TimeUnit.SECONDS.toMicros(1); 19 | private static float NANOSECONDS_PER_SECOND = TimeUnit.SECONDS.toNanos(1); 20 | 21 | public static float millisecondsToSeconds(final float f) { 22 | return f / MILLISECONDS_PER_SECOND; 23 | } 24 | 25 | public static float microsecondsToSeconds(final float f) { 26 | return f / MICROSECONDS_PER_SECOND; 27 | } 28 | 29 | public static float nanosecondsToSeconds(final float f) { 30 | return f / NANOSECONDS_PER_SECOND; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /common/src/main/java/com/zegelin/cassandra/exporter/SamplingCounting.java: -------------------------------------------------------------------------------- 1 | package com.zegelin.cassandra.exporter; 2 | 3 | import com.zegelin.prometheus.domain.Interval; 4 | 5 | import java.util.stream.Stream; 6 | 7 | /** 8 | * Similar to {@link com.codahale.metrics.Sampling} and {@link com.codahale.metrics.Counting} 9 | * but as a concrete interface and also deals with quantiles directly, rather than {@link com.codahale.metrics.Snapshot}s. 10 | * This makes it adaptable to JMX MBeans that only expose known quantiles. 11 | */ 12 | public interface SamplingCounting { 13 | long getCount(); 14 | 15 | Iterable getIntervals(); 16 | } 17 | -------------------------------------------------------------------------------- /common/src/main/java/com/zegelin/cassandra/exporter/collector/CachingCollector.java: -------------------------------------------------------------------------------- 1 | package com.zegelin.cassandra.exporter.collector; 2 | 3 | import com.google.common.base.Supplier; 4 | import com.google.common.base.Suppliers; 5 | import com.zegelin.cassandra.exporter.MBeanGroupMetricFamilyCollector; 6 | import com.zegelin.prometheus.domain.MetricFamily; 7 | 8 | import javax.management.ObjectName; 9 | import java.util.List; 10 | import java.util.concurrent.TimeUnit; 11 | import java.util.stream.Collectors; 12 | import java.util.stream.Stream; 13 | 14 | public class CachingCollector extends MBeanGroupMetricFamilyCollector { 15 | 16 | public static Factory cache(final Factory delegateFactory, final long duration, final TimeUnit unit) { 17 | return (mBean) -> { 18 | final MBeanGroupMetricFamilyCollector collector = delegateFactory.createCollector(mBean); 19 | 20 | if (collector == null) { 21 | return null; 22 | } 23 | 24 | return new CachingCollector(collector, duration, unit); 25 | }; 26 | } 27 | 28 | 29 | private final MBeanGroupMetricFamilyCollector delegate; 30 | private final long duration; 31 | private final TimeUnit unit; 32 | 33 | private final Supplier> cachedCollect; 34 | 35 | 36 | private CachingCollector(final MBeanGroupMetricFamilyCollector delegate, final long duration, final TimeUnit unit) { 37 | this.delegate = delegate; 38 | this.duration = duration; 39 | this.unit = unit; 40 | 41 | this.cachedCollect = Suppliers.memoizeWithExpiration(() -> { 42 | return delegate.collect().map(MetricFamily::cachedCopy).collect(Collectors.toList()); 43 | }, duration, unit); 44 | } 45 | 46 | @Override 47 | public String name() { 48 | return delegate.name(); 49 | } 50 | 51 | @Override 52 | public MBeanGroupMetricFamilyCollector merge(final MBeanGroupMetricFamilyCollector rawOther) { 53 | if (!(rawOther instanceof CachingCollector)) { 54 | throw new IllegalStateException(); 55 | } 56 | 57 | final MBeanGroupMetricFamilyCollector otherDelegate = ((CachingCollector) rawOther).delegate; 58 | 59 | final MBeanGroupMetricFamilyCollector newDelegate = delegate.merge(otherDelegate); 60 | 61 | return new CachingCollector(newDelegate, duration, unit); 62 | } 63 | 64 | @Override 65 | public MBeanGroupMetricFamilyCollector removeMBean(final ObjectName mBeanName) { 66 | final MBeanGroupMetricFamilyCollector newDelegate = delegate.removeMBean(mBeanName); 67 | 68 | if (newDelegate == null) { 69 | return null; 70 | } 71 | 72 | return new CachingCollector(newDelegate, duration, unit); 73 | } 74 | 75 | @Override 76 | public Stream collect() { 77 | return cachedCollect.get().stream(); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /common/src/main/java/com/zegelin/cassandra/exporter/collector/FailureDetectorMBeanMetricFamilyCollector.java: -------------------------------------------------------------------------------- 1 | package com.zegelin.cassandra.exporter.collector; 2 | 3 | import com.zegelin.cassandra.exporter.MBeanGroupMetricFamilyCollector; 4 | import com.zegelin.cassandra.exporter.MetadataFactory; 5 | import com.zegelin.prometheus.domain.GaugeMetricFamily; 6 | import com.zegelin.prometheus.domain.Labels; 7 | import com.zegelin.prometheus.domain.MetricFamily; 8 | import com.zegelin.prometheus.domain.NumericMetric; 9 | import org.apache.cassandra.gms.FailureDetectorMBean; 10 | 11 | import javax.management.openmbean.CompositeData; 12 | import javax.management.openmbean.OpenDataException; 13 | import java.util.Collection; 14 | import java.util.stream.Stream; 15 | 16 | import static com.zegelin.cassandra.exporter.CassandraObjectNames.FAILURE_DETECTOR_MBEAN_NAME; 17 | 18 | public class FailureDetectorMBeanMetricFamilyCollector extends MBeanGroupMetricFamilyCollector { 19 | public static Factory factory(final MetadataFactory metadataFactory) { 20 | return mBean -> { 21 | if (!FAILURE_DETECTOR_MBEAN_NAME.apply(mBean.name)) 22 | return null; 23 | 24 | return new FailureDetectorMBeanMetricFamilyCollector((FailureDetectorMBean) mBean.object, metadataFactory); 25 | }; 26 | }; 27 | 28 | 29 | private final FailureDetectorMBean failureDetector; 30 | private final MetadataFactory metadataFactory; 31 | 32 | private FailureDetectorMBeanMetricFamilyCollector(final FailureDetectorMBean failureDetector, final MetadataFactory metadataFactory) { 33 | this.failureDetector = failureDetector; 34 | this.metadataFactory = metadataFactory; 35 | } 36 | 37 | @Override 38 | public Stream collect() { 39 | final Stream.Builder metricFamilyStreamBuilder = Stream.builder(); 40 | 41 | // endpoint phi 42 | // annoyingly this info is only available as CompositeData 43 | try { 44 | @SuppressWarnings("unchecked") 45 | final Collection endpointPhiValues = (Collection) failureDetector.getPhiValues().values(); 46 | 47 | final Stream phiMetricsStream = endpointPhiValues.stream().map(d -> { 48 | // the endpoint address is from InetAddress.toString() which returns "/
" 49 | final String endpoint = ((String) d.get("Endpoint")).split("/")[1]; 50 | final Labels labels = metadataFactory.endpointLabels(endpoint); 51 | 52 | return new NumericMetric(labels, ((Double) d.get("PHI")).floatValue()); 53 | }); 54 | 55 | metricFamilyStreamBuilder.add(new GaugeMetricFamily("cassandra_endpoint_phi", "level of suspicion that an endpoint might be down.", phiMetricsStream)); 56 | 57 | } catch (final OpenDataException e) { 58 | throw new RuntimeException("Unable to collect metric cassandra_endpoint_phi.", e); // TODO: throw or log? 59 | } 60 | 61 | return metricFamilyStreamBuilder.build(); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /common/src/main/java/com/zegelin/cassandra/exporter/collector/GossiperMBeanMetricFamilyCollector.java: -------------------------------------------------------------------------------- 1 | package com.zegelin.cassandra.exporter.collector; 2 | 3 | import com.zegelin.cassandra.exporter.MBeanGroupMetricFamilyCollector; 4 | import com.zegelin.prometheus.domain.CounterMetricFamily; 5 | import com.zegelin.prometheus.domain.GaugeMetricFamily; 6 | import com.zegelin.prometheus.domain.MetricFamily; 7 | import com.zegelin.prometheus.domain.NumericMetric; 8 | 9 | import java.util.stream.Stream; 10 | 11 | public abstract class GossiperMBeanMetricFamilyCollector extends MBeanGroupMetricFamilyCollector { 12 | protected abstract void collect(final Stream.Builder generationNumberMetrics, 13 | final Stream.Builder downtimeMetrics, 14 | final Stream.Builder activeMetrics); 15 | 16 | @Override 17 | public Stream collect() { 18 | final Stream.Builder generationNumberMetrics = Stream.builder(); 19 | final Stream.Builder downtimeMetrics = Stream.builder(); 20 | final Stream.Builder activeMetrics = Stream.builder(); 21 | 22 | collect(generationNumberMetrics, downtimeMetrics, activeMetrics); 23 | 24 | return Stream.of( 25 | new GaugeMetricFamily("cassandra_endpoint_generation", "Current endpoint generation number.", generationNumberMetrics.build()), 26 | new CounterMetricFamily("cassandra_endpoint_downtime_seconds_total", "Endpoint downtime (since server start).", downtimeMetrics.build()), 27 | new GaugeMetricFamily("cassandra_endpoint_active", "Endpoint activeness (0 = down, 1 = up).", activeMetrics.build()) 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /common/src/main/java/com/zegelin/cassandra/exporter/collector/dynamic/FunctionalMetricFamilyCollector.java: -------------------------------------------------------------------------------- 1 | package com.zegelin.cassandra.exporter.collector.dynamic; 2 | 3 | import com.google.common.collect.ImmutableMap; 4 | import com.google.common.collect.Maps; 5 | import com.zegelin.jmx.NamedObject; 6 | import com.zegelin.cassandra.exporter.MBeanGroupMetricFamilyCollector; 7 | import com.zegelin.prometheus.domain.Labels; 8 | import com.zegelin.prometheus.domain.MetricFamily; 9 | 10 | import javax.management.ObjectName; 11 | import java.util.HashMap; 12 | import java.util.Map; 13 | import java.util.function.Function; 14 | import java.util.stream.Stream; 15 | 16 | public class FunctionalMetricFamilyCollector extends MBeanGroupMetricFamilyCollector { 17 | private final String name, help; 18 | 19 | public interface LabeledObjectGroup { 20 | String name(); 21 | String help(); 22 | Map labeledObjects(); 23 | } 24 | 25 | public interface CollectorFunction extends Function, Stream> {} 26 | 27 | 28 | private final CollectorFunction collectorFunction; 29 | 30 | private final Map> labeledObjects; 31 | 32 | private final LabeledObjectGroup objectGroup = new LabeledObjectGroup() { 33 | @Override 34 | public String name() { 35 | return FunctionalMetricFamilyCollector.this.name; 36 | } 37 | 38 | @Override 39 | public String help() { 40 | return FunctionalMetricFamilyCollector.this.help; 41 | } 42 | 43 | @Override 44 | public Map labeledObjects() { 45 | return Maps.transformValues(FunctionalMetricFamilyCollector.this.labeledObjects, o -> o.object); 46 | } 47 | }; 48 | 49 | public FunctionalMetricFamilyCollector(final String name, final String help, 50 | final Map> labeledObjects, 51 | final CollectorFunction collectorFunction) { 52 | this.name = name; 53 | this.help = help; 54 | this.labeledObjects = ImmutableMap.copyOf(labeledObjects); 55 | this.collectorFunction = collectorFunction; 56 | } 57 | 58 | @Override 59 | public String name() { 60 | return name; 61 | } 62 | 63 | @Override 64 | public MBeanGroupMetricFamilyCollector merge(final MBeanGroupMetricFamilyCollector rawOther) { 65 | if (!(rawOther instanceof FunctionalMetricFamilyCollector)) { 66 | throw new IllegalStateException(); 67 | } 68 | 69 | final FunctionalMetricFamilyCollector other = (FunctionalMetricFamilyCollector) rawOther; 70 | 71 | final Map> newLabeledObjects = new HashMap<>(labeledObjects); 72 | for (final Map.Entry> entry : other.labeledObjects.entrySet()) { 73 | newLabeledObjects.merge(entry.getKey(), entry.getValue(), (o1, o2) -> {throw new IllegalStateException(String.format("Object %s and %s cannot be merged, yet their labels are the same.", o1, o2));}); 74 | } 75 | 76 | return new FunctionalMetricFamilyCollector<>(name, help, newLabeledObjects, collectorFunction); 77 | } 78 | 79 | @Override 80 | public MBeanGroupMetricFamilyCollector removeMBean(final ObjectName objectName) { 81 | @SuppressWarnings("ConstantConditions") // ImmutableMap values cannot be null 82 | final Map> metrics = ImmutableMap.copyOf(Maps.filterValues(this.labeledObjects, m -> !m.name.equals(objectName))); 83 | 84 | if (metrics.isEmpty()) 85 | return null; 86 | 87 | return new FunctionalMetricFamilyCollector<>(this.name, this.help, metrics, collectorFunction); 88 | } 89 | 90 | @Override 91 | public Stream collect() { 92 | return collectorFunction.apply(objectGroup); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /common/src/main/java/com/zegelin/cassandra/exporter/collector/jvm/BufferPoolMXBeanMetricFamilyCollector.java: -------------------------------------------------------------------------------- 1 | package com.zegelin.cassandra.exporter.collector.jvm; 2 | 3 | import com.google.common.collect.ImmutableMap; 4 | import com.zegelin.jmx.ObjectNames; 5 | import com.zegelin.cassandra.exporter.MBeanGroupMetricFamilyCollector; 6 | import com.zegelin.prometheus.domain.GaugeMetricFamily; 7 | import com.zegelin.prometheus.domain.Labels; 8 | import com.zegelin.prometheus.domain.MetricFamily; 9 | import com.zegelin.prometheus.domain.NumericMetric; 10 | 11 | import javax.management.ObjectName; 12 | import java.lang.management.BufferPoolMXBean; 13 | import java.util.HashMap; 14 | import java.util.Map; 15 | import java.util.stream.Stream; 16 | 17 | import static com.zegelin.cassandra.exporter.MetricValueConversionFunctions.neg1ToNaN; 18 | 19 | public class BufferPoolMXBeanMetricFamilyCollector extends MBeanGroupMetricFamilyCollector { 20 | private static final ObjectName BUFFER_POOL_MXBEAN_NAME_PATTERN = ObjectNames.create("java.nio:type=BufferPool,name=*"); 21 | 22 | public static final Factory FACTORY = mBean -> { 23 | if (!BUFFER_POOL_MXBEAN_NAME_PATTERN.apply(mBean.name)) 24 | return null; 25 | 26 | final BufferPoolMXBean bufferPoolMXBean = (BufferPoolMXBean) mBean.object; 27 | 28 | final Labels poolLabels = Labels.of("pool", bufferPoolMXBean.getName()); 29 | 30 | return new BufferPoolMXBeanMetricFamilyCollector(ImmutableMap.of(poolLabels, bufferPoolMXBean)); 31 | }; 32 | 33 | private final Map labeledBufferPoolMXBeans; 34 | 35 | private BufferPoolMXBeanMetricFamilyCollector(final Map labeledBufferPoolMXBeans) { 36 | this.labeledBufferPoolMXBeans = labeledBufferPoolMXBeans; 37 | } 38 | 39 | @Override 40 | public MBeanGroupMetricFamilyCollector merge(final MBeanGroupMetricFamilyCollector rawOther) { 41 | if (!(rawOther instanceof BufferPoolMXBeanMetricFamilyCollector)) { 42 | throw new IllegalStateException(); 43 | } 44 | 45 | final BufferPoolMXBeanMetricFamilyCollector other = (BufferPoolMXBeanMetricFamilyCollector) rawOther; 46 | 47 | final Map labeledBufferPoolMXBeans = new HashMap<>(this.labeledBufferPoolMXBeans); 48 | for (final Map.Entry entry : other.labeledBufferPoolMXBeans.entrySet()) { 49 | labeledBufferPoolMXBeans.merge(entry.getKey(), entry.getValue(), (o1, o2) -> { 50 | throw new IllegalStateException(String.format("Object %s and %s cannot be merged, yet their labels are the same.", o1, o2)); 51 | }); 52 | } 53 | 54 | return new BufferPoolMXBeanMetricFamilyCollector(labeledBufferPoolMXBeans); 55 | } 56 | 57 | @Override 58 | public Stream collect() { 59 | final Stream.Builder estimatedBuffersMetrics = Stream.builder(); 60 | final Stream.Builder totalCapacityBytesMetrics = Stream.builder(); 61 | final Stream.Builder usedBytesMetrics = Stream.builder(); 62 | 63 | for (final Map.Entry entry : labeledBufferPoolMXBeans.entrySet()) { 64 | final Labels labels = entry.getKey(); 65 | final BufferPoolMXBean bufferPoolMXBean = entry.getValue(); 66 | 67 | estimatedBuffersMetrics.add(new NumericMetric(labels, bufferPoolMXBean.getCount())); 68 | totalCapacityBytesMetrics.add(new NumericMetric(labels, bufferPoolMXBean.getTotalCapacity())); 69 | usedBytesMetrics.add(new NumericMetric(labels, neg1ToNaN(bufferPoolMXBean.getMemoryUsed()))); 70 | } 71 | 72 | return Stream.of( 73 | new GaugeMetricFamily("cassandra_jvm_nio_buffer_pool_estimated_buffers", "Estimated current number of buffers in the pool.", estimatedBuffersMetrics.build()), 74 | new GaugeMetricFamily("cassandra_jvm_nio_buffer_pool_estimated_capacity_bytes_total", "Estimated total capacity of the buffers in the pool.", totalCapacityBytesMetrics.build()), 75 | new GaugeMetricFamily("cassandra_jvm_nio_buffer_pool_estimated_used_bytes", "Estimated memory usage by the JVM for the pool.", usedBytesMetrics.build()) 76 | ); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /common/src/main/java/com/zegelin/cassandra/exporter/collector/jvm/GarbageCollectorMXBeanMetricFamilyCollector.java: -------------------------------------------------------------------------------- 1 | package com.zegelin.cassandra.exporter.collector.jvm; 2 | 3 | import com.google.common.collect.ImmutableMap; 4 | import com.sun.management.GcInfo; 5 | import com.zegelin.jmx.ObjectNames; 6 | import com.zegelin.cassandra.exporter.MBeanGroupMetricFamilyCollector; 7 | import com.zegelin.prometheus.domain.*; 8 | 9 | import javax.management.ObjectName; 10 | import java.lang.management.GarbageCollectorMXBean; 11 | import java.lang.management.ManagementFactory; 12 | import java.util.HashMap; 13 | import java.util.Map; 14 | import java.util.stream.Stream; 15 | 16 | import static com.zegelin.cassandra.exporter.MetricValueConversionFunctions.millisecondsToSeconds; 17 | import static com.zegelin.cassandra.exporter.MetricValueConversionFunctions.neg1ToNaN; 18 | 19 | public class GarbageCollectorMXBeanMetricFamilyCollector extends MBeanGroupMetricFamilyCollector { 20 | private static final ObjectName GARBAGE_COLLECTOR_MXBEAN_NAME_PATTERN = ObjectNames.create(ManagementFactory.GARBAGE_COLLECTOR_MXBEAN_DOMAIN_TYPE + ",*"); 21 | 22 | public static final Factory FACTORY = mBean -> { 23 | if (!GARBAGE_COLLECTOR_MXBEAN_NAME_PATTERN.apply(mBean.name)) 24 | return null; 25 | 26 | final GarbageCollectorMXBean garbageCollectorMXBean = (GarbageCollectorMXBean) mBean.object; 27 | 28 | final Labels collectorLabels = Labels.of("collector", garbageCollectorMXBean.getName()); 29 | 30 | return new GarbageCollectorMXBeanMetricFamilyCollector(ImmutableMap.of(collectorLabels, garbageCollectorMXBean)); 31 | }; 32 | 33 | private final Map labeledGarbageCollectorMXBeans; 34 | 35 | private GarbageCollectorMXBeanMetricFamilyCollector(final Map labeledGarbageCollectorMXBeans) { 36 | this.labeledGarbageCollectorMXBeans = labeledGarbageCollectorMXBeans; 37 | } 38 | 39 | @Override 40 | public MBeanGroupMetricFamilyCollector merge(final MBeanGroupMetricFamilyCollector rawOther) { 41 | if (!(rawOther instanceof GarbageCollectorMXBeanMetricFamilyCollector)) { 42 | throw new IllegalStateException(); 43 | } 44 | 45 | final GarbageCollectorMXBeanMetricFamilyCollector other = (GarbageCollectorMXBeanMetricFamilyCollector) rawOther; 46 | 47 | final Map labeledGarbageCollectorMXBeans = new HashMap<>(this.labeledGarbageCollectorMXBeans); 48 | for (final Map.Entry entry : other.labeledGarbageCollectorMXBeans.entrySet()) { 49 | labeledGarbageCollectorMXBeans.merge(entry.getKey(), entry.getValue(), (o1, o2) -> {throw new IllegalStateException(String.format("Object %s and %s cannot be merged, yet their labels are the same.", o1, o2));}); 50 | } 51 | 52 | return new GarbageCollectorMXBeanMetricFamilyCollector(labeledGarbageCollectorMXBeans); 53 | } 54 | 55 | @Override 56 | public Stream collect() { 57 | final Stream.Builder collectionCountMetrics = Stream.builder(); 58 | final Stream.Builder collectionDurationTotalSecondsMetrics = Stream.builder(); 59 | final Stream.Builder lastGCDurationSecondsMetrics = Stream.builder(); 60 | 61 | for (final Map.Entry entry : labeledGarbageCollectorMXBeans.entrySet()) { 62 | final Labels labels = entry.getKey(); 63 | final GarbageCollectorMXBean garbageCollectorMXBean = entry.getValue(); 64 | 65 | collectionCountMetrics.add(new NumericMetric(labels, neg1ToNaN(garbageCollectorMXBean.getCollectionCount()))); 66 | collectionDurationTotalSecondsMetrics.add(new NumericMetric(labels, millisecondsToSeconds(neg1ToNaN(garbageCollectorMXBean.getCollectionTime())))); 67 | 68 | if (garbageCollectorMXBean instanceof com.sun.management.GarbageCollectorMXBean) { 69 | final GcInfo lastGcInfo = ((com.sun.management.GarbageCollectorMXBean) garbageCollectorMXBean).getLastGcInfo(); 70 | 71 | if (lastGcInfo != null) { 72 | lastGCDurationSecondsMetrics.add(new NumericMetric(labels, millisecondsToSeconds(lastGcInfo.getDuration()))); 73 | } 74 | } 75 | } 76 | 77 | return Stream.of( 78 | new CounterMetricFamily("cassandra_jvm_gc_collection_count", "Total number of collections that have occurred (since JVM start).", collectionCountMetrics.build()), 79 | new CounterMetricFamily("cassandra_jvm_gc_estimated_collection_duration_seconds_total", "Estimated cumulative collection elapsed time (since JVM start).", collectionDurationTotalSecondsMetrics.build()), 80 | new GaugeMetricFamily("cassandra_jvm_gc_last_collection_duration_seconds", "Last collection duration.", lastGCDurationSecondsMetrics.build()) 81 | ); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /common/src/main/java/com/zegelin/cassandra/exporter/collector/jvm/MemoryPoolMXBeanMetricFamilyCollector.java: -------------------------------------------------------------------------------- 1 | package com.zegelin.cassandra.exporter.collector.jvm; 2 | 3 | import com.google.common.collect.ImmutableMap; 4 | import com.zegelin.jmx.ObjectNames; 5 | import com.zegelin.cassandra.exporter.MBeanGroupMetricFamilyCollector; 6 | import com.zegelin.prometheus.domain.GaugeMetricFamily; 7 | import com.zegelin.prometheus.domain.Labels; 8 | import com.zegelin.prometheus.domain.MetricFamily; 9 | import com.zegelin.prometheus.domain.NumericMetric; 10 | 11 | import javax.management.ObjectName; 12 | import java.lang.management.ManagementFactory; 13 | import java.lang.management.MemoryPoolMXBean; 14 | import java.lang.management.MemoryUsage; 15 | import java.util.HashMap; 16 | import java.util.Map; 17 | import java.util.stream.Stream; 18 | 19 | import static com.zegelin.cassandra.exporter.MetricValueConversionFunctions.neg1ToNaN; 20 | 21 | public class MemoryPoolMXBeanMetricFamilyCollector extends MBeanGroupMetricFamilyCollector { 22 | private static final ObjectName MEMORY_POOL_MXBEAN_NAME_PATTERN = ObjectNames.create(ManagementFactory.MEMORY_POOL_MXBEAN_DOMAIN_TYPE + ",*"); 23 | 24 | public static final Factory FACTORY = mBean -> { 25 | if (!MEMORY_POOL_MXBEAN_NAME_PATTERN.apply(mBean.name)) 26 | return null; 27 | 28 | final MemoryPoolMXBean memoryPoolMXBean = (MemoryPoolMXBean) mBean.object; 29 | 30 | final Labels poolLabels = new Labels(ImmutableMap.of( 31 | "pool", memoryPoolMXBean.getName(), 32 | "type", memoryPoolMXBean.getType().name() 33 | )); 34 | 35 | return new MemoryPoolMXBeanMetricFamilyCollector(ImmutableMap.of(poolLabels, memoryPoolMXBean)); 36 | }; 37 | 38 | private final Map labeledMemoryPoolMXBeans; 39 | 40 | private MemoryPoolMXBeanMetricFamilyCollector(final Map labeledMemoryPoolMXBeans) { 41 | this.labeledMemoryPoolMXBeans = labeledMemoryPoolMXBeans; 42 | } 43 | 44 | 45 | @Override 46 | public MBeanGroupMetricFamilyCollector merge(final MBeanGroupMetricFamilyCollector rawOther) { 47 | if (!(rawOther instanceof MemoryPoolMXBeanMetricFamilyCollector)) { 48 | throw new IllegalStateException(); 49 | } 50 | 51 | final MemoryPoolMXBeanMetricFamilyCollector other = (MemoryPoolMXBeanMetricFamilyCollector) rawOther; 52 | 53 | final Map labeledMemoryPoolMXBeans = new HashMap<>(this.labeledMemoryPoolMXBeans); 54 | for (final Map.Entry entry : other.labeledMemoryPoolMXBeans.entrySet()) { 55 | labeledMemoryPoolMXBeans.merge(entry.getKey(), entry.getValue(), (o1, o2) -> { 56 | throw new IllegalStateException(String.format("Object %s and %s cannot be merged, yet their labels are the same.", o1, o2)); 57 | }); 58 | } 59 | 60 | return new MemoryPoolMXBeanMetricFamilyCollector(labeledMemoryPoolMXBeans); 61 | } 62 | 63 | @Override 64 | public MBeanGroupMetricFamilyCollector removeMBean(final ObjectName mBeanName) { 65 | return null; 66 | } 67 | 68 | @Override 69 | public Stream collect() { 70 | final Stream.Builder initialBytesMetrics = Stream.builder(); 71 | final Stream.Builder usedBytesMetrics = Stream.builder(); 72 | final Stream.Builder committedBytesMetrics = Stream.builder(); 73 | final Stream.Builder maximumBytesMetrics = Stream.builder(); 74 | 75 | for (final Map.Entry entry : labeledMemoryPoolMXBeans.entrySet()) { 76 | final Labels labels = entry.getKey(); 77 | final MemoryPoolMXBean memoryPoolMXBean = entry.getValue(); 78 | 79 | final MemoryUsage usage = memoryPoolMXBean.getUsage(); 80 | 81 | initialBytesMetrics.add(new NumericMetric(labels, neg1ToNaN(usage.getInit()))); 82 | usedBytesMetrics.add(new NumericMetric(labels, usage.getUsed())); 83 | committedBytesMetrics.add(new NumericMetric(labels, usage.getCommitted())); 84 | maximumBytesMetrics.add(new NumericMetric(labels, neg1ToNaN(usage.getMax()))); 85 | } 86 | 87 | return Stream.of( 88 | new GaugeMetricFamily("cassandra_jvm_memory_pool_initial_bytes", "Initial size of the memory pool.", initialBytesMetrics.build()), 89 | new GaugeMetricFamily("cassandra_jvm_memory_pool_used_bytes", "Current memory pool usage.", usedBytesMetrics.build()), 90 | new GaugeMetricFamily("cassandra_jvm_memory_pool_committed_bytes", null, committedBytesMetrics.build()), 91 | new GaugeMetricFamily("cassandra_jvm_memory_pool_maximum_bytes", "Maximum size of the memory pool.", maximumBytesMetrics.build()) 92 | ); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /common/src/main/java/com/zegelin/cassandra/exporter/collector/jvm/ThreadMXBeanMetricFamilyCollector.java: -------------------------------------------------------------------------------- 1 | package com.zegelin.cassandra.exporter.collector.jvm; 2 | 3 | import com.google.common.collect.ImmutableMap; 4 | import com.zegelin.jmx.ObjectNames; 5 | import com.zegelin.cassandra.exporter.MBeanGroupMetricFamilyCollector; 6 | import com.zegelin.prometheus.domain.*; 7 | 8 | import javax.management.ObjectName; 9 | import java.lang.management.ManagementFactory; 10 | import java.lang.management.ThreadInfo; 11 | import java.lang.management.ThreadMXBean; 12 | import java.util.stream.Stream; 13 | 14 | import static com.zegelin.cassandra.exporter.MetricValueConversionFunctions.nanosecondsToSeconds; 15 | 16 | public class ThreadMXBeanMetricFamilyCollector extends MBeanGroupMetricFamilyCollector { 17 | private static final ObjectName THREAD_MXBEAN_NAME = ObjectNames.create(ManagementFactory.THREAD_MXBEAN_NAME); 18 | 19 | private static final Labels USER_THREAD_COUNT_LABELS = Labels.of("type", "user"); 20 | private static final Labels DAEMON_THREAD_COUNT_LABELS = Labels.of("type", "daemon"); 21 | 22 | public static Factory factory(final boolean perThreadTimingEnabled) { 23 | return mBean -> { 24 | if (!THREAD_MXBEAN_NAME.apply(mBean.name)) 25 | return null; 26 | 27 | return new ThreadMXBeanMetricFamilyCollector((ThreadMXBean) mBean.object, perThreadTimingEnabled); 28 | }; 29 | } 30 | 31 | private final ThreadMXBean threadMXBean; 32 | private final boolean perThreadTimingEnabled; 33 | 34 | private ThreadMXBeanMetricFamilyCollector(final ThreadMXBean threadMXBean, final boolean perThreadTimingEnabled) { 35 | this.threadMXBean = threadMXBean; 36 | this.perThreadTimingEnabled = perThreadTimingEnabled; 37 | } 38 | 39 | @Override 40 | public Stream collect() { 41 | final Stream.Builder metricFamilies = Stream.builder(); 42 | 43 | { 44 | final int threadCount = threadMXBean.getThreadCount(); 45 | final int daemonThreadCount = threadMXBean.getDaemonThreadCount(); 46 | final int userThreadCount = threadCount - daemonThreadCount; 47 | 48 | metricFamilies.add(new GaugeMetricFamily("cassandra_jvm_thread_count", "Current number of live threads.", Stream.of( 49 | new NumericMetric(USER_THREAD_COUNT_LABELS, userThreadCount), 50 | new NumericMetric(DAEMON_THREAD_COUNT_LABELS, daemonThreadCount) 51 | ))); 52 | } 53 | 54 | metricFamilies.add(new GaugeMetricFamily("cassandra_jvm_threads_started_total", "Cumulative number of started threads (since JVM start).", Stream.of(new NumericMetric(null, threadMXBean.getTotalStartedThreadCount())))); 55 | 56 | if (perThreadTimingEnabled && threadMXBean instanceof com.sun.management.ThreadMXBean && threadMXBean.isThreadCpuTimeEnabled()) { 57 | final com.sun.management.ThreadMXBean threadMXBeanEx = (com.sun.management.ThreadMXBean) threadMXBean; 58 | 59 | final long[] threadIds = threadMXBeanEx.getAllThreadIds(); 60 | final ThreadInfo[] threadInfos = threadMXBeanEx.getThreadInfo(threadIds); 61 | final long[] threadCpuTimes = threadMXBeanEx.getThreadCpuTime(threadIds); 62 | final long[] threadUserTimes = threadMXBeanEx.getThreadUserTime(threadIds); 63 | 64 | final Stream.Builder threadCpuTimeMetrics = Stream.builder(); 65 | 66 | for (int i = 0; i < threadIds.length; i++) { 67 | final long threadCpuTime = threadCpuTimes[i]; 68 | final long threadUserTime = threadUserTimes[i]; 69 | 70 | if (threadCpuTime == -1 || threadUserTime == -1) { 71 | continue; 72 | } 73 | 74 | final long threadSystemTime = threadCpuTime - threadUserTime; 75 | 76 | final Labels systemModeLabels = new Labels(ImmutableMap.of( 77 | "id", String.valueOf(threadIds[i]), 78 | "name", threadInfos[i].getThreadName(), 79 | "mode", "system" 80 | )); 81 | 82 | final Labels userModeLabels = new Labels(ImmutableMap.of( 83 | "id", String.valueOf(threadIds[i]), 84 | "name", threadInfos[i].getThreadName(), 85 | "mode", "user" 86 | )); 87 | 88 | threadCpuTimeMetrics.add(new NumericMetric(systemModeLabels, nanosecondsToSeconds(threadSystemTime))); 89 | threadCpuTimeMetrics.add(new NumericMetric(userModeLabels, nanosecondsToSeconds(threadUserTime))); 90 | } 91 | 92 | metricFamilies.add(new CounterMetricFamily("cassandra_jvm_thread_cpu_time_seconds_total", "Cumulative thread CPU time (since JVM start).", threadCpuTimeMetrics.build())); 93 | } 94 | 95 | return metricFamilies.build(); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /common/src/main/java/com/zegelin/cassandra/exporter/netty/SuppressingExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package com.zegelin.cassandra.exporter.netty; 2 | 3 | import io.netty.channel.ChannelHandlerAdapter; 4 | import io.netty.channel.ChannelHandlerContext; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | 8 | import java.io.IOException; 9 | 10 | public class SuppressingExceptionHandler extends ChannelHandlerAdapter { 11 | private static final Logger logger = LoggerFactory.getLogger(SuppressingExceptionHandler.class); 12 | 13 | @Override 14 | public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { 15 | if (brokenPipeException(cause)) { 16 | logger.debug("Exception while processing scrape request: {}", cause.getMessage()); 17 | } else { 18 | ctx.fireExceptionCaught(cause); 19 | } 20 | } 21 | 22 | private boolean brokenPipeException(Throwable cause) { 23 | return cause instanceof IOException 24 | && "Broken pipe".equals(cause.getMessage()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /common/src/main/java/com/zegelin/cassandra/exporter/netty/ssl/ClientAuthentication.java: -------------------------------------------------------------------------------- 1 | package com.zegelin.cassandra.exporter.netty.ssl; 2 | 3 | import io.netty.handler.ssl.ClientAuth; 4 | 5 | public enum ClientAuthentication { 6 | NONE(ClientAuth.NONE, false), 7 | OPTIONAL(ClientAuth.OPTIONAL, false), 8 | REQUIRE(ClientAuth.REQUIRE, false), 9 | VALIDATE(ClientAuth.REQUIRE, true); 10 | 11 | private final ClientAuth clientAuth; 12 | private final boolean hostnameValidation; 13 | 14 | ClientAuthentication(final ClientAuth clientAuth, final boolean hostnameValidation) { 15 | this.clientAuth = clientAuth; 16 | this.hostnameValidation = hostnameValidation; 17 | } 18 | 19 | ClientAuth getClientAuth() { 20 | return clientAuth; 21 | } 22 | 23 | boolean getHostnameValidation() { 24 | return hostnameValidation; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /common/src/main/java/com/zegelin/cassandra/exporter/netty/ssl/ReloadWatcher.java: -------------------------------------------------------------------------------- 1 | package com.zegelin.cassandra.exporter.netty.ssl; 2 | 3 | import com.zegelin.cassandra.exporter.cli.HttpServerOptions; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | 7 | import java.io.File; 8 | import java.util.Collection; 9 | import java.util.Objects; 10 | import java.util.concurrent.TimeUnit; 11 | import java.util.stream.Collectors; 12 | import java.util.stream.Stream; 13 | 14 | // TODO: Switch to using java.time.* classes (Instant, Duration, etc) and java.nio.Path 15 | public class ReloadWatcher { 16 | private static final Logger logger = LoggerFactory.getLogger(ReloadWatcher.class); 17 | 18 | private static final long RELOAD_MARGIN_MILLIS = 1000; 19 | private final long intervalInMs; 20 | private final Collection files; 21 | 22 | private long nextReloadAt; 23 | private long reloadedAt; 24 | 25 | public ReloadWatcher(final HttpServerOptions httpServerOptions) { 26 | intervalInMs = TimeUnit.SECONDS.toMillis(httpServerOptions.sslReloadIntervalInSeconds); 27 | files = Stream.of(httpServerOptions.sslServerKeyFile, 28 | httpServerOptions.sslServerKeyPasswordFile, 29 | httpServerOptions.sslServerCertificateFile, 30 | httpServerOptions.sslTrustedCertificateFile) 31 | .filter(Objects::nonNull) 32 | .collect(Collectors.toSet()); 33 | 34 | logger.info("Watching {} for changes every {} seconds.", this.files, httpServerOptions.sslReloadIntervalInSeconds); 35 | reset(System.currentTimeMillis()); 36 | } 37 | 38 | private void reset(final long now) { 39 | // Create a 1 second margin to compensate for poor resolution of File.lastModified() 40 | reloadedAt = now - RELOAD_MARGIN_MILLIS; 41 | 42 | nextReloadAt = now + intervalInMs; 43 | logger.debug("Next reload at {}.", nextReloadAt); 44 | } 45 | 46 | public synchronized void forceReload() { 47 | if (!enabled()) { 48 | return; 49 | } 50 | 51 | logger.info("Forced reload of exporter certificates on next scrape."); 52 | 53 | reloadedAt = 0L; 54 | nextReloadAt = 0L; 55 | } 56 | 57 | boolean needReload() { 58 | if (!enabled()) { 59 | return false; 60 | } 61 | 62 | final long now = System.currentTimeMillis(); 63 | 64 | if (timeToPoll(now)) { 65 | return reallyNeedReload(now); 66 | } 67 | 68 | return false; 69 | } 70 | 71 | private boolean enabled() { 72 | return intervalInMs > 0; 73 | } 74 | 75 | private boolean timeToPoll(final long now) { 76 | return now > nextReloadAt; 77 | } 78 | 79 | private synchronized boolean reallyNeedReload(final long now) { 80 | if (timeToPoll(now)) { 81 | try { 82 | return anyFileModified(); 83 | } finally { 84 | reset(now); 85 | } 86 | } 87 | return false; 88 | } 89 | 90 | private boolean anyFileModified() { 91 | return files.stream().anyMatch(f -> f.lastModified() > reloadedAt); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /common/src/main/java/com/zegelin/cassandra/exporter/netty/ssl/SslContextFactory.java: -------------------------------------------------------------------------------- 1 | package com.zegelin.cassandra.exporter.netty.ssl; 2 | 3 | import com.google.common.collect.Iterables; 4 | import com.google.common.io.Files; 5 | import com.zegelin.cassandra.exporter.cli.HttpServerOptions; 6 | import io.netty.handler.ssl.SslContext; 7 | import io.netty.handler.ssl.SslContextBuilder; 8 | import io.netty.handler.ssl.util.SelfSignedCertificate; 9 | import org.slf4j.Logger; 10 | import org.slf4j.LoggerFactory; 11 | 12 | import javax.net.ssl.SSLException; 13 | import java.io.IOException; 14 | import java.security.cert.CertificateException; 15 | 16 | import static com.google.common.base.Preconditions.checkArgument; 17 | import static java.nio.charset.StandardCharsets.UTF_8; 18 | 19 | public class SslContextFactory { 20 | private static final Logger logger = LoggerFactory.getLogger(SslContextFactory.class); 21 | 22 | private final HttpServerOptions httpServerOptions; 23 | 24 | public SslContextFactory(HttpServerOptions httpServerOptions) { 25 | this.httpServerOptions = httpServerOptions; 26 | } 27 | 28 | SslContext createSslContext() { 29 | final SslContextBuilder builder = getContextBuilder(); 30 | 31 | builder.sslProvider(httpServerOptions.sslImplementation.getProvider()); 32 | 33 | if (httpServerOptions.sslProtocols != null) { 34 | builder.protocols(Iterables.toArray(httpServerOptions.sslProtocols, String.class)); 35 | } 36 | 37 | builder.clientAuth(httpServerOptions.sslClientAuthentication.getClientAuth()); 38 | 39 | builder.trustManager(httpServerOptions.sslTrustedCertificateFile); 40 | 41 | builder.ciphers(httpServerOptions.sslCiphers); 42 | 43 | try { 44 | return builder.build(); 45 | 46 | } catch (final SSLException e) { 47 | throw new IllegalArgumentException("Failed to initialize an SSL context for the exporter.", e); 48 | } 49 | } 50 | 51 | private SslContextBuilder getContextBuilder() { 52 | if (hasServerKeyAndCert()) { 53 | return SslContextBuilder.forServer(httpServerOptions.sslServerCertificateFile, 54 | httpServerOptions.sslServerKeyFile, 55 | getKeyPassword()); 56 | } else { 57 | return getSelfSignedContextBuilder(); 58 | } 59 | } 60 | 61 | private boolean hasServerKeyAndCert() { 62 | if (httpServerOptions.sslServerKeyFile != null) { 63 | checkArgument(httpServerOptions.sslServerCertificateFile != null, 64 | "A server certificate must be specified together with the server key for the exporter."); 65 | return true; 66 | } 67 | 68 | checkArgument(httpServerOptions.sslServerCertificateFile == null, 69 | "A server key must be specified together with the server certificate for the exporter."); 70 | 71 | return false; 72 | } 73 | 74 | private String getKeyPassword() { 75 | if (httpServerOptions.sslServerKeyPasswordFile == null) { 76 | return null; 77 | } 78 | 79 | try { 80 | return Files.toString(httpServerOptions.sslServerKeyPasswordFile, UTF_8); 81 | 82 | } catch (final IOException e) { 83 | throw new IllegalArgumentException("Unable to read SSL server key password file for the exporter.", e); 84 | } 85 | } 86 | 87 | private SslContextBuilder getSelfSignedContextBuilder() { 88 | logger.warn("Running exporter in SSL mode with insecure self-signed certificate"); 89 | 90 | try { 91 | final SelfSignedCertificate ssc = new SelfSignedCertificate(); 92 | return SslContextBuilder.forServer(ssc.key(), ssc.cert()); 93 | 94 | } catch (final CertificateException e) { 95 | throw new IllegalArgumentException("Failed to create self-signed certificate for the exporter.", e); 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /common/src/main/java/com/zegelin/cassandra/exporter/netty/ssl/SslImplementation.java: -------------------------------------------------------------------------------- 1 | package com.zegelin.cassandra.exporter.netty.ssl; 2 | 3 | import io.netty.handler.ssl.OpenSsl; 4 | import io.netty.handler.ssl.SslProvider; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | 8 | public enum SslImplementation { 9 | OPENSSL(SslProvider.OPENSSL), 10 | JDK(SslProvider.JDK), 11 | DISCOVER(); 12 | 13 | private final SslProvider provider; 14 | 15 | SslImplementation() { 16 | provider = null; 17 | } 18 | 19 | SslImplementation(final SslProvider provider) { 20 | this.provider = provider; 21 | } 22 | 23 | SslProvider getProvider() { 24 | if (provider != null) { 25 | return provider; 26 | } else { 27 | if (OpenSsl.isAvailable()) { 28 | logger().info("Native OpenSSL library discovered for exporter: {}", OpenSsl.versionString()); 29 | return SslProvider.OPENSSL; 30 | } else { 31 | logger().info("No native OpenSSL library discovered for exporter - falling back to JDK implementation"); 32 | return SslProvider.JDK; 33 | } 34 | } 35 | } 36 | 37 | // Instead of normal static initialization which cause JVM to bail out. 38 | private Logger logger() { 39 | return LoggerFactory.getLogger(SslImplementation.class); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /common/src/main/java/com/zegelin/cassandra/exporter/netty/ssl/SslMode.java: -------------------------------------------------------------------------------- 1 | package com.zegelin.cassandra.exporter.netty.ssl; 2 | 3 | public enum SslMode { 4 | DISABLE, 5 | ENABLE, 6 | OPTIONAL 7 | } 8 | -------------------------------------------------------------------------------- /common/src/main/java/com/zegelin/cassandra/exporter/netty/ssl/SslSupport.java: -------------------------------------------------------------------------------- 1 | package com.zegelin.cassandra.exporter.netty.ssl; 2 | 3 | import com.google.common.annotations.VisibleForTesting; 4 | import com.zegelin.cassandra.exporter.cli.HttpServerOptions; 5 | import io.netty.buffer.ByteBufAllocator; 6 | import io.netty.channel.ChannelHandler; 7 | import io.netty.channel.ChannelHandlerContext; 8 | import io.netty.channel.socket.SocketChannel; 9 | import io.netty.handler.ssl.OptionalSslHandler; 10 | import io.netty.handler.ssl.SslContext; 11 | import io.netty.handler.ssl.SslHandler; 12 | import org.slf4j.Logger; 13 | import org.slf4j.LoggerFactory; 14 | 15 | import javax.net.ssl.SSLEngine; 16 | import javax.net.ssl.SSLParameters; 17 | import java.net.InetSocketAddress; 18 | import java.util.concurrent.atomic.AtomicReference; 19 | 20 | public class SslSupport { 21 | private static final Logger logger = LoggerFactory.getLogger(SslSupport.class); 22 | 23 | private final HttpServerOptions httpServerOptions; 24 | private final SslContextFactory sslContextFactory; 25 | private final ReloadWatcher reloadWatcher; 26 | private final AtomicReference sslContextRef = new AtomicReference<>(); 27 | 28 | public SslSupport(final HttpServerOptions httpServerOptions) { 29 | this.httpServerOptions = httpServerOptions; 30 | 31 | if (isEnabled()) { 32 | sslContextFactory = new SslContextFactory(httpServerOptions); 33 | reloadWatcher = new ReloadWatcher(httpServerOptions); 34 | sslContextRef.set(sslContextFactory.createSslContext()); 35 | } else { 36 | sslContextFactory = null; 37 | reloadWatcher = null; 38 | } 39 | } 40 | 41 | public void maybeAddHandler(final SocketChannel ch) { 42 | if (isEnabled()) { 43 | ch.pipeline() 44 | .addFirst(createSslHandler(ch)) 45 | .addLast(new UnexpectedSslExceptionHandler(reloadWatcher)) 46 | .addLast(new SuppressingSslExceptionHandler()); 47 | } 48 | } 49 | 50 | private boolean isEnabled() { 51 | return httpServerOptions.sslMode != SslMode.DISABLE; 52 | } 53 | 54 | private ChannelHandler createSslHandler(final SocketChannel socketChannel) { 55 | maybeReloadContext(); 56 | 57 | if (httpServerOptions.sslMode == SslMode.OPTIONAL) { 58 | if (httpServerOptions.sslClientAuthentication.getHostnameValidation()) { 59 | return new OptionalSslHandler(sslContextRef.get()) { 60 | @Override 61 | protected SslHandler newSslHandler(final ChannelHandlerContext handlerContext, final SslContext context) { 62 | return createValidatingSslHandler(context, handlerContext.alloc(), socketChannel.remoteAddress()); 63 | } 64 | }; 65 | } else { 66 | return new OptionalSslHandler(sslContextRef.get()); 67 | } 68 | } else { 69 | if (httpServerOptions.sslClientAuthentication.getHostnameValidation()) { 70 | return createValidatingSslHandler(sslContextRef.get(), socketChannel.alloc(), socketChannel.remoteAddress()); 71 | } else { 72 | return sslContextRef.get().newHandler(socketChannel.alloc()); 73 | } 74 | } 75 | } 76 | 77 | private SslHandler createValidatingSslHandler(final SslContext context, final ByteBufAllocator allocator, final InetSocketAddress peer) { 78 | final SslHandler handler = context.newHandler(allocator, peer.getHostString(), peer.getPort()); 79 | 80 | final SSLEngine engine = handler.engine(); 81 | 82 | final SSLParameters sslParameters = engine.getSSLParameters(); 83 | sslParameters.setEndpointIdentificationAlgorithm("HTTPS"); 84 | 85 | engine.setSSLParameters(sslParameters); 86 | 87 | return handler; 88 | } 89 | 90 | private void maybeReloadContext() { 91 | if (reloadWatcher.needReload()) { 92 | try { 93 | sslContextRef.set(sslContextFactory.createSslContext()); 94 | logger.info("Reloaded exporter SSL certificate"); 95 | 96 | } catch (final IllegalArgumentException e) { 97 | logger.error("Failed to reload exporter SSL certificate - Next poll in {} seconds.", httpServerOptions.sslReloadIntervalInSeconds); 98 | } 99 | } else { 100 | logger.debug("No need to reload exporter SSL certificate."); 101 | } 102 | } 103 | 104 | @VisibleForTesting 105 | SslContext getSslContext() { 106 | return sslContextRef.get(); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /common/src/main/java/com/zegelin/cassandra/exporter/netty/ssl/SuppressingSslExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package com.zegelin.cassandra.exporter.netty.ssl; 2 | 3 | import io.netty.channel.ChannelHandlerAdapter; 4 | import io.netty.channel.ChannelHandlerContext; 5 | import io.netty.handler.codec.DecoderException; 6 | import io.netty.handler.ssl.NotSslRecordException; 7 | import io.netty.util.ReferenceCountUtil; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | 11 | import javax.net.ssl.SSLHandshakeException; 12 | import java.net.SocketAddress; 13 | 14 | /** 15 | * This handler will catch and suppress exceptions which are triggered when a client send a 16 | * non-SSL request when SSL is required. We mute these exceptions as they are typically 17 | * caused by misbehaving clients and so it doesn't make sense to fill server logs with 18 | * stack traces for these scenarios. 19 | */ 20 | public class SuppressingSslExceptionHandler extends ChannelHandlerAdapter { 21 | private static final Logger logger = LoggerFactory.getLogger(SuppressingSslExceptionHandler.class); 22 | 23 | @Override 24 | public void exceptionCaught(final ChannelHandlerContext ctx, final Throwable cause) { 25 | if (handshakeException(cause) 26 | || sslRecordException(cause) 27 | || decoderSslRecordException(cause)) { 28 | try { 29 | logger.info("Exception while processing SSL scrape request from {}: {}", remotePeer(ctx), cause.getMessage()); 30 | logger.debug("Exception while processing SSL scrape request.", cause); 31 | } finally { 32 | ReferenceCountUtil.release(cause); 33 | } 34 | } else { 35 | ctx.fireExceptionCaught(cause); 36 | } 37 | } 38 | 39 | private SocketAddress remotePeer(final ChannelHandlerContext ctx) { 40 | if (ctx.channel() == null) { 41 | return null; 42 | } 43 | return ctx.channel().remoteAddress(); 44 | } 45 | 46 | private boolean handshakeException(final Throwable cause) { 47 | return cause instanceof DecoderException 48 | && cause.getCause() instanceof SSLHandshakeException; 49 | } 50 | 51 | private boolean sslRecordException(final Throwable cause) { 52 | return cause instanceof NotSslRecordException; 53 | } 54 | 55 | private boolean decoderSslRecordException(final Throwable cause) { 56 | return cause instanceof DecoderException 57 | && cause.getCause() instanceof NotSslRecordException; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /common/src/main/java/com/zegelin/cassandra/exporter/netty/ssl/UnexpectedSslExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package com.zegelin.cassandra.exporter.netty.ssl; 2 | 3 | import io.netty.channel.ChannelHandlerAdapter; 4 | import io.netty.channel.ChannelHandlerContext; 5 | import io.netty.handler.codec.DecoderException; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | 9 | import javax.net.ssl.SSLException; 10 | 11 | public class UnexpectedSslExceptionHandler extends ChannelHandlerAdapter { 12 | private static final Logger logger = LoggerFactory.getLogger(UnexpectedSslExceptionHandler.class); 13 | 14 | private final ReloadWatcher reloadWatcher; 15 | 16 | UnexpectedSslExceptionHandler(final ReloadWatcher reloadWatcher) { 17 | this.reloadWatcher = reloadWatcher; 18 | } 19 | 20 | @Override 21 | public void exceptionCaught(final ChannelHandlerContext ctx, final Throwable cause) { 22 | try { 23 | if (unexpectedMessage(cause)) { 24 | logger.warn(cause.getMessage()); 25 | // This may indicate that we're currently using invalid combo of key & cert 26 | reloadWatcher.forceReload(); 27 | } 28 | } finally { 29 | ctx.fireExceptionCaught(cause); 30 | } 31 | } 32 | 33 | private boolean unexpectedMessage(final Throwable cause) { 34 | return cause instanceof DecoderException 35 | && cause.getCause() instanceof SSLException 36 | && cause.getCause().getMessage().contains("unexpected_message"); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /common/src/main/java/com/zegelin/function/FloatFloatFunction.java: -------------------------------------------------------------------------------- 1 | package com.zegelin.function; 2 | 3 | @FunctionalInterface 4 | public interface FloatFloatFunction { 5 | float apply(float f); 6 | 7 | static FloatFloatFunction identity() { 8 | return (f) -> f; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /common/src/main/java/com/zegelin/jmx/NamedObject.java: -------------------------------------------------------------------------------- 1 | package com.zegelin.jmx; 2 | 3 | import com.google.common.base.MoreObjects; 4 | import com.google.common.base.Preconditions; 5 | 6 | import javax.management.ObjectName; 7 | import java.util.function.BiFunction; 8 | 9 | /** 10 | * An arbitrary object (typically an MBean) and its JMX name. 11 | * 12 | * @param Object type 13 | */ 14 | public class NamedObject { 15 | public final ObjectName name; 16 | public final T object; 17 | 18 | public NamedObject(final ObjectName name, final T object) { 19 | Preconditions.checkNotNull(name); 20 | Preconditions.checkNotNull(object); 21 | 22 | this.object = object; 23 | this.name = name; 24 | } 25 | 26 | @SuppressWarnings("unchecked") 27 | public NamedObject cast() { 28 | return map((n, o) -> (U) o); 29 | } 30 | 31 | public NamedObject map(final BiFunction mapper) { 32 | final U mappedObject = mapper.apply(name, object); 33 | 34 | return mappedObject == null ? null : new NamedObject<>(name, mappedObject); 35 | } 36 | 37 | @Override 38 | public String toString() { 39 | return MoreObjects.toStringHelper(this) 40 | .add("name", name) 41 | .add("object", object) 42 | .toString(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /common/src/main/java/com/zegelin/jmx/ObjectNames.java: -------------------------------------------------------------------------------- 1 | package com.zegelin.jmx; 2 | 3 | import javax.management.MalformedObjectNameException; 4 | import javax.management.ObjectName; 5 | 6 | public final class ObjectNames { 7 | private ObjectNames() {} 8 | 9 | public static ObjectName create(final String name) { 10 | try { 11 | return ObjectName.getInstance(name); 12 | 13 | } catch (final MalformedObjectNameException e) { 14 | throw new IllegalStateException(e); 15 | } 16 | } 17 | 18 | public static ObjectName format(final String nameFormat, final Object... args) { 19 | return create(String.format(nameFormat, args)); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /common/src/main/java/com/zegelin/netty/Floats.java: -------------------------------------------------------------------------------- 1 | package com.zegelin.netty; 2 | 3 | import info.adams.ryu.RyuFloat; 4 | import io.netty.buffer.ByteBuf; 5 | import io.netty.buffer.ByteBufUtil; 6 | 7 | import java.nio.ByteBuffer; 8 | 9 | import static java.nio.charset.StandardCharsets.US_ASCII; 10 | 11 | public final class Floats { 12 | public static boolean useFastFloat = true; 13 | 14 | private Floats() {} 15 | 16 | public static int writeFloatString(final ByteBuf buffer, final float f) { 17 | return ByteBufUtil.writeAscii(buffer, Float.toString(f)); 18 | } 19 | 20 | public static int writeFloatString(final ByteBuffer buffer, final float f) { 21 | if (useFastFloat) { 22 | return RyuFloat.floatToString(buffer, f); 23 | } else { 24 | byte[] byteBuffer = Float.toString(f).getBytes(US_ASCII); 25 | int size = byteBuffer.length; 26 | buffer.put(byteBuffer); 27 | return size; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /common/src/main/java/com/zegelin/netty/Resources.java: -------------------------------------------------------------------------------- 1 | package com.zegelin.netty; 2 | 3 | import com.google.common.io.ByteStreams; 4 | import io.netty.buffer.ByteBuf; 5 | import io.netty.buffer.Unpooled; 6 | 7 | import java.io.IOException; 8 | import java.io.InputStream; 9 | import java.io.UncheckedIOException; 10 | 11 | public final class Resources { 12 | private Resources() {} 13 | 14 | public static ByteBuf asByteBuf(final Class clazz, final String name) { 15 | try (final InputStream stream = clazz.getResourceAsStream(name)) { 16 | final byte[] bytes = ByteStreams.toByteArray(stream); 17 | 18 | return Unpooled.unmodifiableBuffer(Unpooled.unreleasableBuffer(Unpooled.wrappedBuffer(bytes))); 19 | 20 | } catch (final IOException e) { 21 | throw new UncheckedIOException(e); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /common/src/main/java/com/zegelin/picocli/InetSocketAddressTypeConverter.java: -------------------------------------------------------------------------------- 1 | package com.zegelin.picocli; 2 | 3 | import com.google.common.base.Splitter; 4 | import picocli.CommandLine; 5 | 6 | import java.net.InetAddress; 7 | import java.net.InetSocketAddress; 8 | import java.util.List; 9 | 10 | public abstract class InetSocketAddressTypeConverter implements CommandLine.ITypeConverter { 11 | @Override 12 | public InetSocketAddress convert(final String value) throws Exception { 13 | final List addressParts = Splitter.on(':').limit(2).splitToList(value); 14 | 15 | String hostname = addressParts.get(0).trim(); 16 | hostname = (hostname.length() == 0 ? null : hostname); // an empty hostname == wildcard/any 17 | 18 | int port = defaultPort(); 19 | if (addressParts.size() == 2) { 20 | try { 21 | port = Integer.parseInt(addressParts.get(1).trim()); 22 | 23 | } catch (final NumberFormatException e) { 24 | throw new CommandLine.TypeConversionException("Specified port is not a valid number"); 25 | } 26 | } 27 | 28 | try { 29 | // why can you pass a null InetAddress, but a null String hostname is an error... 30 | return (hostname == null ? 31 | new InetSocketAddress((InetAddress) null, port) : 32 | new InetSocketAddress(hostname, port)); 33 | 34 | } catch (final IllegalArgumentException e) { 35 | // invalid port, etc... 36 | throw new CommandLine.TypeConversionException(e.getLocalizedMessage()); 37 | } 38 | } 39 | 40 | protected abstract int defaultPort(); 41 | } 42 | -------------------------------------------------------------------------------- /common/src/main/java/com/zegelin/picocli/JMXServiceURLTypeConverter.java: -------------------------------------------------------------------------------- 1 | package com.zegelin.picocli; 2 | 3 | import picocli.CommandLine; 4 | 5 | import javax.management.remote.JMXServiceURL; 6 | import java.net.MalformedURLException; 7 | 8 | public class JMXServiceURLTypeConverter implements CommandLine.ITypeConverter { 9 | @Override 10 | public JMXServiceURL convert(final String value) { 11 | try { 12 | return new JMXServiceURL(value); 13 | 14 | } catch (final MalformedURLException e) { 15 | throw new CommandLine.TypeConversionException("Invalid JMX service URL (" + e.getLocalizedMessage() + ")"); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /common/src/main/java/com/zegelin/prometheus/domain/CounterMetricFamily.java: -------------------------------------------------------------------------------- 1 | package com.zegelin.prometheus.domain; 2 | 3 | import java.util.List; 4 | import java.util.function.Supplier; 5 | import java.util.stream.Collectors; 6 | import java.util.stream.Stream; 7 | 8 | public class CounterMetricFamily extends MetricFamily { 9 | public CounterMetricFamily(final String name, final String help, final Stream metrics) { 10 | this(name, help, () -> metrics); 11 | } 12 | 13 | CounterMetricFamily(final String name, final String help, final Supplier> metricsStreamSupplier) { 14 | super(name, help, metricsStreamSupplier); 15 | } 16 | 17 | @Override 18 | public R accept(final MetricFamilyVisitor visitor) { 19 | return visitor.visit(this); 20 | } 21 | 22 | @Override 23 | public CounterMetricFamily cachedCopy() { 24 | final List metrics = metrics().collect(Collectors.toList()); 25 | 26 | return new CounterMetricFamily(name, help, metrics::stream); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /common/src/main/java/com/zegelin/prometheus/domain/GaugeMetricFamily.java: -------------------------------------------------------------------------------- 1 | package com.zegelin.prometheus.domain; 2 | 3 | import java.util.List; 4 | import java.util.function.Supplier; 5 | import java.util.stream.Collectors; 6 | import java.util.stream.Stream; 7 | 8 | public class GaugeMetricFamily extends MetricFamily { 9 | public GaugeMetricFamily(final String name, final String help, final Stream metrics) { 10 | this(name, help, () -> metrics); 11 | } 12 | 13 | GaugeMetricFamily(final String name, final String help, final Supplier> metricsStreamSupplier) { 14 | super(name, help, metricsStreamSupplier); 15 | } 16 | 17 | 18 | @Override 19 | public R accept(final MetricFamilyVisitor visitor) { 20 | return visitor.visit(this); 21 | } 22 | 23 | @Override 24 | public GaugeMetricFamily cachedCopy() { 25 | final List metrics = metrics().collect(Collectors.toList()); 26 | 27 | return new GaugeMetricFamily(name, help, metrics::stream); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /common/src/main/java/com/zegelin/prometheus/domain/HistogramMetricFamily.java: -------------------------------------------------------------------------------- 1 | package com.zegelin.prometheus.domain; 2 | 3 | import com.google.common.collect.ImmutableList; 4 | 5 | import java.util.List; 6 | import java.util.function.Supplier; 7 | import java.util.stream.Collectors; 8 | import java.util.stream.Stream; 9 | 10 | public class HistogramMetricFamily extends MetricFamily { 11 | public HistogramMetricFamily(final String name, final String help, final Stream metrics) { 12 | this(name, help, () -> metrics); 13 | } 14 | 15 | HistogramMetricFamily(final String name, final String help, final Supplier> metricsStreamSupplier) { 16 | super(name, help, metricsStreamSupplier); 17 | } 18 | 19 | @Override 20 | public R accept(final MetricFamilyVisitor visitor) { 21 | return visitor.visit(this); 22 | } 23 | 24 | @Override 25 | public HistogramMetricFamily cachedCopy() { 26 | final List metrics = metrics().collect(Collectors.toList()); 27 | 28 | return new HistogramMetricFamily(name, help, metrics::stream); 29 | } 30 | 31 | public static class Histogram extends Metric { 32 | public final float sum; 33 | public final float count; 34 | public final Iterable buckets; 35 | 36 | public Histogram(final Labels labels, final float sum, final float count, final Iterable buckets) { 37 | super(labels); 38 | 39 | this.sum = sum; 40 | this.count = count; 41 | this.buckets = ImmutableList.copyOf(buckets); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /common/src/main/java/com/zegelin/prometheus/domain/Interval.java: -------------------------------------------------------------------------------- 1 | package com.zegelin.prometheus.domain; 2 | 3 | import com.google.common.collect.ImmutableSet; 4 | import com.google.common.collect.Iterables; 5 | import com.zegelin.function.FloatFloatFunction; 6 | 7 | import java.util.Set; 8 | import java.util.function.Function; 9 | import java.util.stream.Stream; 10 | import java.util.stream.StreamSupport; 11 | 12 | /* 13 | A Summary quanitle or Histogram bucket and associated value. 14 | */ 15 | public final class Interval { 16 | public static class Quantile { 17 | public static final Quantile P_50 = q(.5f); 18 | public static final Quantile P_75 = q(.75f); 19 | public static final Quantile P_95 = q(.95f); 20 | public static final Quantile P_98 = q(.98f); 21 | public static final Quantile P_99 = q(.99f); 22 | public static final Quantile P_99_9 = q(.999f); 23 | 24 | public static final Set STANDARD_PERCENTILES = ImmutableSet.of(P_50, P_75, P_95, P_98, P_99, P_99_9); 25 | public static final Quantile POSITIVE_INFINITY = q(Float.POSITIVE_INFINITY); 26 | 27 | public final float value; 28 | 29 | private final String stringRepr; 30 | private final Labels summaryLabel, histogramLabel; 31 | 32 | public Quantile(final float value) { 33 | this.value = value; 34 | 35 | this.stringRepr = Float.toString(value); 36 | this.summaryLabel = Labels.of("quantile", this.stringRepr); 37 | this.histogramLabel = Labels.of("le", this.stringRepr); 38 | } 39 | 40 | public static Quantile q(final float value) { 41 | return new Quantile(value); 42 | } 43 | 44 | public Labels asSummaryLabel() { 45 | return summaryLabel; 46 | } 47 | 48 | public Labels asHistogramLabel() { 49 | return histogramLabel; 50 | } 51 | 52 | @Override 53 | public String toString() { 54 | return stringRepr; 55 | } 56 | } 57 | 58 | public final Quantile quantile; 59 | public final float value; 60 | 61 | public Interval(final Quantile quantile, final float value) { 62 | this.quantile = quantile; 63 | this.value = value; 64 | } 65 | 66 | public static Iterable asIntervals(final Iterable quantiles, final Function valueFn) { 67 | return Iterables.transform(quantiles, q -> new Interval(q, valueFn.apply(q))); 68 | } 69 | 70 | public Interval transform(final FloatFloatFunction valueTransformFn) { 71 | if (valueTransformFn == FloatFloatFunction.identity()) 72 | return this; 73 | 74 | return new Interval(this.quantile, valueTransformFn.apply(this.value)); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /common/src/main/java/com/zegelin/prometheus/domain/Labels.java: -------------------------------------------------------------------------------- 1 | package com.zegelin.prometheus.domain; 2 | 3 | import com.google.common.collect.ForwardingMap; 4 | import com.google.common.collect.ImmutableMap; 5 | import com.zegelin.prometheus.exposition.json.JsonFormatExposition; 6 | import com.zegelin.prometheus.exposition.text.TextFormatLabels; 7 | import io.netty.buffer.ByteBuf; 8 | 9 | import java.util.Map; 10 | 11 | public final class Labels extends ForwardingMap { 12 | private final ImmutableMap labels; 13 | private final boolean isEmpty; 14 | 15 | private ByteBuf plainTextFormatUTF8EncodedByteBuf, jsonFormatUTF8EncodedByteBuf; 16 | 17 | public Labels(final Map labels) { 18 | this.labels = ImmutableMap.copyOf(labels); 19 | this.isEmpty = this.labels.isEmpty(); 20 | } 21 | 22 | public static Labels of(final String key, final String value) { 23 | return new Labels(ImmutableMap.of(key, value)); 24 | } 25 | 26 | public static Labels of() { 27 | return new Labels(ImmutableMap.of()); 28 | } 29 | 30 | @Override 31 | protected Map delegate() { 32 | return labels; 33 | } 34 | 35 | @Override 36 | public boolean isEmpty() { 37 | return isEmpty; 38 | } 39 | 40 | public ByteBuf asPlainTextFormatUTF8EncodedByteBuf() { 41 | if (plainTextFormatUTF8EncodedByteBuf == null) { 42 | this.plainTextFormatUTF8EncodedByteBuf = TextFormatLabels.formatLabels(labels); 43 | } 44 | 45 | return plainTextFormatUTF8EncodedByteBuf; 46 | } 47 | 48 | public ByteBuf asJSONFormatUTF8EncodedByteBuf() { 49 | if (jsonFormatUTF8EncodedByteBuf == null) { 50 | this.jsonFormatUTF8EncodedByteBuf = JsonFormatExposition.formatLabels(labels); 51 | } 52 | 53 | return jsonFormatUTF8EncodedByteBuf; 54 | } 55 | 56 | @Override 57 | protected void finalize() throws Throwable { 58 | try { 59 | maybeRelease(); 60 | } finally { 61 | super.finalize(); 62 | } 63 | } 64 | 65 | private void maybeRelease() { 66 | if (this.plainTextFormatUTF8EncodedByteBuf != null) { 67 | this.plainTextFormatUTF8EncodedByteBuf.release(); 68 | this.plainTextFormatUTF8EncodedByteBuf = null; 69 | } 70 | 71 | if (this.jsonFormatUTF8EncodedByteBuf != null) { 72 | this.jsonFormatUTF8EncodedByteBuf.release(); 73 | this.jsonFormatUTF8EncodedByteBuf = null; 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /common/src/main/java/com/zegelin/prometheus/domain/Metric.java: -------------------------------------------------------------------------------- 1 | package com.zegelin.prometheus.domain; 2 | 3 | import java.util.Objects; 4 | 5 | public abstract class Metric { 6 | public final Labels labels; 7 | 8 | protected Metric(final Labels labels) { 9 | this.labels = labels; 10 | } 11 | 12 | @Override 13 | public boolean equals(final Object o) { 14 | if (this == o) return true; 15 | if (!(o instanceof Metric)) return false; 16 | 17 | final Metric metric = (Metric) o; 18 | return Objects.equals(labels, metric.labels); 19 | } 20 | 21 | @Override 22 | public int hashCode() { 23 | return Objects.hash(labels); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /common/src/main/java/com/zegelin/prometheus/domain/MetricFamily.java: -------------------------------------------------------------------------------- 1 | package com.zegelin.prometheus.domain; 2 | 3 | import com.google.common.base.MoreObjects; 4 | 5 | import java.util.Objects; 6 | import java.util.function.Supplier; 7 | import java.util.stream.Stream; 8 | 9 | public abstract class MetricFamily { 10 | public final String name, help; 11 | 12 | private Supplier> metricsStreamSupplier; 13 | 14 | public MetricFamily(final String name, final String help, final Supplier> metricsStreamSupplier) { 15 | this.name = name; 16 | this.help = help; 17 | this.metricsStreamSupplier = metricsStreamSupplier; 18 | } 19 | 20 | public abstract R accept(final MetricFamilyVisitor visitor); 21 | 22 | public abstract MetricFamily cachedCopy(); 23 | 24 | public Stream metrics() { 25 | return metricsStreamSupplier.get(); 26 | } 27 | 28 | @Override 29 | public boolean equals(final Object o) { 30 | if (this == o) return true; 31 | if (!(o instanceof MetricFamily)) return false; 32 | 33 | final MetricFamily that = (MetricFamily) o; 34 | return Objects.equals(name, that.name); 35 | } 36 | 37 | @Override 38 | public int hashCode() { 39 | return Objects.hash(name); 40 | } 41 | 42 | @Override 43 | public String toString() { 44 | return MoreObjects.toStringHelper(this) 45 | .add("name", name) 46 | .add("help", help) 47 | .toString(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /common/src/main/java/com/zegelin/prometheus/domain/MetricFamilyVisitor.java: -------------------------------------------------------------------------------- 1 | package com.zegelin.prometheus.domain; 2 | 3 | public interface MetricFamilyVisitor { 4 | T visit(final CounterMetricFamily metricFamily); 5 | T visit(final GaugeMetricFamily metricFamily); 6 | T visit(final SummaryMetricFamily metricFamily); 7 | T visit(final HistogramMetricFamily metricFamily); 8 | T visit(final UntypedMetricFamily metricFamily); 9 | } 10 | -------------------------------------------------------------------------------- /common/src/main/java/com/zegelin/prometheus/domain/NumericMetric.java: -------------------------------------------------------------------------------- 1 | package com.zegelin.prometheus.domain; 2 | 3 | public class NumericMetric extends Metric { 4 | public final float value; 5 | 6 | public NumericMetric(final Labels labels, final float value) { 7 | super(labels); 8 | this.value = value; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /common/src/main/java/com/zegelin/prometheus/domain/SummaryMetricFamily.java: -------------------------------------------------------------------------------- 1 | package com.zegelin.prometheus.domain; 2 | 3 | import com.google.common.collect.ImmutableList; 4 | 5 | import java.util.List; 6 | import java.util.function.Supplier; 7 | import java.util.stream.Collectors; 8 | import java.util.stream.Stream; 9 | 10 | public class SummaryMetricFamily extends MetricFamily { 11 | public SummaryMetricFamily(final String name, final String help, final Stream metrics) { 12 | this(name, help, () -> metrics); 13 | } 14 | 15 | SummaryMetricFamily(final String name, final String help, final Supplier> metricsStreamSupplier) { 16 | super(name, help, metricsStreamSupplier); 17 | } 18 | 19 | @Override 20 | public R accept(final MetricFamilyVisitor visitor) { 21 | return visitor.visit(this); 22 | } 23 | 24 | @Override 25 | public SummaryMetricFamily cachedCopy() { 26 | final List metrics = metrics().collect(Collectors.toList()); 27 | 28 | return new SummaryMetricFamily(name, help, metrics::stream); 29 | } 30 | 31 | public static class Summary extends Metric { 32 | public final float sum; 33 | public final float count; 34 | public final Iterable quantiles; 35 | 36 | public Summary(final Labels labels, final float sum, final float count, final Iterable quantiles) { 37 | super(labels); 38 | 39 | this.sum = sum; 40 | this.count = count; 41 | this.quantiles = ImmutableList.copyOf(quantiles); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /common/src/main/java/com/zegelin/prometheus/domain/UntypedMetricFamily.java: -------------------------------------------------------------------------------- 1 | package com.zegelin.prometheus.domain; 2 | 3 | import java.util.List; 4 | import java.util.Objects; 5 | import java.util.function.Supplier; 6 | import java.util.stream.Collectors; 7 | import java.util.stream.Stream; 8 | 9 | public class UntypedMetricFamily extends MetricFamily { 10 | public UntypedMetricFamily(final String name, final String help, final Stream metrics) { 11 | this(name, help, () -> metrics); 12 | } 13 | 14 | UntypedMetricFamily(final String name, final String help, final Supplier> metricsStreamSupplier) { 15 | super(name, help, metricsStreamSupplier); 16 | } 17 | 18 | @Override 19 | public R accept(final MetricFamilyVisitor visitor) { 20 | return visitor.visit(this); 21 | } 22 | 23 | @Override 24 | public MetricFamily cachedCopy() { 25 | final List metrics = metrics().collect(Collectors.toList()); 26 | 27 | return new UntypedMetricFamily(name, help, metrics::stream); 28 | } 29 | 30 | public static class Untyped extends NumericMetric { 31 | public final String name; 32 | 33 | public Untyped(final Labels labels, final String name, final float value) { 34 | super(labels, value); 35 | 36 | this.name = name; 37 | } 38 | 39 | @Override 40 | public boolean equals(final Object o) { 41 | if (this == o) return true; 42 | if (!(o instanceof Untyped)) return false; 43 | if (!super.equals(o)) return false; 44 | 45 | final Untyped untyped = (Untyped) o; 46 | return Objects.equals(name, untyped.name); 47 | } 48 | 49 | @Override 50 | public int hashCode() { 51 | return Objects.hash(super.hashCode(), name); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /common/src/main/java/com/zegelin/prometheus/exposition/ExpositionSink.java: -------------------------------------------------------------------------------- 1 | package com.zegelin.prometheus.exposition; 2 | 3 | import java.nio.ByteBuffer; 4 | 5 | public interface ExpositionSink { 6 | void writeByte(int asciiChar); 7 | 8 | void writeBytes(ByteBuffer nioBuffer); 9 | 10 | void writeAscii(String asciiString); 11 | 12 | void writeUtf8(String utf8String); 13 | 14 | void writeFloat(float value); 15 | 16 | T getBuffer(); 17 | 18 | int getIngestedByteCount(); 19 | } 20 | -------------------------------------------------------------------------------- /common/src/main/java/com/zegelin/prometheus/exposition/FormattedByteChannel.java: -------------------------------------------------------------------------------- 1 | package com.zegelin.prometheus.exposition; 2 | 3 | import java.nio.ByteBuffer; 4 | import java.nio.channels.ReadableByteChannel; 5 | 6 | public class FormattedByteChannel implements ReadableByteChannel { 7 | public static final int MIN_CHUNK_SIZE = 1024 * 1024; 8 | public static final int MAX_CHUNK_SIZE = MIN_CHUNK_SIZE * 5; 9 | 10 | private final FormattedExposition formattedExposition; 11 | 12 | public FormattedByteChannel(FormattedExposition formattedExposition) { 13 | this.formattedExposition = formattedExposition; 14 | } 15 | 16 | @Override 17 | public int read(ByteBuffer dst) { 18 | if (!isOpen()) { 19 | return -1; 20 | } 21 | 22 | final NioExpositionSink sink = new NioExpositionSink(dst); 23 | while (sink.getIngestedByteCount() < MIN_CHUNK_SIZE && isOpen()) { 24 | formattedExposition.nextSlice(sink); 25 | } 26 | 27 | return sink.getIngestedByteCount(); 28 | } 29 | 30 | @Override 31 | public boolean isOpen() { 32 | return !formattedExposition.isEndOfInput(); 33 | } 34 | 35 | @Override 36 | public void close() { 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /common/src/main/java/com/zegelin/prometheus/exposition/FormattedExposition.java: -------------------------------------------------------------------------------- 1 | package com.zegelin.prometheus.exposition; 2 | 3 | public interface FormattedExposition { 4 | void nextSlice(final ExpositionSink sink); 5 | 6 | boolean isEndOfInput(); 7 | } 8 | -------------------------------------------------------------------------------- /common/src/main/java/com/zegelin/prometheus/exposition/NettyExpositionSink.java: -------------------------------------------------------------------------------- 1 | package com.zegelin.prometheus.exposition; 2 | 3 | import com.zegelin.netty.Floats; 4 | import io.netty.buffer.ByteBuf; 5 | import io.netty.buffer.ByteBufUtil; 6 | 7 | import java.nio.ByteBuffer; 8 | 9 | public class NettyExpositionSink implements ExpositionSink { 10 | private int ingestedByteCount = 0; 11 | private final ByteBuf buffer; 12 | 13 | public NettyExpositionSink(ByteBuf buffer) { 14 | this.buffer = buffer; 15 | } 16 | 17 | @Override 18 | public void writeByte(int asciiChar) { 19 | ingestedByteCount++; 20 | buffer.writeByte(asciiChar); 21 | } 22 | 23 | @Override 24 | public void writeBytes(ByteBuffer nioBuffer) { 25 | ingestedByteCount += nioBuffer.remaining(); 26 | buffer.writeBytes(nioBuffer); 27 | } 28 | 29 | @Override 30 | public void writeAscii(String asciiString) { 31 | ingestedByteCount += ByteBufUtil.writeAscii(buffer, asciiString); 32 | } 33 | 34 | @Override 35 | public void writeUtf8(String utf8String) { 36 | ingestedByteCount += ByteBufUtil.writeUtf8(buffer, utf8String); 37 | } 38 | 39 | @Override 40 | public void writeFloat(float value) { 41 | ingestedByteCount += Floats.writeFloatString(buffer, value); 42 | } 43 | 44 | @Override 45 | public ByteBuf getBuffer() { 46 | return buffer; 47 | } 48 | 49 | @Override 50 | public int getIngestedByteCount() { 51 | return ingestedByteCount; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /common/src/main/java/com/zegelin/prometheus/exposition/NioExpositionSink.java: -------------------------------------------------------------------------------- 1 | package com.zegelin.prometheus.exposition; 2 | 3 | import com.zegelin.netty.Floats; 4 | 5 | import java.nio.ByteBuffer; 6 | 7 | import static java.nio.charset.StandardCharsets.US_ASCII; 8 | import static java.nio.charset.StandardCharsets.UTF_8; 9 | 10 | class NioExpositionSink implements ExpositionSink { 11 | private int ingestedByteCount = 0; 12 | private final ByteBuffer buffer; 13 | 14 | NioExpositionSink(ByteBuffer buffer) { 15 | this.buffer = buffer; 16 | } 17 | 18 | @Override 19 | public void writeByte(int asciiChar) { 20 | ingestedByteCount++; 21 | buffer.put((byte) asciiChar); 22 | } 23 | 24 | @Override 25 | public void writeBytes(ByteBuffer nioBuffer) { 26 | ingestedByteCount += nioBuffer.remaining(); 27 | buffer.put(nioBuffer); 28 | } 29 | 30 | @Override 31 | public void writeAscii(String asciiString) { 32 | byte[] byteBuffer = asciiString.getBytes(US_ASCII); 33 | ingestedByteCount += byteBuffer.length; 34 | buffer.put(byteBuffer); 35 | } 36 | 37 | @Override 38 | public void writeUtf8(String utf8String) { 39 | byte[] byteBuffer = utf8String.getBytes(UTF_8); 40 | ingestedByteCount += byteBuffer.length; 41 | buffer.put(byteBuffer); 42 | } 43 | 44 | @Override 45 | public void writeFloat(float value) { 46 | ingestedByteCount += Floats.writeFloatString(buffer, value); 47 | } 48 | 49 | @Override 50 | public ByteBuffer getBuffer() { 51 | return buffer; 52 | } 53 | 54 | @Override 55 | public int getIngestedByteCount() { 56 | return ingestedByteCount; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /common/src/main/java/com/zegelin/prometheus/exposition/json/JsonFragment.java: -------------------------------------------------------------------------------- 1 | package com.zegelin.prometheus.exposition.json; 2 | 3 | import com.google.common.escape.CharEscaperBuilder; 4 | import com.google.common.escape.Escaper; 5 | import com.zegelin.prometheus.exposition.ExpositionSink; 6 | 7 | final class JsonFragment { 8 | private JsonFragment() {} 9 | 10 | private static Escaper STRING_ESCAPER = new CharEscaperBuilder() 11 | .addEscape('\b', "\\b") 12 | .addEscape('\f', "\\f") 13 | .addEscape('\n', "\\n") 14 | .addEscape('\r', "\\r") 15 | .addEscape('\t', "\\t") 16 | .addEscape('"', "\\\"") 17 | .addEscape('\\', "\\\\") 18 | .toEscaper(); 19 | 20 | static void writeNull(final ExpositionSink buffer) { 21 | buffer.writeAscii("null"); 22 | } 23 | 24 | static void writeAsciiString(final ExpositionSink buffer, final String string) { 25 | JsonToken.DOUBLE_QUOTE.write(buffer); 26 | buffer.writeAscii(STRING_ESCAPER.escape(string)); 27 | JsonToken.DOUBLE_QUOTE.write(buffer); 28 | } 29 | 30 | static void writeUtf8String(final ExpositionSink buffer, final String string) { 31 | JsonToken.DOUBLE_QUOTE.write(buffer); 32 | buffer.writeUtf8(STRING_ESCAPER.escape(string)); 33 | JsonToken.DOUBLE_QUOTE.write(buffer); 34 | } 35 | 36 | static void writeObjectKey(final ExpositionSink buffer, final String key) { 37 | buffer.writeAscii(key); 38 | JsonToken.COLON.write(buffer); 39 | } 40 | 41 | static void writeFloat(final ExpositionSink buffer, final float f) { 42 | if (Float.isNaN(f)) { 43 | buffer.writeAscii("\"NaN\""); 44 | return; 45 | } 46 | 47 | if (Float.isInfinite(f)) { 48 | buffer.writeAscii((f < 0 ? "\"-Inf\"" : "\"+Inf\"")); 49 | return; 50 | } 51 | 52 | buffer.writeFloat(f); 53 | } 54 | 55 | static void writeLong(final ExpositionSink buffer, final long l) { 56 | buffer.writeAscii(Long.toString(l)); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /common/src/main/java/com/zegelin/prometheus/exposition/json/JsonToken.java: -------------------------------------------------------------------------------- 1 | package com.zegelin.prometheus.exposition.json; 2 | 3 | import com.zegelin.prometheus.exposition.ExpositionSink; 4 | 5 | enum JsonToken { 6 | OBJECT_START('{'), 7 | OBJECT_END('}'), 8 | ARRAY_START('['), 9 | ARRAY_END(']'), 10 | DOUBLE_QUOTE('"'), 11 | COMMA(','), 12 | COLON(':'); 13 | 14 | final byte encoded; 15 | 16 | JsonToken(final char c) { 17 | this.encoded = (byte) c; 18 | } 19 | 20 | void write(final ExpositionSink buffer) { 21 | buffer.writeByte(encoded); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /common/src/main/java/com/zegelin/prometheus/exposition/text/TextFormatExposition.java: -------------------------------------------------------------------------------- 1 | package com.zegelin.prometheus.exposition.text; 2 | 3 | import com.google.common.base.Stopwatch; 4 | import com.zegelin.netty.Resources; 5 | import com.zegelin.prometheus.domain.*; 6 | import com.zegelin.prometheus.exposition.ExpositionSink; 7 | import com.zegelin.prometheus.exposition.FormattedExposition; 8 | import io.netty.buffer.ByteBuf; 9 | 10 | import java.time.Instant; 11 | import java.util.Iterator; 12 | import java.util.stream.Stream; 13 | 14 | public class TextFormatExposition implements FormattedExposition { 15 | private enum State { 16 | BANNER, 17 | METRIC_FAMILY, 18 | METRIC, 19 | FOOTER, 20 | EOF 21 | } 22 | 23 | private static final ByteBuf BANNER = Resources.asByteBuf(TextFormatExposition.class, "banner.txt"); 24 | 25 | private final Iterator metricFamiliesIterator; 26 | 27 | private final Instant timestamp; 28 | private final Labels globalLabels; 29 | private final boolean includeHelp; 30 | 31 | private State state = State.BANNER; 32 | private TextFormatMetricFamilyWriter metricFamilyWriter; 33 | 34 | private int metricFamilyCount = 0; 35 | private int metricCount = 0; 36 | 37 | private final Stopwatch stopwatch = Stopwatch.createUnstarted(); 38 | 39 | 40 | public TextFormatExposition(final Stream metricFamilies, final Instant timestamp, final Labels globalLabels, final boolean includeHelp) { 41 | this.metricFamiliesIterator = metricFamilies.iterator(); 42 | this.timestamp = timestamp; 43 | this.globalLabels = globalLabels; 44 | this.includeHelp = includeHelp; 45 | } 46 | 47 | @Override 48 | public boolean isEndOfInput() { 49 | return state == State.EOF; 50 | } 51 | 52 | @Override 53 | public void nextSlice(final ExpositionSink chunkBuffer) { 54 | switch (state) { 55 | case BANNER: 56 | stopwatch.start(); 57 | 58 | chunkBuffer.writeBytes(BANNER.nioBuffer()); 59 | 60 | state = State.METRIC_FAMILY; 61 | return; 62 | 63 | case METRIC_FAMILY: 64 | if (!metricFamiliesIterator.hasNext()) { 65 | state = State.FOOTER; 66 | return; 67 | } 68 | 69 | metricFamilyCount++; 70 | 71 | final MetricFamily metricFamily = metricFamiliesIterator.next(); 72 | 73 | metricFamilyWriter = new TextFormatMetricFamilyWriter(timestamp, globalLabels, includeHelp, metricFamily); 74 | 75 | metricFamilyWriter.writeFamilyHeader(chunkBuffer); 76 | 77 | state = State.METRIC; 78 | return; 79 | 80 | case METRIC: 81 | final boolean hasMoreMetrics = metricFamilyWriter.writeMetric(chunkBuffer); 82 | 83 | metricCount ++; 84 | 85 | if (!hasMoreMetrics) { 86 | chunkBuffer.writeByte('\n'); // separate from next family 87 | state = State.METRIC_FAMILY; 88 | return; 89 | } 90 | 91 | return; 92 | 93 | case FOOTER: 94 | stopwatch.stop(); 95 | chunkBuffer.writeAscii("\n\n# Thanks and come again!\n\n"); 96 | chunkBuffer.writeAscii(String.format("# Wrote %s metrics for %s metric families in %s\n", metricCount, metricFamilyCount, stopwatch.toString())); 97 | 98 | state = State.EOF; 99 | return; 100 | 101 | case EOF: 102 | return; 103 | 104 | default: 105 | throw new IllegalStateException(); 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /common/src/main/java/com/zegelin/prometheus/exposition/text/TextFormatLabels.java: -------------------------------------------------------------------------------- 1 | package com.zegelin.prometheus.exposition.text; 2 | 3 | import com.google.common.escape.CharEscaperBuilder; 4 | import com.google.common.escape.Escaper; 5 | import io.netty.buffer.ByteBuf; 6 | import io.netty.buffer.ByteBufAllocator; 7 | import io.netty.buffer.ByteBufUtil; 8 | import io.netty.buffer.Unpooled; 9 | 10 | import java.util.Iterator; 11 | import java.util.Map; 12 | 13 | public final class TextFormatLabels { 14 | private static Escaper LABEL_VALUE_ESCAPER = new CharEscaperBuilder() 15 | .addEscape('\\', "\\\\") 16 | .addEscape('\n', "\\n") 17 | .addEscape('"', "\\\"") 18 | .toEscaper(); 19 | 20 | private TextFormatLabels() {} 21 | 22 | public static ByteBuf formatLabels(final Map labels) { 23 | if (labels.isEmpty()) 24 | return Unpooled.EMPTY_BUFFER; 25 | 26 | final StringBuilder stringBuilder = new StringBuilder(); 27 | final Iterator> labelsIterator = labels.entrySet().iterator(); 28 | 29 | while (labelsIterator.hasNext()) { 30 | final Map.Entry label = labelsIterator.next(); 31 | 32 | stringBuilder.append(label.getKey()) 33 | .append("=\"") 34 | .append(LABEL_VALUE_ESCAPER.escape(label.getValue())) 35 | .append('"'); 36 | 37 | if (labelsIterator.hasNext()) { 38 | stringBuilder.append(','); 39 | } 40 | } 41 | 42 | return ByteBufUtil.writeUtf8(ByteBufAllocator.DEFAULT, stringBuilder); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /common/src/main/java/info/adams/ryu/RoundingMode.java: -------------------------------------------------------------------------------- 1 | package info.adams.ryu; 2 | 3 | // Copyright 2018 Ulf Adams 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | 18 | public enum RoundingMode { 19 | CONSERVATIVE { 20 | @Override 21 | public boolean acceptUpperBound(boolean even) { 22 | return false; 23 | } 24 | 25 | @Override 26 | public boolean acceptLowerBound(boolean even) { 27 | return false; 28 | } 29 | }, 30 | ROUND_EVEN { 31 | @Override 32 | public boolean acceptUpperBound(boolean even) { 33 | return even; 34 | } 35 | 36 | @Override 37 | public boolean acceptLowerBound(boolean even) { 38 | return even; 39 | } 40 | }; 41 | 42 | public abstract boolean acceptUpperBound(boolean even); 43 | public abstract boolean acceptLowerBound(boolean even); 44 | } -------------------------------------------------------------------------------- /common/src/main/resources/com/zegelin/cassandra/exporter/netty/root.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Cassandra Prometheus Exporter 6 | 7 | 8 | 9 |

Cassandra Prometheus Exporter

10 | 11 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /common/src/main/resources/com/zegelin/prometheus/exposition/text/banner.txt: -------------------------------------------------------------------------------- 1 | # _ _ 2 | # ___ __ _ ___ ___ __ _ _ __ __| |_ __ __ _ _____ ___ __ ___ _ __| |_ ___ _ __ 3 | # / __/ _` / __/ __|/ _` | '_ \ / _` | '__/ _` |_____ / _ \ \/ / '_ \ / _ \| '__| __/ _ \ '__| 4 | # | (_| (_| \__ \__ \ (_| | | | | (_| | | | (_| |_____| __/> <| |_) | (_) | | | || __/ | 5 | # \___\__,_|___/___/\__,_|_| |_|\__,_|_| \__,_| \___/_/\_\ .__/ \___/|_| \__\___|_| 6 | # |_| 7 | # 8 | -------------------------------------------------------------------------------- /common/src/test/java/com/zegelin/cassandra/exporter/netty/TestSuppressingExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package com.zegelin.cassandra.exporter.netty; 2 | 3 | import io.netty.channel.ChannelHandlerContext; 4 | import org.mockito.Mock; 5 | import org.mockito.MockitoAnnotations; 6 | import org.testng.annotations.BeforeMethod; 7 | import org.testng.annotations.Test; 8 | 9 | import java.io.IOException; 10 | 11 | import static org.mockito.ArgumentMatchers.any; 12 | import static org.mockito.Mockito.times; 13 | import static org.mockito.Mockito.verify; 14 | 15 | public class TestSuppressingExceptionHandler { 16 | @Mock 17 | private ChannelHandlerContext context; 18 | 19 | private SuppressingExceptionHandler handler; 20 | 21 | @BeforeMethod 22 | public void before() { 23 | MockitoAnnotations.initMocks(this); 24 | handler = new SuppressingExceptionHandler(); 25 | } 26 | 27 | @Test 28 | public void testBrokenPipeIoExceptionIsMuted() throws Exception { 29 | handler.exceptionCaught(context, new IOException("Broken pipe")); 30 | verify(context, times(0)).fireExceptionCaught(any()); 31 | } 32 | 33 | @Test 34 | public void testOtherIoExceptionIsPropagated() throws Exception { 35 | handler.exceptionCaught(context, new IOException("Other")); 36 | verify(context, times(1)).fireExceptionCaught(any()); 37 | } 38 | 39 | @Test 40 | public void testOtherExceptionIsPropagated() throws Exception { 41 | handler.exceptionCaught(context, new NullPointerException()); 42 | verify(context, times(1)).fireExceptionCaught(any()); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /common/src/test/java/com/zegelin/cassandra/exporter/netty/ssl/TestReloadWatcher.java: -------------------------------------------------------------------------------- 1 | package com.zegelin.cassandra.exporter.netty.ssl; 2 | 3 | import com.zegelin.cassandra.exporter.cli.HttpServerOptions; 4 | import org.testng.annotations.BeforeMethod; 5 | import org.testng.annotations.Test; 6 | 7 | import java.io.File; 8 | import java.io.IOException; 9 | import java.nio.file.Files; 10 | 11 | import static org.assertj.core.api.Assertions.assertThat; 12 | 13 | public class TestReloadWatcher { 14 | public static final long INITIAL_FILE_AGE_MILLIS = 5000; 15 | public static final long SLEEP_MILLIS = 1001; 16 | 17 | private HttpServerOptions options; 18 | private ReloadWatcher watcher; 19 | 20 | @BeforeMethod 21 | public void before() throws IOException { 22 | options = new HttpServerOptions(); 23 | options.sslReloadIntervalInSeconds = 1; 24 | 25 | options.sslServerKeyFile = givenTemporaryFile("server-key"); 26 | options.sslServerCertificateFile = givenTemporaryFile("server-cert"); 27 | options.sslTrustedCertificateFile = givenTemporaryFile("trusted-cert"); 28 | 29 | options.sslServerKeyFile.setLastModified(System.currentTimeMillis() - INITIAL_FILE_AGE_MILLIS); 30 | options.sslServerCertificateFile.setLastModified(System.currentTimeMillis() - INITIAL_FILE_AGE_MILLIS); 31 | options.sslTrustedCertificateFile.setLastModified(System.currentTimeMillis() - INITIAL_FILE_AGE_MILLIS); 32 | 33 | watcher = new ReloadWatcher(options); 34 | } 35 | 36 | @Test 37 | public void testNoImmediateReload() { 38 | options.sslServerKeyFile.setLastModified(System.currentTimeMillis()); 39 | 40 | assertThat(watcher.needReload()).isFalse(); 41 | } 42 | 43 | @Test 44 | public void testNoReloadWhenFilesAreUntouched() throws InterruptedException { 45 | Thread.sleep(SLEEP_MILLIS); 46 | 47 | assertThat(watcher.needReload()).isFalse(); 48 | } 49 | 50 | @Test 51 | public void testReloadOnceWhenFilesAreTouched() throws InterruptedException { 52 | Thread.sleep(SLEEP_MILLIS); 53 | 54 | options.sslServerKeyFile.setLastModified(System.currentTimeMillis()); 55 | options.sslServerCertificateFile.setLastModified(System.currentTimeMillis()); 56 | 57 | Thread.sleep(SLEEP_MILLIS); 58 | 59 | assertThat(watcher.needReload()).isTrue(); 60 | 61 | Thread.sleep(SLEEP_MILLIS); 62 | 63 | assertThat(watcher.needReload()).isFalse(); 64 | } 65 | 66 | // Verify that we reload certificates on next pass again in case files are modified 67 | // just as we check for reload. 68 | @Test 69 | public void testReloadAgainWhenFilesAreTouchedJustAfterReload() throws InterruptedException { 70 | Thread.sleep(SLEEP_MILLIS); 71 | 72 | options.sslServerKeyFile.setLastModified(System.currentTimeMillis()); 73 | assertThat(watcher.needReload()).isTrue(); 74 | options.sslServerCertificateFile.setLastModified(System.currentTimeMillis()); 75 | 76 | Thread.sleep(SLEEP_MILLIS); 77 | 78 | assertThat(watcher.needReload()).isTrue(); 79 | } 80 | 81 | @Test 82 | public void testReloadWhenForced() throws InterruptedException { 83 | Thread.sleep(SLEEP_MILLIS); 84 | 85 | watcher.forceReload(); 86 | 87 | assertThat(watcher.needReload()).isTrue(); 88 | } 89 | 90 | @Test 91 | public void testNoReloadWhenDisabled() throws InterruptedException { 92 | options.sslReloadIntervalInSeconds = 0; 93 | watcher = new ReloadWatcher(options); 94 | 95 | Thread.sleep(SLEEP_MILLIS); 96 | options.sslServerKeyFile.setLastModified(System.currentTimeMillis()); 97 | 98 | assertThat(watcher.needReload()).isFalse(); 99 | } 100 | 101 | private File givenTemporaryFile(String filename) throws IOException { 102 | File file = File.createTempFile(filename, "tmp"); 103 | Files.write(file.toPath(), "dummy".getBytes()); 104 | 105 | return file; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /common/src/test/java/com/zegelin/cassandra/exporter/netty/ssl/TestSuppressingSslExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package com.zegelin.cassandra.exporter.netty.ssl; 2 | 3 | import io.netty.channel.Channel; 4 | import io.netty.channel.ChannelHandlerContext; 5 | import io.netty.handler.codec.DecoderException; 6 | import io.netty.handler.ssl.NotSslRecordException; 7 | import org.mockito.Mock; 8 | import org.mockito.MockitoAnnotations; 9 | import org.testng.annotations.BeforeMethod; 10 | import org.testng.annotations.Test; 11 | 12 | import javax.net.ssl.SSLHandshakeException; 13 | 14 | import java.net.InetSocketAddress; 15 | 16 | import static org.mockito.ArgumentMatchers.any; 17 | import static org.mockito.Mockito.times; 18 | import static org.mockito.Mockito.verify; 19 | import static org.mockito.Mockito.when; 20 | 21 | 22 | public class TestSuppressingSslExceptionHandler { 23 | @Mock 24 | private ChannelHandlerContext context; 25 | 26 | @Mock 27 | private Channel channel; 28 | 29 | private SuppressingSslExceptionHandler handler; 30 | 31 | @BeforeMethod 32 | public void before() { 33 | MockitoAnnotations.initMocks(this); 34 | handler = new SuppressingSslExceptionHandler(); 35 | 36 | when(context.channel()).thenReturn(channel); 37 | when(channel.remoteAddress()).thenReturn(InetSocketAddress.createUnresolved("127.0.0.1", 12345)); 38 | } 39 | 40 | @Test 41 | public void testNotSslExceptionFromJdkImplementationIsMuted() { 42 | handler.exceptionCaught(context, new NotSslRecordException("Some HTTP_REQUEST in message")); 43 | verify(context, times(0)).fireExceptionCaught(any()); 44 | } 45 | 46 | @Test 47 | public void testSslHandshakeExceptionFromOpenSslImplementationIsMuted() { 48 | handler.exceptionCaught(context, new DecoderException(new SSLHandshakeException("Some HTTP_REQUEST in message"))); 49 | verify(context, times(0)).fireExceptionCaught(any()); 50 | } 51 | 52 | @Test 53 | public void testNotSslRecordExceptionIsMuted() { 54 | handler.exceptionCaught(context, new DecoderException(new NotSslRecordException("Some HTTP_REQUEST in message"))); 55 | verify(context, times(0)).fireExceptionCaught(any()); 56 | } 57 | 58 | @Test 59 | public void testInfoLogDoNotBailOnNullChannel() { 60 | when(context.channel()).thenReturn(null); 61 | handler.exceptionCaught(context, new NotSslRecordException("Some HTTP_REQUEST in message")); 62 | verify(context, times(0)).fireExceptionCaught(any()); 63 | } 64 | 65 | @Test 66 | public void testInfoLogDoNotBailOnNullRemoteAddress() { 67 | when(channel.remoteAddress()).thenReturn(null); 68 | handler.exceptionCaught(context, new NotSslRecordException("Some HTTP_REQUEST in message")); 69 | verify(context, times(0)).fireExceptionCaught(any()); 70 | } 71 | 72 | @Test 73 | public void testOtherExceptionIsPropagated() { 74 | handler.exceptionCaught(context, new NullPointerException()); 75 | verify(context, times(1)).fireExceptionCaught(any()); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /common/src/test/java/com/zegelin/cassandra/exporter/netty/ssl/TestUnexpectedSslExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package com.zegelin.cassandra.exporter.netty.ssl; 2 | 3 | import io.netty.channel.Channel; 4 | import io.netty.channel.ChannelHandlerContext; 5 | import io.netty.handler.codec.DecoderException; 6 | import org.mockito.Mock; 7 | import org.mockito.MockitoAnnotations; 8 | import org.testng.annotations.AfterMethod; 9 | import org.testng.annotations.BeforeMethod; 10 | import org.testng.annotations.Test; 11 | 12 | import javax.net.ssl.SSLException; 13 | import java.net.InetSocketAddress; 14 | 15 | import static org.mockito.ArgumentMatchers.any; 16 | import static org.mockito.Mockito.times; 17 | import static org.mockito.Mockito.verify; 18 | import static org.mockito.Mockito.verifyNoMoreInteractions; 19 | import static org.mockito.Mockito.when; 20 | 21 | 22 | public class TestUnexpectedSslExceptionHandler { 23 | @Mock 24 | private ChannelHandlerContext context; 25 | 26 | @Mock 27 | private Channel channel; 28 | 29 | @Mock 30 | private ReloadWatcher watcher; 31 | 32 | private UnexpectedSslExceptionHandler handler; 33 | 34 | @BeforeMethod 35 | public void before() { 36 | MockitoAnnotations.initMocks(this); 37 | 38 | handler = new UnexpectedSslExceptionHandler(watcher); 39 | 40 | when(context.channel()).thenReturn(channel); 41 | when(channel.remoteAddress()).thenReturn(InetSocketAddress.createUnresolved("127.0.0.1", 12345)); 42 | } 43 | 44 | @AfterMethod 45 | public void after() { 46 | verify(context, times(1)).fireExceptionCaught(any()); 47 | verifyNoMoreInteractions(context, watcher); 48 | } 49 | 50 | @Test 51 | public void testUnexpectedSslExceptionCauseForcedReload() { 52 | handler.exceptionCaught(context, new DecoderException(new SSLException("Received fatal alert: unexpected_message"))); 53 | verify(watcher, times(1)).forceReload(); 54 | } 55 | 56 | @Test 57 | public void testOtherSslExceptionIsPropagated() { 58 | handler.exceptionCaught(context, new DecoderException(new SSLException("Other message"))); 59 | } 60 | 61 | @Test 62 | public void testDecoderExceptionIsPropagated() { 63 | handler.exceptionCaught(context, new DecoderException()); 64 | } 65 | 66 | @Test 67 | public void testOtherExceptionIsPropagated() { 68 | handler.exceptionCaught(context, new NullPointerException()); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /common/src/test/java/com/zegelin/prometheus/domain/TestLabels.java: -------------------------------------------------------------------------------- 1 | package com.zegelin.prometheus.domain; 2 | 3 | import ch.qos.logback.classic.LoggerContext; 4 | import ch.qos.logback.classic.spi.ILoggingEvent; 5 | import ch.qos.logback.core.Appender; 6 | import io.netty.util.ResourceLeakDetector; 7 | import org.mockito.Mock; 8 | import org.mockito.MockitoAnnotations; 9 | import org.slf4j.LoggerFactory; 10 | import org.testng.annotations.AfterClass; 11 | import org.testng.annotations.AfterMethod; 12 | import org.testng.annotations.BeforeClass; 13 | import org.testng.annotations.BeforeMethod; 14 | import org.testng.annotations.Ignore; 15 | import org.testng.annotations.Test; 16 | 17 | import static org.assertj.core.api.Assertions.assertThatCode; 18 | import static org.mockito.Mockito.verifyNoMoreInteractions; 19 | 20 | public class TestLabels { 21 | private static ResourceLeakDetector.Level originalDetectionLevel; 22 | 23 | @Mock 24 | private Appender loggingEventAppender; 25 | 26 | @BeforeClass 27 | public static void beforeClass() { 28 | originalDetectionLevel = ResourceLeakDetector.getLevel(); 29 | ResourceLeakDetector.setLevel(ResourceLeakDetector.Level.PARANOID); 30 | } 31 | 32 | @BeforeMethod 33 | public void beforeMethod() { 34 | MockitoAnnotations.initMocks(this); 35 | 36 | LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory(); 37 | loggerContext.getLogger(ResourceLeakDetector.class).addAppender(loggingEventAppender); 38 | } 39 | 40 | @AfterMethod 41 | public void afterMethod() { 42 | LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory(); 43 | loggerContext.getLogger(ResourceLeakDetector.class).detachAppender(loggingEventAppender); 44 | } 45 | 46 | @AfterClass 47 | public static void afterClass() { 48 | ResourceLeakDetector.setLevel(originalDetectionLevel); 49 | } 50 | 51 | @Ignore ("Fails on a false-positive leak detection in Netty. See #46") 52 | @Test 53 | public void testPlainTextNettyLeakWarning() { 54 | for (int i = 0; i < 10; i++) { 55 | Labels labels = Labels.of("key", "value"); 56 | labels.asPlainTextFormatUTF8EncodedByteBuf(); 57 | Runtime.getRuntime().gc(); 58 | } 59 | 60 | verifyNoMoreInteractions(loggingEventAppender); 61 | } 62 | 63 | @Test 64 | public void testJSONNettyLeakWarning() { 65 | for (int i = 0; i < 10; i++) { 66 | Labels labels = Labels.of("key", "value"); 67 | labels.asJSONFormatUTF8EncodedByteBuf(); 68 | Runtime.getRuntime().gc(); 69 | } 70 | 71 | verifyNoMoreInteractions(loggingEventAppender); 72 | } 73 | 74 | @Test 75 | public void testPlainTextFinalizer() throws Throwable { 76 | Labels labels = Labels.of("key", "value"); 77 | labels.asPlainTextFormatUTF8EncodedByteBuf(); 78 | assertThatCode(() -> labels.finalize()).doesNotThrowAnyException(); 79 | } 80 | 81 | @Test 82 | public void testJSONFinalizer() throws Throwable { 83 | Labels labels = Labels.of("key", "value"); 84 | labels.asJSONFormatUTF8EncodedByteBuf(); 85 | assertThatCode(() -> labels.finalize()).doesNotThrowAnyException(); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /common/src/test/java/com/zegelin/prometheus/exposition/TestFormattedByteChannel.java: -------------------------------------------------------------------------------- 1 | package com.zegelin.prometheus.exposition; 2 | 3 | import org.mockito.Mock; 4 | import org.mockito.MockitoAnnotations; 5 | import org.testng.annotations.BeforeMethod; 6 | import org.testng.annotations.Test; 7 | 8 | import java.nio.ByteBuffer; 9 | 10 | import static org.assertj.core.api.Assertions.assertThat; 11 | import static org.mockito.ArgumentMatchers.any; 12 | import static org.mockito.Mockito.doAnswer; 13 | import static org.mockito.Mockito.when; 14 | 15 | public class TestFormattedByteChannel { 16 | @Mock 17 | private FormattedExposition formattedExposition; 18 | 19 | private ByteBuffer buffer; 20 | private FormattedByteChannel channel; 21 | 22 | @BeforeMethod 23 | public void before() { 24 | MockitoAnnotations.initMocks(this); 25 | buffer = ByteBuffer.allocate(128); 26 | channel = new FormattedByteChannel(formattedExposition); 27 | } 28 | 29 | @Test 30 | public void testClosed() { 31 | when(formattedExposition.isEndOfInput()).thenReturn(true); 32 | 33 | assertThat(channel.read(buffer)).isEqualTo(-1); 34 | assertThat(channel.isOpen()).isEqualTo(false); 35 | } 36 | 37 | @Test 38 | public void testOpen() { 39 | when(formattedExposition.isEndOfInput()).thenReturn(false); 40 | 41 | assertThat(channel.isOpen()).isEqualTo(true); 42 | } 43 | 44 | @Test 45 | public void testOneChunk() { 46 | when(formattedExposition.isEndOfInput()).thenReturn(false).thenReturn(false).thenReturn(true); 47 | doAnswer(invocation -> { 48 | NioExpositionSink sink = invocation.getArgument(0); 49 | sink.writeAscii("abcdefghij"); 50 | return null; 51 | }).when(formattedExposition).nextSlice(any(NioExpositionSink.class)); 52 | 53 | assertThat(channel.read(buffer)).isEqualTo(10); 54 | assertThat(channel.isOpen()).isEqualTo(false); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /common/src/test/java/com/zegelin/prometheus/exposition/TestNettyExpositionSink.java: -------------------------------------------------------------------------------- 1 | package com.zegelin.prometheus.exposition; 2 | 3 | import io.netty.buffer.ByteBuf; 4 | import io.netty.buffer.Unpooled; 5 | import org.testng.annotations.BeforeMethod; 6 | import org.testng.annotations.Test; 7 | 8 | import java.nio.ByteBuffer; 9 | 10 | import static java.nio.charset.StandardCharsets.US_ASCII; 11 | import static org.assertj.core.api.Assertions.assertThat; 12 | 13 | public class TestNettyExpositionSink { 14 | 15 | private NettyExpositionSink sink; 16 | 17 | @BeforeMethod 18 | public void before() { 19 | ByteBuf buffer = Unpooled.buffer(); 20 | sink = new NettyExpositionSink(buffer); 21 | } 22 | 23 | @Test 24 | public void testAsciiCharSize() { 25 | sink.writeByte('a'); 26 | sink.writeByte('b'); 27 | sink.writeByte('c'); 28 | 29 | assertThat(sink.getIngestedByteCount()).isEqualTo(3); 30 | } 31 | 32 | @Test 33 | public void testAsciiStringSize() { 34 | sink.writeAscii("123"); 35 | sink.writeAscii("abc"); 36 | 37 | assertThat(sink.getIngestedByteCount()).isEqualTo(6); 38 | } 39 | 40 | @Test 41 | public void testUtf8StringSize() { 42 | sink.writeUtf8("123"); 43 | sink.writeUtf8("abc"); 44 | sink.writeUtf8("åäö"); 45 | 46 | assertThat(sink.getIngestedByteCount()).isEqualTo(12); 47 | } 48 | 49 | @Test 50 | public void testFloatSize() { 51 | sink.writeFloat(0.123f); 52 | 53 | assertThat(sink.getIngestedByteCount()).isEqualTo(5); 54 | } 55 | 56 | @Test 57 | public void testBufferSize() { 58 | ByteBuffer buffer = ByteBuffer.wrap("abc".getBytes(US_ASCII)); 59 | sink.writeBytes(buffer); 60 | 61 | assertThat(sink.getIngestedByteCount()).isEqualTo(3); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /common/src/test/java/com/zegelin/prometheus/exposition/TestNioExpositionSink.java: -------------------------------------------------------------------------------- 1 | package com.zegelin.prometheus.exposition; 2 | 3 | import org.testng.annotations.BeforeMethod; 4 | import org.testng.annotations.Test; 5 | 6 | import java.nio.ByteBuffer; 7 | 8 | import static java.nio.charset.StandardCharsets.US_ASCII; 9 | import static org.assertj.core.api.Assertions.assertThat; 10 | 11 | public class TestNioExpositionSink { 12 | 13 | private NioExpositionSink sink; 14 | 15 | @BeforeMethod 16 | public void before() { 17 | ByteBuffer buffer = ByteBuffer.allocate(128); 18 | sink = new NioExpositionSink(buffer); 19 | } 20 | 21 | @Test 22 | public void testAsciiCharSize() { 23 | sink.writeByte('a'); 24 | sink.writeByte('b'); 25 | sink.writeByte('c'); 26 | 27 | assertThat(sink.getIngestedByteCount()).isEqualTo(3); 28 | } 29 | 30 | @Test 31 | public void testAsciiStringSize() { 32 | sink.writeAscii("123"); 33 | sink.writeAscii("abc"); 34 | 35 | assertThat(sink.getIngestedByteCount()).isEqualTo(6); 36 | } 37 | 38 | @Test 39 | public void testUtf8StringSize() { 40 | sink.writeUtf8("123"); 41 | sink.writeUtf8("abc"); 42 | sink.writeUtf8("åäö"); 43 | 44 | assertThat(sink.getIngestedByteCount()).isEqualTo(12); 45 | } 46 | 47 | @Test 48 | public void testFloatSize() { 49 | sink.writeFloat(0.123f); 50 | 51 | assertThat(sink.getIngestedByteCount()).isEqualTo(5); 52 | } 53 | 54 | @Test 55 | public void testBufferSize() { 56 | ByteBuffer buffer = ByteBuffer.wrap("abc".getBytes(US_ASCII)); 57 | sink.writeBytes(buffer); 58 | 59 | assertThat(sink.getIngestedByteCount()).isEqualTo(3); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /common/src/test/java/info/adams/ryu/TestRyuFloat.java: -------------------------------------------------------------------------------- 1 | package info.adams.ryu; 2 | 3 | import org.apache.commons.codec.binary.Hex; 4 | import org.testng.annotations.Test; 5 | 6 | import java.nio.ByteBuffer; 7 | 8 | import static java.nio.charset.StandardCharsets.UTF_8; 9 | import static org.assertj.core.api.Assertions.assertThat; 10 | 11 | public class TestRyuFloat { 12 | @Test 13 | public void testFloatToBuffer() { 14 | float f = 0.33007812f; 15 | final ByteBuffer buffer = ByteBuffer.allocate(10); 16 | 17 | int size = RyuFloat.floatToString(buffer, f, RoundingMode.ROUND_EVEN); 18 | 19 | assertThat(size).isEqualTo(10); 20 | assertThat(Hex.encodeHexString(buffer.array())).isEqualTo("302e3333303037383132"); 21 | assertThat(new String(buffer.array(), UTF_8)).isEqualTo("0.33007812"); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /common/src/test/resources/cert/cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDTDCCAjSgAwIBAgIJAKWYKinU7ISSMA0GCSqGSIb3DQEBCwUAMDsxEjAQBgNV 3 | BAMMCWxvY2FsaG9zdDEYMBYGA1UECgwPRXhhbXBsZSBDb21wYW55MQswCQYDVQQG 4 | EwJTRTAeFw0yMDAxMTUxMjI3NTRaFw00NzA2MDIxMjI3NTRaMDsxEjAQBgNVBAMM 5 | CWxvY2FsaG9zdDEYMBYGA1UECgwPRXhhbXBsZSBDb21wYW55MQswCQYDVQQGEwJT 6 | RTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK8ynksuPHrifO5rYUtE 7 | xRjONbYSVpEeWuGTIluX32EItGkporfBiFySbcdEFjC3+P0QJhgDTK2lu5FMjefI 8 | lZYjByVMaaWmnJnW7VjGu2jdvZkFstylSsfFx79XxoHma3b0a6j+OXTR/Hrr2ytL 9 | sp0kTScwo2THbNlmLFIHZgIUvLzwOALHbfOlVDkXrt3/hJnlm+4ssCEYycSRc2lG 10 | lvB0fXLxMbBoJAtsqzeSzl+EptUf2dyB849nwX3XLtYLOI7gY2jKNiMn+ZnEM5IB 11 | lqIPwS0GCqfRPuVUpr+DV4w4n8JK62UD24omnomhRjg1jZuyb5ewDI4nwaB5gjog 12 | JuMCAwEAAaNTMFEwHQYDVR0OBBYEFDLHQ8iMY1GKrOd8sE7TqUsLMmDHMB8GA1Ud 13 | IwQYMBaAFDLHQ8iMY1GKrOd8sE7TqUsLMmDHMA8GA1UdEwEB/wQFMAMBAf8wDQYJ 14 | KoZIhvcNAQELBQADggEBAJReqMSSYuh+YjIb/Xwd1QteRs0bWlHrMReU26XOHWwf 15 | EfYSQgiEAeJmeLPbx10GVhT2TtUCE4zkuesaAx7TpWduS5UPeKmu1ltbZGQWsLsG 16 | k0NuEqWnuOMowkWjKHyA/4TEWbs/eRiwiCfJHNRa9PuJ2luusp78CZPTqereLc1W 17 | wF5ZtbXU6ZXi1dKxR6NXn4ZTBwBJyKq+wCLmNlo0aW4IbH1yUdmvp2CjLpg4GHYt 18 | RkcYNlkBLHqVwxCCoTzATTBk2iyPjU3iI7nH/V8BTn30if9KE51eKzqompTvicxy 19 | 3qORBOHXz+F4vC4rNSy1TcKOt5jcWD7bcFUkPJB4ifU= 20 | -----END CERTIFICATE----- 21 | -------------------------------------------------------------------------------- /common/src/test/resources/cert/key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCvMp5LLjx64nzu 3 | a2FLRMUYzjW2ElaRHlrhkyJbl99hCLRpKaK3wYhckm3HRBYwt/j9ECYYA0ytpbuR 4 | TI3nyJWWIwclTGmlppyZ1u1Yxrto3b2ZBbLcpUrHxce/V8aB5mt29Guo/jl00fx6 5 | 69srS7KdJE0nMKNkx2zZZixSB2YCFLy88DgCx23zpVQ5F67d/4SZ5ZvuLLAhGMnE 6 | kXNpRpbwdH1y8TGwaCQLbKs3ks5fhKbVH9ncgfOPZ8F91y7WCziO4GNoyjYjJ/mZ 7 | xDOSAZaiD8EtBgqn0T7lVKa/g1eMOJ/CSutlA9uKJp6JoUY4NY2bsm+XsAyOJ8Gg 8 | eYI6ICbjAgMBAAECggEATkMyayswYUSjwm29fL4vvbEAVWFDwnfo6TOs+XWSl+on 9 | jYLH6YZOv+u5lnZX41OLqqB4I+n1auzKKVIlYhE8oWDsZEEKQLF461ATnsDIH0RO 10 | 2fitudss2KkFXfh7+LNR9kWhglBuojzbqJ2Lvn+GPqRkwsj7dJ2RhlwaGFqtuueC 11 | 3YNA+HWtN9C+2mq0pfwMlnKbTmOb5/JwRP38lp3101vQ0jhYLGqkNzgPSEj3VFYI 12 | 8CAs1pwaz1Jk42TbFfC4rbDqQ1kUzAN3uoTyVdseUTWFCCTRuD3JIqGleFyb9w8R 13 | A+je7bc4GfCBPzKKHpHAsuvArjzeqd7SMyNalsVeIQKBgQDkEEi1p4RcIAIZGeUC 14 | 7B7Gmt/P4LyqJ3e3e/iR/wy/I222UxRIBgETRUeCNVCMpwb0r8UzTX9nRxfgyV2M 15 | Dc2dsp5PDoU2I5d7L0Pa+OEGlyIgm6SujLMGJrzSoRcC3a8gudHuCvfZeQ54K5HL 16 | XJVZV0z6hErrC/m8NHpFXTQcFQKBgQDEqIse6wf6YPbfqcPWFi2rio4E2OA+9Q/F 17 | 7spTKAa8rov4bMSnBuSy8r0vcRCycQ4bTeIcIOSGYFS831eduJKK0dIm7GBH+OqZ 18 | FiE8IxDJTER92w/GrEdgykBQaUG5sxe2wtnaNjYR77h1a0ioEvYUiWcjxU/SXZST 19 | EDzRZMZdFwKBgFGirpQvoYZkgru/dCVmpeGLoJ/Fn2L8+7J3MtP3yvVEVkpUVMcv 20 | NwVR4DXwpaOjSi8eF+W0UO2EGj0Bd3Xhnv9a56X+2Zo2hUu38H8aZVo5kSLA8+Lz 21 | REXoaeCvfxAskDqTqVyfGncDCTXkyqxTuLYhNNHbtwGJ6NwAoN/ha/y9AoGAOP4j 22 | e21f3YcWZNF9SOEakTlRWURFENnSnWlLx3e1JB9tvyw+fa14wAerHkGlNiGflfgn 23 | TEGYGP138VjEupfQGF0gi1k7ugAAdSc9sID1D3GD8/l1g/1PnlRe+S7X9mpZuASW 24 | QDAv0Vjo21ahMtxz5pW/h1VagbvPICO3jHOpLTcCgYAyi6oiIUsPcTv00wRWwL9R 25 | +gHDxjAyIORnFQlrc6ZtsH9sJpipwyDIT5OiaJ5HZMM8ezq+aTDl0sUUKpkmPtks 26 | 6uIDhkDzBK+86A98nl3tZ92zLvZbZnxCLZRXqQKmZnERCJoQsRW/o92MZFpZonqJ 27 | jKMFKV7ctU50n5yMLhPlsQ== 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /common/src/test/resources/cert/protected-key.pass: -------------------------------------------------------------------------------- 1 | password -------------------------------------------------------------------------------- /common/src/test/resources/cert/protected-key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN ENCRYPTED PRIVATE KEY----- 2 | MIIE4jAcBgoqhkiG9w0BDAEBMA4ECPlGrgu2rA1JAgIIAASCBMCWwmevE5bgkpg2 3 | yaPUmtA3CpUSisRaO2ZSpfECguUEU3GvII1j7y9PTynuu1v7KKab/gKiA2jEj4AO 4 | AUSmzGwdCsT4rzBPq34t2hkAV028YPmrb7KvjPJbqskIguyIbkfszLVdjj5sdzg6 5 | rTdu7p5q9zsxjgI1JJ2Zoh8F5C4CvqXyFx0EOxx1jBWS2unwKT+Cuk4IJ6P9vHDe 6 | skJpnSwHLb9TGVVeJIsqXbQB1/BvCSadLjKfPOUCKwiTMLrqyKi1o8g+kQS4LxCN 7 | oSV6NOmJqRWOZ7JWW3ufi2mYsuNgo6Jc1s0JDIRuGtB9+86CGrm1FW1D83ttvDCX 8 | 9/PE8dcVQ0AwN/bHcpG7ZEjIsDGLVuWFpmhlZJpcRn3AyhaikSiQXyMnkMYBag7P 9 | FNTWIKLPFlfC17A1mvnri1CQTiRF9rorNNe81v+jMa4uyDwixkd5i5xr8mBc9uWE 10 | MB2cKPf7RKHxG4PqPpD1BIuwaCfN+8KVoJ4Q4Nh4WlS+Z679egwpkqodHoU47YB2 11 | P+hcYmH32rNwZTn/AILLYCMCbgEccSkcb52/DY6xDWq+HsrkgeAwYcQDK8sXoALU 12 | s7cEPeQI41S0KMd37Do0E9CfRzxcJO4zQVn1nvnvBv7x5x/v3h0CS6ADzw/igxqY 13 | uHY4dhpRPo5YjKL7nFepr4dW5G2I9GlquLStYYR/mZ8FMhTTFF+8Z/OFNGys12Id 14 | tmUu+gRazExD86J2nnLyQWzmSjCYcObScaBP7i1e5+Ihd8joXjCkwu0VmBsJ6Cci 15 | tJtemzS3p86wdUR9LGYltDmP8vsiVbEr2KRvoi1SZo5STgg7LgaU/dvOrKWeFYmz 16 | yNgnEH2y8aS0N+aYwoSaLxZTfcbhDU/Bmr0osB7Ya6NG2OppKX89HoJyQKl6dYVy 17 | alkqmwCZWTH6fqW6vCnb7jW7P0msIpfA4Gz7f4pxI1blTb+bDVkFGYZymHCiYikw 18 | 8E0fF42CS1HhZhPmaOvqlnhkI2IiIZ4+uFe+zeWUnjUV6gfXO3/OaDl7RCdLIbUN 19 | 8P6vYTrLdTQRsRlEgfW+vqTHtdcWsovjtIHT5xr7LoESOz1uoLQ9Nxv3xAWp2n0N 20 | 7x97LuHE6oICfwUKzMyidlxjosWiulW4fTBiGMuzEr6yYuj74XPDUIPv4+XXApKx 21 | 0u1X5qo3qD7PTe6zZ0FIMvCcTFIctta8xyiovq9gwMajPN94rNCrsjDINQkdfvAZ 22 | gOFVvI3Na3zvCreMp6t4llXRR/i4dkuFe7tk5axJPXS+Ri7F3HIF6WjXpnTyO/Ok 23 | f9fJUBCSgvlNODlGLVc3SEXnFVO0t5/2lraRBuoi/yAEPKJ59tE0la2O7Sg5zn3H 24 | k631neviXTwnhXBdFWXopREaXULa7Z89U/P6i5r/eQGU2bXXb8+KALLgt0UMbhB9 25 | OKMtPjQKgkZ2MWS5PDWwcCCZKIOMbI48uI01BRLXwQtPw+GkgT2dUhksd3jDMfWi 26 | Dic/skbVUvtbc3NEoZqcXj4Vnxal34xT8ukWWoHviB1Sn417xaY+Ro79q4ZvetZi 27 | dyJT7ABbshTwyb15v56vxNZMxzlzoj1n/Bhb8XRC6nFpwIqQ+IA1VXRYMmrS6drC 28 | lCAmx33A 29 | -----END ENCRYPTED PRIVATE KEY----- 30 | -------------------------------------------------------------------------------- /doc/benchmark-results.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/instaclustr/cassandra-exporter/aa129bff8566fe1d84bf78f0e9dd105448562e3f/doc/benchmark-results.png -------------------------------------------------------------------------------- /entrypoint.sh: -------------------------------------------------------------------------------- 1 | #/bin/sh 2 | if [ -z ${CASSANDRA_EXPORTER_USER} ] && [ -z ${CASSANDRA_EXPORTER_PASSWORD} ]; then 3 | java -jar /opt/cassandra_exporter/cassandra_exporter.jar 4 | else 5 | java -jar /opt/cassandra_exporter/cassandra_exporter.jar --jmx-user=CASSANDRA_EXPORTER_USER --jmx-password=CASSANDRA_EXPORTER_PASSWORD --table-labels=TABLE_TYPE --global-labels=CLUSTER,NODE 6 | fi -------------------------------------------------------------------------------- /github-metric-help.py: -------------------------------------------------------------------------------- 1 | import collections 2 | import itertools 3 | import json 4 | import re 5 | import urllib.request 6 | 7 | 8 | def combine_dicts(dicts): 9 | res = collections.defaultdict(list) 10 | 11 | for (key, value) in itertools.chain.from_iterable(map(dict.items, dicts)): 12 | res[key].append(value) 13 | return res 14 | 15 | 16 | label_help = { 17 | r'cassandra_cache.*': { 18 | 'cache': 'The cache name.' 19 | }, 20 | r'cassandra_table_.*': { 21 | 'keyspace': 'The keyspace name.', 22 | 'table': 'The table name.', 23 | 'table_type': 'Type of table: `table`, `index` or `view`.', 24 | 'compaction_strategy_class': 'The compaction strategy class of the table.' 25 | } 26 | } 27 | 28 | def get_label_help(family, label): 29 | for (pattern, labels) in label_help.items(): 30 | if not re.match(pattern, family): 31 | continue 32 | 33 | return labels.get(label, '_No help specified._') 34 | 35 | 36 | 37 | request = urllib.request.Request(url='http://localhost:9500/metrics', headers={'Accept': 'application/json'}) 38 | response = urllib.request.urlopen(request) 39 | 40 | data = json.load(response) 41 | 42 | print('== Contents') 43 | 44 | print('''.Metric Families 45 | |=== 46 | |Metric Family |Type |Help 47 | ''') 48 | 49 | for (familyName, metricFamily) in sorted(data['metricFamilies'].items()): 50 | print('|', '<<_{},{}>>'.format(familyName, familyName)) 51 | print('|', metricFamily['type'].lower()) 52 | print('|', metricFamily.get('help', '_No help specified._')) 53 | print() 54 | 55 | print('|===') 56 | 57 | 58 | 59 | def exclude_system_table_labels(labels): 60 | if labels.get('keyspace') in ('system_traces', 'system_schema', 'system_auth', 'system', 'system_distributed'): 61 | return {} 62 | 63 | return labels 64 | 65 | 66 | 67 | print('== Metric Families') 68 | for (familyName, metricFamily) in sorted(data['metricFamilies'].items()): 69 | print('===', familyName) 70 | print(metricFamily.get('help', '_No help specified._')) 71 | print() 72 | 73 | print('''.Available Labels 74 | |=== 75 | |Label |Help |Example 76 | ''') 77 | 78 | labels = combine_dicts(map(lambda m: exclude_system_table_labels(m.get('labels') or {}), metricFamily['metrics'])) 79 | 80 | if len(labels) == 0: 81 | print('3+| _No labels defined._') 82 | 83 | else: 84 | for label, examples in labels.items(): 85 | print('|', '`{}`'.format(label)) 86 | print('|', get_label_help(familyName, label)) 87 | print('|', ', '.join(map(lambda e: '`{}`'.format(e), set(examples)))) 88 | 89 | print() 90 | 91 | 92 | print('|===') 93 | 94 | 95 | print() 96 | 97 | 98 | -------------------------------------------------------------------------------- /install-ccm.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | find . -path '*/node*/conf/cassandra-env.sh' | while read file; do 4 | echo "Processing $file" 5 | 6 | port=$((19499+$(echo ${file} | sed 's/[^0-9]*//g'))) 7 | 8 | sed -i -e "/cassandra-exporter/d" "${file}" 9 | 10 | echo "JVM_OPTS=\"\$JVM_OPTS -javaagent:/home/adam/Projects/cassandra-exporter/agent/target/cassandra-exporter-agent-0.9.4-SNAPSHOT.jar=--listen=:${port},--cache=true,--enable-collector-timing\"" >> \ 11 | "${file}" 12 | done; -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | 4 | com.zegelin.cassandra-exporter 5 | exporter-parent 6 | 0.9.15-SNAPSHOT 7 | pom 8 | 9 | Cassandra Exporter Parent 10 | 11 | 12 | common 13 | agent 14 | standalone 15 | 16 | 17 | 18 | 4.1.4 19 | 20 | 2.5.3 21 | 3.1.1 22 | 3.1.1 23 | 24 | UTF-8 25 | 26 | 1.8 27 | 1.8 28 | 29 | ${project.build.directory} 30 | 31 | 32 | 33 | scm:git:git://git@github.com:instaclustr/cassandra-exporter.git 34 | scm:git:ssh://github.com/instaclustr/cassandra-exporter.git 35 | git://github.com/instaclustr/cassandra-exporter.git 36 | HEAD 37 | 38 | 39 | 40 | 41 | 42 | com.zegelin.cassandra-exporter 43 | common 44 | 0.9.15-SNAPSHOT 45 | 46 | 47 | org.glassfish.hk2.external 48 | javax.inject 49 | 50 | 51 | 52 | 53 | 54 | org.apache.cassandra 55 | cassandra-all 56 | ${version.cassandra.all} 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | org.apache.maven.plugins 66 | maven-release-plugin 67 | ${version.maven.release.plugin} 68 | 69 | 70 | 71 | org.apache.maven.plugins 72 | maven-dependency-plugin 73 | ${version.maven.dependency.plugin} 74 | 75 | 76 | 77 | org.apache.maven.plugins 78 | maven-shade-plugin 79 | ${version.maven.shade.plugin} 80 | 81 | 82 | ${outputDirectory} 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | org.apache.maven.plugins 91 | maven-release-plugin 92 | 93 | 94 | true 95 | v@{project.version} 96 | 97 | 98 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /standalone/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | 6 | com.zegelin.cassandra-exporter 7 | exporter-parent 8 | 0.9.15-SNAPSHOT 9 | 10 | 11 | standalone 12 | 0.9.15-SNAPSHOT 13 | 14 | Cassandra Exporter Standalone/CLI 15 | 16 | 17 | 18.0 18 | 3.4.0 19 | 4.1.58.Final 20 | 21 | 1.2.3 22 | 1.7.16 23 | 24 | 25 | 26 | 27 | com.zegelin.cassandra-exporter 28 | common 29 | 30 | 31 | 32 | com.google.guava 33 | guava 34 | ${version.guava} 35 | 36 | 37 | 38 | ch.qos.logback 39 | logback-classic 40 | ${version.logback.classic} 41 | 42 | 43 | 44 | org.slf4j 45 | jul-to-slf4j 46 | ${version.jul.to.slf4j} 47 | 48 | 49 | 50 | com.datastax.cassandra 51 | cassandra-driver-core 52 | ${version.cassandra.datastax.driver.core} 53 | 54 | 55 | 56 | netty-handler 57 | io.netty 58 | 59 | 60 | 61 | 62 | 63 | io.netty 64 | netty-all 65 | ${version.netty} 66 | 67 | 68 | 69 | 70 | 71 | 72 | org.apache.maven.plugins 73 | maven-shade-plugin 74 | 75 | cassandra-exporter-standalone-${project.version} 76 | 77 | 78 | 79 | *:* 80 | 81 | META-INF/*.SF 82 | META-INF/*.DSA 83 | META-INF/*.RSA 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | com.zegelin.cassandra.exporter.Application 93 | ${maven.compiler.source} 94 | ${maven.compiler.target} 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | package 103 | 104 | shade 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /standalone/src/main/java/com/zegelin/cassandra/exporter/RemoteMetadataFactory.java: -------------------------------------------------------------------------------- 1 | package com.zegelin.cassandra.exporter; 2 | 3 | import com.datastax.driver.core.*; 4 | import com.datastax.driver.core.policies.LoadBalancingPolicy; 5 | import com.zegelin.cassandra.exporter.MetadataFactory; 6 | 7 | import java.net.InetAddress; 8 | import java.util.Optional; 9 | import java.util.Set; 10 | import java.util.stream.Collectors; 11 | 12 | public class RemoteMetadataFactory extends MetadataFactory { 13 | private final Cluster cluster; 14 | 15 | RemoteMetadataFactory(final Cluster cluster) { 16 | this.cluster = cluster; 17 | } 18 | 19 | @Override 20 | public Optional indexMetadata(final String keyspaceName, final String tableName, final String indexName) { 21 | return Optional.ofNullable(cluster.getMetadata().getKeyspace(keyspaceName)) 22 | .flatMap(k -> Optional.ofNullable(k.getTable(tableName))) 23 | .flatMap(t -> Optional.ofNullable(t.getIndex(indexName))) 24 | .map(i -> new IndexMetadata() { 25 | @Override 26 | public IndexType indexType() { 27 | return IndexType.valueOf(i.getKind().name()); 28 | } 29 | 30 | @Override 31 | public Optional customClassName() { 32 | return Optional.ofNullable(i.getIndexClassName()); 33 | } 34 | }); 35 | } 36 | 37 | @Override 38 | public Optional tableOrViewMetadata(final String keyspaceName, final String tableOrViewName) { 39 | return Optional.ofNullable(cluster.getMetadata().getKeyspace(keyspaceName)) 40 | .flatMap(k -> { 41 | final AbstractTableMetadata tableMetadata = k.getTable(tableOrViewName); 42 | final AbstractTableMetadata materializedViewMetadata = k.getMaterializedView(tableOrViewName); 43 | 44 | return Optional.ofNullable(tableMetadata != null ? tableMetadata : materializedViewMetadata); 45 | }) 46 | .map(m -> new TableMetadata() { 47 | @Override 48 | public String compactionStrategyClassName() { 49 | return m.getOptions().getCompaction().get("class"); 50 | } 51 | 52 | @Override 53 | public boolean isView() { 54 | return (m instanceof MaterializedViewMetadata); 55 | } 56 | }); 57 | } 58 | 59 | @Override 60 | public Set keyspaces() { 61 | return cluster.getMetadata().getKeyspaces().stream().map(KeyspaceMetadata::getName).collect(Collectors.toSet()); 62 | } 63 | 64 | @Override 65 | public Optional endpointMetadata(final InetAddress endpoint) { 66 | return cluster.getMetadata().getAllHosts().stream() 67 | .filter(h -> h.getBroadcastAddress().equals(endpoint)) 68 | .findFirst() 69 | .map(h -> new EndpointMetadata() { 70 | @Override 71 | public String dataCenter() { 72 | return h.getDatacenter(); 73 | } 74 | 75 | @Override 76 | public String rack() { 77 | return h.getRack(); 78 | } 79 | }); 80 | } 81 | 82 | @Override 83 | public String clusterName() { 84 | return cluster.getMetadata().getClusterName(); 85 | } 86 | 87 | @Override 88 | public InetAddress localBroadcastAddress() { 89 | final LoadBalancingPolicy loadBalancingPolicy = cluster.getConfiguration().getPolicies().getLoadBalancingPolicy(); 90 | 91 | // if the LoadBalancingPolicy is correctly configured, this should return just the local host 92 | final Host host = cluster.getMetadata().getAllHosts().stream() 93 | .filter(h -> loadBalancingPolicy.distance(h) == HostDistance.LOCAL) 94 | .findFirst() 95 | .orElseThrow(() -> new IllegalStateException("No Cassandra node with LOCAL distance found.")); 96 | 97 | return host.getBroadcastAddress(); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /standalone/src/main/java/com/zegelin/cassandra/exporter/collector/RemoteGossiperMBeanMetricFamilyCollector.java: -------------------------------------------------------------------------------- 1 | package com.zegelin.cassandra.exporter.collector; 2 | 3 | import com.zegelin.cassandra.exporter.MBeanGroupMetricFamilyCollector; 4 | import com.zegelin.cassandra.exporter.MetadataFactory; 5 | import com.zegelin.prometheus.domain.Labels; 6 | import com.zegelin.prometheus.domain.NumericMetric; 7 | import org.apache.cassandra.gms.FailureDetectorMBean; 8 | import org.apache.cassandra.gms.GossiperMBean; 9 | 10 | import java.net.UnknownHostException; 11 | import java.util.Map; 12 | import java.util.stream.Stream; 13 | 14 | import static com.zegelin.cassandra.exporter.CassandraObjectNames.FAILURE_DETECTOR_MBEAN_NAME; 15 | import static com.zegelin.cassandra.exporter.CassandraObjectNames.GOSSIPER_MBEAN_NAME; 16 | import static com.zegelin.cassandra.exporter.MetricValueConversionFunctions.millisecondsToSeconds; 17 | 18 | public class RemoteGossiperMBeanMetricFamilyCollector extends GossiperMBeanMetricFamilyCollector { 19 | public static MBeanGroupMetricFamilyCollector.Factory factory(final MetadataFactory metadataFactory) { 20 | return mBean -> { 21 | if (GOSSIPER_MBEAN_NAME.apply(mBean.name)) { 22 | return new RemoteGossiperMBeanMetricFamilyCollector(metadataFactory, (GossiperMBean) mBean.object, null); 23 | } 24 | 25 | if (FAILURE_DETECTOR_MBEAN_NAME.apply(mBean.name)) { 26 | return new RemoteGossiperMBeanMetricFamilyCollector(metadataFactory, null, (FailureDetectorMBean) mBean.object); 27 | } 28 | 29 | return null; 30 | }; 31 | } 32 | 33 | private final MetadataFactory metadataFactory; 34 | private final GossiperMBean gossiperMBean; 35 | private final FailureDetectorMBean failureDetectorMBean; 36 | 37 | private RemoteGossiperMBeanMetricFamilyCollector(final MetadataFactory metadataFactory, final GossiperMBean gossiperMBean, final FailureDetectorMBean failureDetectorMBean) { 38 | this.metadataFactory = metadataFactory; 39 | this.gossiperMBean = gossiperMBean; 40 | this.failureDetectorMBean = failureDetectorMBean; 41 | } 42 | 43 | @Override 44 | public MBeanGroupMetricFamilyCollector merge(final MBeanGroupMetricFamilyCollector rawOther) { 45 | if (!(rawOther instanceof RemoteGossiperMBeanMetricFamilyCollector)) { 46 | throw new IllegalStateException(); 47 | } 48 | 49 | final RemoteGossiperMBeanMetricFamilyCollector other = (RemoteGossiperMBeanMetricFamilyCollector) rawOther; 50 | 51 | return new RemoteGossiperMBeanMetricFamilyCollector( 52 | this.metadataFactory, 53 | this.gossiperMBean != null ? this.gossiperMBean : other.gossiperMBean, 54 | this.failureDetectorMBean != null ? this.failureDetectorMBean : other.failureDetectorMBean 55 | ); 56 | } 57 | 58 | @Override 59 | protected void collect(final Stream.Builder generationNumberMetrics, final Stream.Builder downtimeMetrics, final Stream.Builder activeMetrics) { 60 | if (failureDetectorMBean == null || gossiperMBean == null) { 61 | return; 62 | } 63 | 64 | for (final Map.Entry entry : failureDetectorMBean.getSimpleStates().entrySet()) { 65 | // annoyingly getSimpleStates uses InetAddress.toString() which returns "/
" 66 | // yet getCurrentGenerationNumber, etc, all take IP address strings (and internally call InetAddress.getByName(...)) 67 | 68 | final String endpoint = entry.getKey().split("/")[1]; 69 | final String state = entry.getValue(); 70 | 71 | final Labels labels = metadataFactory.endpointLabels(endpoint); 72 | 73 | try { 74 | generationNumberMetrics.add(new NumericMetric(labels, gossiperMBean.getCurrentGenerationNumber(endpoint))); 75 | downtimeMetrics.add(new NumericMetric(labels, millisecondsToSeconds(gossiperMBean.getEndpointDowntime(endpoint)))); 76 | 77 | } catch (final UnknownHostException e) { 78 | throw new RuntimeException("Failed to collect gossip metrics.", e); // TODO: exception or log? 79 | } 80 | 81 | activeMetrics.add(new NumericMetric(labels, state.equalsIgnoreCase("UP") ? 1 : 0)); 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /standalone/src/main/java/org/apache/cassandra/gms/FailureDetectorMBean.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | package org.apache.cassandra.gms; 19 | 20 | import javax.management.openmbean.OpenDataException; 21 | import javax.management.openmbean.TabularData; 22 | import java.net.UnknownHostException; 23 | import java.util.Map; 24 | 25 | public interface FailureDetectorMBean { 26 | public void dumpInterArrivalTimes(); 27 | 28 | public void setPhiConvictThreshold(double phi); 29 | 30 | public double getPhiConvictThreshold(); 31 | 32 | public String getAllEndpointStates(); 33 | 34 | public String getEndpointState(String address) throws UnknownHostException; 35 | 36 | public Map getSimpleStates(); 37 | 38 | public int getDownEndpointCount(); 39 | 40 | public int getUpEndpointCount(); 41 | 42 | public TabularData getPhiValues() throws OpenDataException; 43 | } 44 | -------------------------------------------------------------------------------- /standalone/src/main/java/org/apache/cassandra/gms/GossiperMBean.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | package org.apache.cassandra.gms; 19 | 20 | import java.net.UnknownHostException; 21 | 22 | public interface GossiperMBean 23 | { 24 | public long getEndpointDowntime(String address) throws UnknownHostException; 25 | 26 | public int getCurrentGenerationNumber(String address) throws UnknownHostException; 27 | 28 | public void unsafeAssassinateEndpoint(String address) throws UnknownHostException; 29 | 30 | public void assassinateEndpoint(String address) throws UnknownHostException; 31 | 32 | } -------------------------------------------------------------------------------- /standalone/src/main/java/org/apache/cassandra/locator/EndpointSnitchInfoMBean.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | package org.apache.cassandra.locator; 19 | 20 | import java.net.UnknownHostException; 21 | 22 | /** 23 | * MBean exposing standard Snitch info 24 | */ 25 | public interface EndpointSnitchInfoMBean { 26 | /** 27 | * Provides the Rack name depending on the respective snitch used, given the host name/ip 28 | * 29 | * @param host 30 | * @throws UnknownHostException 31 | */ 32 | public String getRack(String host) throws UnknownHostException; 33 | 34 | /** 35 | * Provides the Datacenter name depending on the respective snitch used, given the hostname/ip 36 | * 37 | * @param host 38 | * @throws UnknownHostException 39 | */ 40 | public String getDatacenter(String host) throws UnknownHostException; 41 | 42 | /** 43 | * Provides the Rack name depending on the respective snitch used for this node 44 | */ 45 | public String getRack(); 46 | 47 | /** 48 | * Provides the Datacenter name depending on the respective snitch used for this node 49 | */ 50 | public String getDatacenter(); 51 | 52 | /** 53 | * Provides the snitch name of the cluster 54 | * 55 | * @return Snitch name 56 | */ 57 | public String getSnitchName(); 58 | } 59 | -------------------------------------------------------------------------------- /standalone/src/main/java/org/apache/cassandra/metrics/CassandraMetricsRegistry.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | package org.apache.cassandra.metrics; 19 | 20 | import javax.management.ObjectName; 21 | 22 | 23 | public class CassandraMetricsRegistry { 24 | public interface MetricMBean { 25 | ObjectName objectName(); 26 | } 27 | 28 | public interface JmxGaugeMBean extends MetricMBean { 29 | Object getValue(); 30 | } 31 | 32 | public interface JmxHistogramMBean extends MetricMBean { 33 | long getCount(); 34 | 35 | long getMin(); 36 | 37 | long getMax(); 38 | 39 | double getMean(); 40 | 41 | double getStdDev(); 42 | 43 | double get50thPercentile(); 44 | 45 | double get75thPercentile(); 46 | 47 | double get95thPercentile(); 48 | 49 | double get98thPercentile(); 50 | 51 | double get99thPercentile(); 52 | 53 | double get999thPercentile(); 54 | 55 | long[] values(); 56 | } 57 | 58 | public interface JmxCounterMBean extends MetricMBean { 59 | long getCount(); 60 | } 61 | 62 | public interface JmxMeterMBean extends MetricMBean { 63 | long getCount(); 64 | 65 | double getMeanRate(); 66 | 67 | double getOneMinuteRate(); 68 | 69 | double getFiveMinuteRate(); 70 | 71 | double getFifteenMinuteRate(); 72 | 73 | String getRateUnit(); 74 | } 75 | 76 | public interface JmxTimerMBean extends JmxMeterMBean { 77 | double getMin(); 78 | 79 | double getMax(); 80 | 81 | double getMean(); 82 | 83 | double getStdDev(); 84 | 85 | double get50thPercentile(); 86 | 87 | double get75thPercentile(); 88 | 89 | double get95thPercentile(); 90 | 91 | double get98thPercentile(); 92 | 93 | double get99thPercentile(); 94 | 95 | double get999thPercentile(); 96 | 97 | long[] values(); 98 | 99 | String getDurationUnit(); 100 | } 101 | } 102 | 103 | 104 | -------------------------------------------------------------------------------- /standalone/src/main/java/org/apache/cassandra/package-info.java: -------------------------------------------------------------------------------- 1 | package org.apache.cassandra; 2 | 3 | // this package exists to contain all the MBean interfaces used by the exporter 4 | // and prevents us from adding Cassandra as a hard dependency (and bloating the output jar) -------------------------------------------------------------------------------- /test/lib/dump.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | from pathlib import Path 3 | from typing import NamedTuple, Any, Union, Iterable, List 4 | 5 | import io 6 | 7 | from frozendict import frozendict 8 | from prometheus_client import Metric 9 | from prometheus_client.parser import text_fd_to_metric_families 10 | import prometheus_client.samples 11 | 12 | 13 | class ValidationResult(NamedTuple): 14 | untyped_families: Any 15 | duplicate_families: Any 16 | duplicate_samples: Any 17 | 18 | # = namedtuple('ValidationResult', ['duplicate_families', 'duplicate_samples']) 19 | #DiffResult = namedtuple('DiffResult', ['added_families', 'removed_families', 'added_samples', 'removed_samples']) 20 | 21 | 22 | class MetricsDump(NamedTuple): 23 | path: Union[str, Path] 24 | metric_families: List[Metric] 25 | 26 | @classmethod 27 | def from_file(cls, path: Path) -> 'MetricsDump': 28 | with open(path, 'rt', encoding='utf-8') as fd: 29 | return MetricsDump.from_lines(fd) 30 | 31 | @classmethod 32 | def from_str(cls, s: str) -> 'MetricsDump': 33 | with io.StringIO(s) as fd: 34 | return MetricsDump.from_lines(fd) 35 | 36 | @classmethod 37 | def from_lines(cls, lines: Iterable[str]) -> 'MetricsDump': 38 | def parse_lines(): 39 | for family in text_fd_to_metric_families(lines): 40 | # freeze the labels dict so its hashable and the keys can be used as a set 41 | #family.samples = [sample._replace(labels=frozendict(sample.labels)) for sample in family.samples] 42 | 43 | yield family 44 | 45 | metric_families = list(parse_lines()) 46 | 47 | path = '' 48 | if isinstance(lines, io.BufferedReader): 49 | path = lines.name 50 | 51 | return MetricsDump(path, metric_families) 52 | 53 | def validate(self) -> ValidationResult: 54 | def find_duplicate_families(): 55 | def family_name_key_fn(f): 56 | return f.name 57 | 58 | families = sorted(self.metric_families, key=family_name_key_fn) # sort by name 59 | family_groups = itertools.groupby(families, key=family_name_key_fn) # group by name 60 | family_groups = [(k, list(group)) for k, group in family_groups] # convert groups to lists 61 | 62 | return {name: group for name, group in family_groups if len(group) > 1} 63 | 64 | def find_duplicate_samples(): 65 | samples = itertools.chain(family.samples for family in self.metric_families) 66 | #sample_groups = 67 | 68 | return 69 | 70 | 71 | return ValidationResult( 72 | duplicate_families=find_duplicate_families(), 73 | duplicate_samples=find_duplicate_samples() 74 | ) 75 | 76 | def diff(self, other: 'MetricsDump'): 77 | pass -------------------------------------------------------------------------------- /test/lib/dump_tests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from lib.dump import MetricsDump 4 | 5 | 6 | class Tests(unittest.TestCase): 7 | def test(self): 8 | dump1 = MetricsDump.from_str(""" 9 | # the following are duplicate families 10 | test_family_d {abc="123"} 0 0 11 | test_family_d {abc="456"} 0 0 12 | """) 13 | 14 | dump2 = MetricsDump.from_str(""" 15 | # the following are duplicate families 16 | # TYPE test_family_d counter 17 | test_family_d {abc="123"} 0 0 18 | test_family_d {abc="456"} 0 0 19 | """) 20 | 21 | pass 22 | 23 | 24 | class ValidationTests(unittest.TestCase): 25 | # def test_invalid_input(self): 26 | # """ 27 | # Test the 28 | # """ 29 | # data = """ 30 | # busted busted busted 31 | # """ 32 | # 33 | # with self.assertRaises(ValueError): 34 | # metric_dump_tool.MetricsDump.from_lines(data) 35 | 36 | def test_duplicate_families(self): 37 | """ 38 | Test that validation finds duplicated metric families 39 | """ 40 | dump = MetricsDump.from_str(""" 41 | # TYPE test_family_a counter 42 | test_family_a {} 1234 1234 43 | 44 | test_family_b {} 0 0 45 | 46 | # TYPE test_family_a gauge 47 | test_family_a {} 5678 1234 48 | 49 | # the following are duplicate samples, not duplicate families 50 | # TYPE test_family_c gauge 51 | test_family_c {} 1234 1234 52 | test_family_c {} 1234 1234 53 | 54 | # the following are duplicate families 55 | test_family_d {abc="123"} 0 0 56 | test_family_d {abc="456"} 0 0 57 | """) 58 | 59 | result = dump.validate() 60 | 61 | self.assertIn('test_family_a', result.duplicate_families) 62 | self.assertIn('test_family_d', result.duplicate_families) 63 | self.assertNotIn('test_family_b', result.duplicate_families) 64 | self.assertNotIn('test_family_c', result.duplicate_families) 65 | 66 | def test_duplicate_samples(self): 67 | """ 68 | Test that validation finds duplicated metric families 69 | """ 70 | dump = MetricsDump.from_lines(""" 71 | # TYPE test_family_a gauge 72 | test_family_a {hello="world"} 1234 1234 73 | test_family_a {hello="world"} 1234 1234 74 | """) 75 | 76 | result = dump.validate() 77 | 78 | self.assertIn('test_family_a', result.duplicate_families) 79 | self.assertNotIn('test_family_b', result.duplicate_families) 80 | 81 | 82 | class DiffTests(unittest.TestCase): 83 | def test_added_families(self): 84 | from_dump = MetricsDump.from_lines(""" 85 | test_family_a {hello="world"} 0 0 86 | """) 87 | 88 | to_dump = MetricsDump.from_lines(""" 89 | test_family_a {hello="world"} 0 0 90 | test_family_a {hello="universe"} 0 0 91 | 92 | test_family_b {} 0 0 93 | """) 94 | 95 | result = from_dump.diff(to_dump) 96 | 97 | self.assertIn('test_family_b', result.added_families) 98 | self.assertNotIn('test_family_a', result.added_families) 99 | 100 | def test_removed_families(self): 101 | from_dump = MetricsDump.from_lines(""" 102 | test_family_a {hello="world"} 0 0 103 | test_family_a {hello="universe"} 0 0 104 | 105 | test_family_b {} 0 0 106 | """) 107 | 108 | to_dump = MetricsDump.from_lines(""" 109 | test_family_a {hello="world"} 0 0 110 | """) 111 | 112 | result = from_dump.diff(to_dump) 113 | 114 | self.assertIn('test_family_b', result.removed_families) 115 | self.assertNotIn('test_family_a', result.removed_families) -------------------------------------------------------------------------------- /test/lib/experiment.py: -------------------------------------------------------------------------------- 1 | import contextlib 2 | import http.server 3 | import logging 4 | import random 5 | import socketserver 6 | import tempfile 7 | import threading 8 | import time 9 | import typing 10 | import unittest 11 | from collections import defaultdict 12 | from datetime import datetime 13 | from enum import Enum, auto 14 | from functools import partial 15 | from pathlib import Path 16 | from typing import Dict 17 | 18 | from frozendict import frozendict 19 | 20 | from lib.net import SocketAddress 21 | from lib.prometheus import PrometheusInstance, RemotePrometheusArchive 22 | 23 | 24 | logging.basicConfig(level=logging.DEBUG) 25 | logger = logging.getLogger(f'{__name__}') 26 | 27 | 28 | ENDPOINT_ADDRESS = SocketAddress('localhost', 9500) 29 | 30 | 31 | class TestMetricsHTTPHandler(http.server.BaseHTTPRequestHandler): 32 | """A test HTTP endpoint for Prometheus to scrape.""" 33 | 34 | 35 | def do_GET(self): 36 | if self.path != '/metrics': 37 | self.send_error(404) 38 | 39 | self.send_response(200) 40 | self.end_headers() 41 | 42 | self.wfile.write(b""" 43 | # TYPE test_counter counter 44 | test_counter {abc="123"} 0 45 | test_counter {abc="456"} 0 46 | 47 | test_untyped {abc="123"} 0 48 | test_untyped {abc="456"} 0 49 | """) 50 | 51 | 52 | cm = contextlib.ExitStack() 53 | 54 | work_dir = Path(cm.enter_context(tempfile.TemporaryDirectory())) 55 | 56 | archive = RemotePrometheusArchive.for_tag('latest').download() 57 | prometheus: PrometheusInstance = cm.enter_context(PrometheusInstance(archive, work_dir)) 58 | 59 | prometheus.start() 60 | 61 | 62 | 63 | httpd = http.server.HTTPServer(ENDPOINT_ADDRESS, TestMetricsHTTPHandler) 64 | thread = threading.Thread(target=httpd.serve_forever, daemon=True) 65 | 66 | prometheus.set_static_scrape_config('test', [ENDPOINT_ADDRESS]) 67 | 68 | thread.start() 69 | 70 | input('Press any key...') 71 | 72 | 73 | httpd.shutdown() 74 | thread.join() 75 | 76 | 77 | cm.close() 78 | 79 | 80 | -------------------------------------------------------------------------------- /test/lib/jar_utils.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | import logging 3 | import re 4 | import subprocess 5 | import typing as t 6 | import zipfile 7 | from enum import Enum 8 | from os import PathLike 9 | from pathlib import Path 10 | from xml.etree import ElementTree 11 | 12 | import click 13 | 14 | from lib.net import SocketAddress 15 | from lib.path_utils import existing_file_arg 16 | 17 | @dataclass 18 | class ExporterJar: 19 | logger = logging.getLogger(f'{__name__}.{__qualname__}') 20 | 21 | class ExporterType(Enum): 22 | AGENT = ('Premain-Class', 'com.zegelin.cassandra.exporter.Agent') 23 | STANDALONE = ('Main-Class', 'com.zegelin.cassandra.exporter.Application') 24 | 25 | def path(self, version: str): 26 | lname = self.name.lower() 27 | return f'{lname}/target/cassandra-exporter-{lname}-{version}.jar' 28 | 29 | path: Path 30 | type: ExporterType 31 | 32 | @classmethod 33 | def from_path(cls, path: PathLike) -> 'ExporterJar': 34 | path = existing_file_arg(path) 35 | 36 | # determine the JAR type (agent or standalone) via the Main/Premain class name listed in the manifest 37 | try: 38 | with zipfile.ZipFile(path) as zf: 39 | manifest = zf.open('META-INF/MANIFEST.MF').readlines() 40 | 41 | def parse_line(line): 42 | m = re.match('(.+): (.+)', line.decode("utf-8").strip()) 43 | return None if m is None else m.groups() 44 | 45 | manifest = dict(filter(None, map(parse_line, manifest))) 46 | 47 | type = next(iter([t for t in ExporterJar.ExporterType if t.value in manifest.items()]), None) 48 | if type is None: 49 | raise ValueError(f'no manifest attribute found that matches known values') 50 | 51 | return cls(path, type) 52 | 53 | except Exception as e: 54 | raise ValueError(f'{path} is not a valid cassandra-exporter jar: {e}') 55 | 56 | @staticmethod 57 | def default_jar_path(type: ExporterType = ExporterType.AGENT) -> Path: 58 | project_dir = Path(__file__).parents[2] 59 | 60 | root_pom = ElementTree.parse(project_dir / 'pom.xml').getroot() 61 | project_version = root_pom.find('{http://maven.apache.org/POM/4.0.0}version').text 62 | 63 | return project_dir / type.path(project_version) 64 | 65 | def __str__(self) -> str: 66 | return f'{self.path} ({self.type.name})' 67 | 68 | def start_standalone(self, listen_address: SocketAddress, 69 | jmx_address: SocketAddress, 70 | cql_address: SocketAddress, 71 | logfile_path: Path): 72 | 73 | self.logger.info('Standalone log file: %s', logfile_path) 74 | 75 | logfile = logfile_path.open('w') # TODO: cleanup 76 | 77 | command = ['java', 78 | '-jar', self.path, 79 | '--listen', listen_address, 80 | '--jmx-service-url', f'service:jmx:rmi:///jndi/rmi://{jmx_address}/jmxrmi', 81 | '--cql-address', cql_address 82 | ] 83 | command = [str(v) for v in command] 84 | 85 | self.logger.debug('Standalone exec(%s)', ' '.join(command)) 86 | 87 | return subprocess.Popen(command, stdout=logfile, stderr=subprocess.STDOUT) 88 | 89 | 90 | class ExporterJarParamType(click.ParamType): 91 | name = "path" 92 | 93 | def convert(self, value: t.Any, param: t.Optional[click.Parameter], ctx: t.Optional[click.Context]) -> ExporterJar: 94 | if isinstance(value, ExporterJar): 95 | return value 96 | 97 | try: 98 | if isinstance(value, str): 99 | for t in ExporterJar.ExporterType: 100 | if t.name.lower() == value.lower(): 101 | return ExporterJar.from_path(ExporterJar.default_jar_path(t)) 102 | 103 | return ExporterJar.from_path(value) 104 | 105 | except Exception as e: 106 | self.fail(str(e), param, ctx) 107 | -------------------------------------------------------------------------------- /test/lib/net.py: -------------------------------------------------------------------------------- 1 | import typing 2 | 3 | 4 | class SocketAddress(typing.NamedTuple): 5 | host: str 6 | port: int 7 | 8 | def __str__(self) -> str: 9 | return f'{self.host}:{self.port}' 10 | -------------------------------------------------------------------------------- /test/lib/path_utils.py: -------------------------------------------------------------------------------- 1 | from os import PathLike 2 | from pathlib import Path 3 | 4 | 5 | def existing_file_arg(path: PathLike): 6 | path = Path(path) 7 | if not path.exists(): 8 | raise ValueError(f'{path}: file does not exist.') 9 | 10 | if not path.is_file(): 11 | raise ValueError(f'{path}: not a regular file.') 12 | 13 | return path 14 | 15 | 16 | def nonexistent_or_empty_directory_arg(path): 17 | path = Path(path) 18 | 19 | if path.exists(): 20 | if not path.is_dir(): 21 | raise ValueError(f'"{path}" must be a directory.') 22 | 23 | if next(path.iterdir(), None) is not None: 24 | raise ValueError(f'"{path}" must be an empty directory.') 25 | 26 | return path -------------------------------------------------------------------------------- /test/lib/schema.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | from dataclasses import dataclass 3 | from os import PathLike 4 | import typing as t 5 | 6 | import click 7 | import yaml 8 | from pathlib import Path 9 | from collections import namedtuple 10 | 11 | from lib.path_utils import existing_file_arg 12 | 13 | @dataclass 14 | class CqlSchema: 15 | path: Path 16 | statements: t.List[str] 17 | 18 | @classmethod 19 | def from_path(cls, path: PathLike) -> 'CqlSchema': 20 | path = existing_file_arg(path) 21 | 22 | with open(path, 'r') as f: 23 | schema = yaml.load(f, Loader=yaml.SafeLoader) 24 | 25 | if not isinstance(schema, list): 26 | raise ValueError(f'root of the schema YAML must be a list. Got a {type(schema).__name__}.') 27 | 28 | for i, o in enumerate(schema): 29 | if not isinstance(o, str): 30 | raise ValueError(f'schema YAML must be a list of statement strings. Item {i} is a {type(o).__name__}.') 31 | 32 | return cls(path, schema) 33 | 34 | @staticmethod 35 | def default_schema_path() -> Path: 36 | test_dir = Path(__file__).parents[1] 37 | return test_dir / "schema.yaml" 38 | 39 | 40 | class CqlSchemaParamType(click.ParamType): 41 | name = "path" 42 | 43 | def convert(self, value: t.Any, param: t.Optional[click.Parameter], ctx: t.Optional[click.Context]) -> CqlSchema: 44 | if isinstance(value, CqlSchema): 45 | return value 46 | 47 | try: 48 | return CqlSchema.from_path(value) 49 | 50 | except Exception as e: 51 | self.fail(str(e), param, ctx) 52 | -------------------------------------------------------------------------------- /test/old/capture_dump.py: -------------------------------------------------------------------------------- 1 | # spin up a multi-node CCM cluster with cassandra-exporter installed, apply a schema, and capture the metrics output 2 | 3 | import argparse 4 | import contextlib 5 | import logging 6 | import os 7 | import tempfile 8 | import urllib.request 9 | from pathlib import Path 10 | 11 | from lib.ccm import TestCluster 12 | from lib.jar_utils import ExporterJar 13 | from lib.schema import CqlSchema 14 | 15 | logging.basicConfig(level=logging.DEBUG) 16 | logger = logging.getLogger(__name__) 17 | 18 | 19 | def cluster_directory(path): 20 | path = Path(path) 21 | 22 | if path.exists(): 23 | if not path.is_dir(): 24 | raise argparse.ArgumentTypeError(f'"{path}" must be a directory.') 25 | 26 | if next(path.iterdir(), None) is not None: 27 | raise argparse.ArgumentTypeError(f'"{path}" must be an empty directory.') 28 | 29 | return path 30 | 31 | 32 | def output_directory(path): 33 | path = Path(path) 34 | 35 | if path.exists(): 36 | if not path.is_dir(): 37 | raise argparse.ArgumentTypeError(f'"{path}" must be a directory.') 38 | 39 | # the empty directory check is done later, since it can be skipped with --overwrite-output 40 | 41 | return path 42 | 43 | 44 | if __name__ == '__main__': 45 | parser = argparse.ArgumentParser() 46 | parser.add_argument('cassandra_version', type=str, help="version of Cassandra to run", metavar="CASSANDRA_VERSION") 47 | parser.add_argument('output_directory', type=output_directory, help="location to write metrics dumps", metavar="OUTPUT_DIRECTORY") 48 | 49 | parser.add_argument('-o', '--overwrite-output', action='store_true', help="don't abort when the output directory exists or is not empty") 50 | 51 | parser.add_argument('--cluster-directory', type=cluster_directory, help="location to install Cassandra. Must be empty or not exist. (default is a temporary directory)") 52 | parser.add_argument('--keep-cluster-directory', type=bool, help="don't delete the cluster directory on exit") 53 | parser.add_argument('--keep-cluster-running', type=bool, help="don't stop the cluster on exit (implies --keep-cluster-directory)") 54 | 55 | parser.add_argument('-d', '--datacenters', type=int, help="number of data centers (default: %(default)s)", default=2) 56 | parser.add_argument('-r', '--racks', type=int, help="number of racks per data center (default: %(default)s)", default=3) 57 | parser.add_argument('-n', '--nodes', type=int, help="number of nodes (default: %(default)s)", default=6) 58 | 59 | parser.add_argument('-j', '--exporter-jar', type=ExporterJar.from_path, help="location of the cassandra-exporter jar, either agent or standalone (default: %(default)s)", default=str(ExporterJar.default_jar_path())) 60 | parser.add_argument('-s', '--schema', type=CqlSchema.from_path, help="CQL schema to apply (default: %(default)s)", default=str(CqlSchema.default_schema_path())) 61 | 62 | args = parser.parse_args() 63 | 64 | if args.cluster_directory is None: 65 | args.cluster_directory = Path(tempfile.mkdtemp()) / "test-cluster" 66 | 67 | if args.output_directory.exists() and not args.overwrite_output: 68 | if next(args.output_directory.iterdir(), None) is not None: 69 | raise argparse.ArgumentTypeError(f'"{args.output_directory}" must be an empty directory.') 70 | 71 | os.makedirs(args.output_directory, exist_ok=True) 72 | 73 | with contextlib.ExitStack() as defer: 74 | logger.info('Setting up Cassandra cluster.') 75 | ccm_cluster = defer.push(TestCluster( 76 | cluster_directory=args.cluster_directory, 77 | cassandra_version=args.cassandra_version, 78 | exporter_jar=args.exporter_jar, 79 | nodes=args.nodes, racks=args.racks, datacenters=args.datacenters, 80 | delete_cluster_on_stop=not args.keep_cluster_directory, 81 | )) 82 | 83 | logger.info('Starting cluster.') 84 | ccm_cluster.start() 85 | 86 | logger.info('Applying CQL schema.') 87 | ccm_cluster.apply_schema(args.schema) 88 | 89 | logger.info('Capturing metrics dump.') 90 | for node in ccm_cluster.nodelist(): 91 | url = f'http://{node.ip_addr}:{node.exporter_port}/metrics?x-accept=text/plain' 92 | destination = args.output_directory / f'{node.name}.txt' 93 | urllib.request.urlretrieve(url, destination) 94 | 95 | logger.info(f'Wrote {url} to {destination}') 96 | -------------------------------------------------------------------------------- /test/old/create_demo_cluster.py: -------------------------------------------------------------------------------- 1 | # spin up a CCM cluster of the specified C* version and Exporter build. 2 | # Useful for testing and demos. 3 | 4 | import argparse 5 | import contextlib 6 | import http.server 7 | import logging 8 | import random 9 | import shutil 10 | import sys 11 | import tempfile 12 | import time 13 | from collections import defaultdict 14 | from pathlib import Path 15 | 16 | import yaml 17 | from frozendict import frozendict 18 | 19 | from lib.ccm import TestCluster 20 | from lib.jar_utils import ExporterJar 21 | from lib.path_utils import nonexistent_or_empty_directory_arg 22 | from lib.prometheus import PrometheusInstance, RemotePrometheusArchive 23 | from lib.schema import CqlSchema 24 | 25 | logging.basicConfig(level=logging.DEBUG) 26 | logger = logging.getLogger(__name__) 27 | 28 | 29 | if __name__ == '__main__': 30 | parser = argparse.ArgumentParser() 31 | parser.add_argument('cassandra_version', type=str, help="version of Cassandra to run", metavar="CASSANDRA_VERSION") 32 | 33 | parser.add_argument('-C', '--working-directory', type=nonexistent_or_empty_directory_arg, 34 | help="location to install Cassandra and Prometheus. Must be empty or not exist. (default is a temporary directory)") 35 | parser.add_argument('--keep-working-directory', help="don't delete the cluster directory on exit", 36 | action='store_true') 37 | 38 | parser.add_argument('-d', '--datacenters', type=int, help="number of data centers (default: %(default)s)", 39 | default=1) 40 | parser.add_argument('-r', '--racks', type=int, help="number of racks per data center (default: %(default)s)", 41 | default=3) 42 | parser.add_argument('-n', '--nodes', type=int, help="number of nodes per data center rack (default: %(default)s)", 43 | default=3) 44 | 45 | ExporterJar.add_jar_argument('--exporter-jar', parser) 46 | CqlSchema.add_schema_argument('--schema', parser) 47 | RemotePrometheusArchive.add_archive_argument('--prometheus-archive', parser) 48 | 49 | args = parser.parse_args() 50 | 51 | if args.working_directory is None: 52 | args.working_directory = Path(tempfile.mkdtemp()) 53 | 54 | 55 | def delete_working_dir(): 56 | shutil.rmtree(args.working_directory) 57 | 58 | 59 | with contextlib.ExitStack() as defer: 60 | if not args.keep_working_directory: 61 | defer.callback(delete_working_dir) # LIFO order -- this gets called last 62 | 63 | logger.info('Setting up Cassandra cluster.') 64 | ccm_cluster = defer.push(TestCluster( 65 | cluster_directory=args.working_directory / 'test-cluster', 66 | cassandra_version=args.cassandra_version, 67 | exporter_jar=args.exporter_jar, 68 | nodes=args.nodes, racks=args.racks, datacenters=args.datacenters, 69 | delete_cluster_on_stop=not args.keep_working_directory, 70 | )) 71 | 72 | 73 | 74 | print('Prometheus scrape config:') 75 | config = {'scrape_configs': [{ 76 | 'job_name': 'cassandra', 77 | 'scrape_interval': '10s', 78 | 'static_configs': [{ 79 | 'targets': [f'http://localhost:{node.exporter_port}' for node in ccm_cluster.nodelist()] 80 | }] 81 | }]} 82 | 83 | yaml.safe_dump(config, sys.stdout) 84 | 85 | ccm_cluster.start() 86 | logger.info("Cluster is now running.") 87 | 88 | input("Press any key to stop cluster...") -------------------------------------------------------------------------------- /test/old/debug_agent.py: -------------------------------------------------------------------------------- 1 | # simple script to launch a single-node CCM cluster with the exporter agent installed, and the C* JVM 2 | # configured to start the remote debugger agent 3 | 4 | import argparse 5 | import os 6 | from pathlib import Path 7 | 8 | from ccmlib.cluster import Cluster 9 | from ccmlib.cluster_factory import ClusterFactory 10 | 11 | from lib.jar_utils import ExporterJar 12 | 13 | 14 | def create_ccm_cluster(cluster_directory: Path, cassandra_version: str, node_count: int): 15 | if cluster_directory.exists(): 16 | cluster_directory.rmdir() # CCM wants to create this 17 | 18 | print('Creating cluster...') 19 | ccm_cluster = Cluster( 20 | path=cluster_directory.parent, 21 | name=cluster_directory.name, 22 | version=cassandra_version, 23 | create_directory=True # if this is false, various config files wont be created... 24 | ) 25 | 26 | ccm_cluster.populate(nodes=node_count) 27 | 28 | return ccm_cluster 29 | 30 | 31 | def yesno_bool(b: bool): 32 | return ('n', 'y')[b] 33 | 34 | 35 | if __name__ == '__main__': 36 | parser = argparse.ArgumentParser() 37 | parser.add_argument('cassandra_version', type=str, help="version of Cassandra to run", metavar="CASSANDRA_VERSION") 38 | parser.add_argument('cluster_directory', type=Path, help="location", metavar="CLUSTER_DIRECTORY") 39 | 40 | parser.add_argument('--jvm-debug-wait-attach', dest='jvm_debug_wait_attach', help="suspend JVM on startup and wait for debugger to attach", action='store_true') 41 | parser.add_argument('--no-jvm-debug-wait-attach', dest='jvm_debug_wait_attach', help="suspend JVM on startup and wait for debugger to attach", action='store_false') 42 | parser.add_argument('--jvm-debug-address', type=str, help="address/port for JVM debug agent to listen on", default='5005') 43 | 44 | parser.add_argument('--exporter-args', type=str, help="exporter agent arguments", default='-l:9500') 45 | 46 | ExporterJar.add_jar_argument('--exporter-jar', parser) 47 | 48 | parser.set_defaults(jvm_debug_wait_attach=True) 49 | 50 | args = parser.parse_args() 51 | 52 | print(f'Cluster directory is: {args.cluster_directory}') 53 | 54 | if not args.cluster_directory.exists() or \ 55 | (args.cluster_directory.exists() and next(args.cluster_directory.iterdir(), None) is None): 56 | 57 | # non-existent or empty directory -- new cluster 58 | ccm_cluster = create_ccm_cluster(args.cluster_directory, args.cassandra_version, node_count=1) 59 | 60 | else: 61 | # existing, non-empty directory -- assume existing cluster 62 | print('Loading cluster...') 63 | ccm_cluster = ClusterFactory.load(args.cluster_directory.parent, args.cluster_directory.name) 64 | 65 | node = ccm_cluster.nodelist()[0] 66 | print(f'Configuring node {node.name}') 67 | 68 | node.set_environment_variable('JVM_OPTS', f'-javaagent:{args.exporter_jar.path}={args.exporter_args} ' 69 | f'-agentlib:jdwp=transport=dt_socket,server=y,suspend={yesno_bool(args.jvm_debug_wait_attach)},address={args.jvm_debug_address}') 70 | 71 | print(f'JVM remote debugger listening on {args.jvm_debug_address}. JVM will suspend on start.') 72 | print('Starting single node cluster...') 73 | 74 | launch_bin = node.get_launch_bin() 75 | args = [launch_bin, '-f'] 76 | env = node.get_env() 77 | 78 | os.execve(launch_bin, args, env) 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /test/old/e2e_test_tests.py: -------------------------------------------------------------------------------- 1 | # Tests for the End-to-End Test! 2 | 3 | import pprint 4 | import unittest 5 | from metric_dump_tool import MetricsDump 6 | import metric_dump_tool 7 | 8 | 9 | class ValidationTests(unittest.TestCase): 10 | pass -------------------------------------------------------------------------------- /test/old/metric_dump_tool_tests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from metric_dump_tool import MetricsDump 3 | import metric_dump_tool 4 | 5 | 6 | class ValidationTests(unittest.TestCase): 7 | # def test_invalid_input(self): 8 | # """ 9 | # Test the 10 | # """ 11 | # data = """ 12 | # busted busted busted 13 | # """ 14 | # 15 | # with self.assertRaises(ValueError): 16 | # metric_dump_tool.MetricsDump.from_lines(data) 17 | 18 | def test_duplicate_families(self): 19 | """ 20 | Test that validation finds duplicated metric families 21 | """ 22 | dump = MetricsDump.from_lines(""" 23 | # TYPE test_family_a counter 24 | test_family_a {} 1234 1234 25 | 26 | test_family_b {} 0 0 27 | 28 | # TYPE test_family_a gauge 29 | test_family_a {} 5678 1234 30 | 31 | # the following are duplicate samples, not duplicate families 32 | # TYPE test_family_c gauge 33 | test_family_c {} 1234 1234 34 | test_family_c {} 1234 1234 35 | 36 | # the following are duplicate families 37 | test_family_d {abc="123"} 0 0 38 | test_family_d {abc="456"} 0 0 39 | """) 40 | 41 | result = metric_dump_tool.validate_dump(dump) 42 | 43 | self.assertIn('test_family_a', result.duplicate_families) 44 | self.assertIn('test_family_d', result.duplicate_families) 45 | self.assertNotIn('test_family_b', result.duplicate_families) 46 | self.assertNotIn('test_family_c', result.duplicate_families) 47 | 48 | def test_duplicate_samples(self): 49 | """ 50 | Test that validation finds duplicated metric families 51 | """ 52 | dump = MetricsDump.from_lines(""" 53 | # TYPE test_family_a gauge 54 | test_family_a {hello="world"} 1234 1234 55 | test_family_a {hello="world"} 1234 1234 56 | """) 57 | 58 | result = metric_dump_tool.validate_dump(dump) 59 | 60 | self.assertIn('test_family_a', result.duplicate_families) 61 | self.assertNotIn('test_family_b', result.duplicate_families) 62 | 63 | 64 | class DiffTests(unittest.TestCase): 65 | def test_added_families(self): 66 | from_dump = MetricsDump.from_lines(""" 67 | test_family_a {hello="world"} 0 0 68 | """) 69 | 70 | to_dump = MetricsDump.from_lines(""" 71 | test_family_a {hello="world"} 0 0 72 | test_family_a {hello="universe"} 0 0 73 | 74 | test_family_b {} 0 0 75 | """) 76 | 77 | result = metric_dump_tool.diff_dump(from_dump, to_dump) 78 | 79 | self.assertIn('test_family_b', result.added_families) 80 | self.assertNotIn('test_family_a', result.added_families) 81 | 82 | def test_removed_families(self): 83 | from_dump = MetricsDump.from_lines(""" 84 | test_family_a {hello="world"} 0 0 85 | test_family_a {hello="universe"} 0 0 86 | 87 | test_family_b {} 0 0 88 | """) 89 | 90 | to_dump = MetricsDump.from_lines(""" 91 | test_family_a {hello="world"} 0 0 92 | """) 93 | 94 | result = metric_dump_tool.diff_dump(from_dump, to_dump) 95 | 96 | self.assertIn('test_family_b', result.removed_families) 97 | self.assertNotIn('test_family_a', result.removed_families) 98 | 99 | 100 | 101 | if __name__ == '__main__': 102 | unittest.main() 103 | -------------------------------------------------------------------------------- /test/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools", "setuptools-scm"] 3 | 4 | [project] 5 | name = "cassandra-exporter" 6 | requires-python = ">=3.8" 7 | dynamic = ["version", "description", "authors", "dependencies"] -------------------------------------------------------------------------------- /test/schema.yaml: -------------------------------------------------------------------------------- 1 | - > 2 | CREATE KEYSPACE example WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1}; 3 | - > 4 | CREATE TABLE example.metric_families ( 5 | name text, 6 | type text, 7 | help text, 8 | PRIMARY KEY (name) 9 | ) 10 | - > 11 | CREATE INDEX ON example.metric_families (type) 12 | - > 13 | CREATE TABLE example.numeric_metrics ( 14 | family text, 15 | labels frozen>, 16 | bucket date, 17 | time timestamp, 18 | value float, 19 | 20 | PRIMARY KEY ((family, labels, bucket), time) 21 | ) 22 | 23 | -------------------------------------------------------------------------------- /test/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name='cassandra-exporter-e2e-tests', 5 | version='1.0', 6 | description='End-to-end testing tools for cassandra-exporter', 7 | author='Adam Zegelin', 8 | author_email='adam@instaclustr.com', 9 | packages=['lib', 'tools'], 10 | install_requires=['ccm', 'prometheus_client', 11 | 'cassandra-driver', 'frozendict', 'pyyaml', 'tqdm', 'click', 12 | 'cloup', 'appdirs', 'cryptography'], 13 | ) 14 | -------------------------------------------------------------------------------- /test/test_tool.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import sys 4 | import time 5 | import typing as t 6 | from itertools import chain 7 | 8 | import pkg_resources 9 | 10 | import cloup 11 | 12 | from lib.ccm import TestCluster, with_ccm_cluster 13 | from lib.click_helpers import with_working_directory 14 | from lib.prometheus import PrometheusInstance, with_prometheus 15 | from tools.dump import dump 16 | 17 | logger = logging.getLogger('test-tool') 18 | 19 | 20 | 21 | 22 | @cloup.group() 23 | def cli(): 24 | pass 25 | 26 | 27 | @cli.command('demo') 28 | @with_working_directory() 29 | @with_ccm_cluster() 30 | def run_demo_cluster(ccm_cluster: TestCluster, **kwargs): 31 | """ 32 | Start a Cassandra cluster with cassandra-exporter installed (agent or standalone). 33 | Optionally setup a schema. 34 | Wait for ctrl-c to shut everything down. 35 | """ 36 | ccm_cluster.start() 37 | 38 | for node in ccm_cluster.nodelist(): 39 | logger.info('Node %s cassandra-exporter running on http://%s', node.name, node.exporter_address) 40 | 41 | sys.stderr.flush() 42 | sys.stdout.flush() 43 | 44 | input("Press any key to stop cluster...") 45 | 46 | 47 | 48 | 49 | @cli.command() 50 | @with_working_directory() 51 | @with_ccm_cluster() 52 | @with_prometheus() 53 | def e2e(ccm_cluster: TestCluster, prometheus: PrometheusInstance, **kwargs): 54 | """ 55 | Run cassandra-exporter end-to-end tests. 56 | 57 | - Start C* with the exporter JAR (agent or standalone). 58 | - Setup a schema. 59 | - Configure and start prometheus. 60 | - Wait for all scrape targets to get healthy. 61 | - Run some tests. 62 | """ 63 | 64 | ccm_cluster.start() 65 | 66 | prometheus.start() 67 | 68 | for node in ccm_cluster.nodelist(): 69 | logger.info('Node %s cassandra-exporter running on http://%s', node.name, node.exporter_address) 70 | 71 | logger.info("Prometheus running on: https://%s", prometheus.listen_address) 72 | 73 | input("Press any key to stop cluster...") 74 | 75 | while True: 76 | targets = prometheus.api.get_targets() 77 | 78 | pass 79 | 80 | # if len(targets['activeTargets']) > 0: 81 | # for target in targets['activeTargets']: 82 | # labels = frozendict(target['labels']) 83 | # 84 | # # even if the target health is unknown, ensure the key exists so the length check below 85 | # # is aware of the target 86 | # history = target_histories[labels] 87 | # 88 | # if target['health'] == 'unknown': 89 | # continue 90 | # 91 | # history[target['lastScrape']] = (target['health'], target['lastError']) 92 | # 93 | # if all([len(v) >= 5 for v in target_histories.values()]): 94 | # break 95 | 96 | time.sleep(1) 97 | 98 | # unhealthy_targets = dict((target, history) for target, history in target_histories.items() 99 | # if any([health != 'up' for (health, error) in history.values()])) 100 | # 101 | # if len(unhealthy_targets): 102 | # logger.error('One or more Prometheus scrape targets was unhealthy.') 103 | # logger.error(unhealthy_targets) 104 | # sys.exit(-1) 105 | 106 | 107 | 108 | @cli.command('benchmark') 109 | @with_working_directory() 110 | @with_ccm_cluster() 111 | def benchmark(ccm_cluster: TestCluster, **kwargs): 112 | """""" 113 | pass 114 | 115 | 116 | 117 | cli.add_command(dump) 118 | 119 | 120 | def main(): 121 | os.environ['CCM_JAVA8_DEBUG'] = 'please' 122 | logging.basicConfig(level=logging.DEBUG) 123 | 124 | # load ccm extensions (useful for ccm-java8, for example) 125 | for entry_point in pkg_resources.iter_entry_points(group='ccm_extension'): 126 | entry_point.load()() 127 | 128 | cli() 129 | 130 | 131 | if __name__ == '__main__': 132 | main() -------------------------------------------------------------------------------- /test/tools/dump.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from pathlib import Path 3 | 4 | import yaml 5 | import urllib.request 6 | import typing as t 7 | 8 | from lib.ccm import TestCluster, with_ccm_cluster 9 | from lib.click_helpers import with_working_directory 10 | 11 | import click 12 | import cloup 13 | 14 | logger = logging.getLogger('dump') 15 | 16 | @cloup.group('dump') 17 | def dump(): 18 | """Commands to capture, validate and diff metrics dumps""" 19 | 20 | 21 | DUMP_MANIFEST_NAME = 'dump-manifest.yaml' 22 | 23 | 24 | @dump.command('capture') 25 | @with_working_directory() 26 | @with_ccm_cluster() 27 | @click.argument('destination') 28 | def dump_capture(ccm_cluster: TestCluster, destination: Path, **kwargs): 29 | """Start a Cassandra cluster, capture metrics from each node's cassandra-exporter and save them to disk.""" 30 | 31 | ccm_cluster.start() 32 | 33 | destination = Path(destination) 34 | destination.mkdir(exist_ok=True) 35 | 36 | logger.info('Capturing metrics dump to %s...', destination) 37 | 38 | with (destination / DUMP_MANIFEST_NAME).open('w') as f: 39 | manifest = { 40 | 'version': '20221207', 41 | 'cassandra': { 42 | 'version': ccm_cluster.version(), 43 | 'topology': { 44 | 'nodes': {n.name: { 45 | 'rack': n.rack, 46 | 'datacenter': n.data_center, 47 | 'ip': n.ip_addr 48 | } for n in ccm_cluster.nodelist()} 49 | } 50 | }, 51 | 'exporter': { 52 | 'version': 'unknown' 53 | } 54 | } 55 | 56 | yaml.safe_dump(manifest, f) 57 | 58 | for node in ccm_cluster.nodelist(): 59 | for mimetype, ext in (('text/plain', 'txt'), ('application/json', 'json')): 60 | url = f'http://{node.exporter_address}/metrics?x-accept={mimetype}' 61 | download_path = destination / f'{node.name}-metrics.{ext}' 62 | 63 | urllib.request.urlretrieve(url, download_path) 64 | 65 | logger.info(f'Wrote {url} to {download_path}') 66 | 67 | 68 | class DumpPathParamType(click.ParamType): 69 | name = 'dump' 70 | 71 | def convert(self, value: t.Any, param: t.Optional[click.Parameter], ctx: t.Optional[click.Context]) -> t.Any: 72 | if isinstance(value, Path): 73 | return value 74 | 75 | p = Path(value) 76 | if p.is_file(): 77 | p = p.parent 78 | 79 | manifest = p / DUMP_MANIFEST_NAME 80 | if not manifest.exists(): 81 | self.fail(f'{p}: not a valid dump: {manifest} does not exist.', param, ctx) 82 | 83 | return p 84 | 85 | 86 | @dump.command('validate') 87 | @click.argument('dump', type=DumpPathParamType()) 88 | def dump_validate(dump: Path, **kwargs): 89 | """Validate a metrics dump using Prometheus's promtool""" 90 | pass 91 | 92 | 93 | @dump.command('diff') 94 | @click.argument('dump1', type=DumpPathParamType()) 95 | @click.argument('dump2', type=DumpPathParamType()) 96 | def dump_diff(dump1: Path, dump2: Path): 97 | """Compare two metrics dumps and output the difference""" 98 | pass 99 | 100 | 101 | 102 | # capture dump (start C* with exporter, fetch and write metrics to file) 103 | # this is every similar to the demo cmd 104 | # validate dump (check for syntax errors, etc) 105 | # compare/diff dump (list metrics added & removed) 106 | --------------------------------------------------------------------------------