├── .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 extends LabelEnum> 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 extends LabelEnum> 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