diff --git a/apm-agent-api/src/main/java/co/elastic/apm/api/AbstractSpanImpl.java b/apm-agent-api/src/main/java/co/elastic/apm/api/AbstractSpanImpl.java index c38226199d..1828c2cbd9 100644 --- a/apm-agent-api/src/main/java/co/elastic/apm/api/AbstractSpanImpl.java +++ b/apm-agent-api/src/main/java/co/elastic/apm/api/AbstractSpanImpl.java @@ -7,9 +7,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. diff --git a/apm-agent-core/src/main/java/co/elastic/apm/agent/configuration/CoreConfiguration.java b/apm-agent-core/src/main/java/co/elastic/apm/agent/configuration/CoreConfiguration.java index 544258b1d0..9e8fd78be3 100644 --- a/apm-agent-core/src/main/java/co/elastic/apm/agent/configuration/CoreConfiguration.java +++ b/apm-agent-core/src/main/java/co/elastic/apm/agent/configuration/CoreConfiguration.java @@ -36,6 +36,8 @@ import java.util.Set; import java.util.TreeSet; +import static co.elastic.apm.agent.configuration.validation.RangeValidator.isInRange; + public class CoreConfiguration extends ConfigurationOptionProvider { public static final String ACTIVE = "active"; @@ -102,16 +104,7 @@ public class CoreConfiguration extends ConfigurationOptionProvider { "To reduce overhead and storage requirements, you can set the sample rate to a value between 0.0 and 1.0. " + "We still record overall time and the result for unsampled transactions, but no context information, tags, or spans.") .dynamic(true) - .addValidator(new ConfigurationOption.Validator() { - @Override - public void assertValid(Double value) { - if (value != null) { - if (value < 0 || value > 1) { - throw new IllegalArgumentException("The sample rate must be between 0 and 1"); - } - } - } - }) + .addValidator(isInRange(0d, 1d)) .buildWithDefault(1.0); private final ConfigurationOption transactionMaxSpans = ConfigurationOption.integerOption() diff --git a/apm-agent-core/src/main/java/co/elastic/apm/agent/configuration/converter/TimeDuration.java b/apm-agent-core/src/main/java/co/elastic/apm/agent/configuration/converter/TimeDuration.java index f7779e11c1..81caf45c28 100644 --- a/apm-agent-core/src/main/java/co/elastic/apm/agent/configuration/converter/TimeDuration.java +++ b/apm-agent-core/src/main/java/co/elastic/apm/agent/configuration/converter/TimeDuration.java @@ -22,7 +22,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -public class TimeDuration { +public class TimeDuration implements Comparable { public static final Pattern DURATION_PATTERN = Pattern.compile("^(-)?(\\d+)(ms|s|m)$"); private final String durationString; @@ -68,4 +68,9 @@ public long getMillis() { public String toString() { return durationString; } + + @Override + public int compareTo(TimeDuration o) { + return Long.compare(durationMs, o.durationMs); + } } diff --git a/apm-agent-core/src/main/java/co/elastic/apm/agent/configuration/validation/RangeValidator.java b/apm-agent-core/src/main/java/co/elastic/apm/agent/configuration/validation/RangeValidator.java new file mode 100644 index 0000000000..ce037d131e --- /dev/null +++ b/apm-agent-core/src/main/java/co/elastic/apm/agent/configuration/validation/RangeValidator.java @@ -0,0 +1,76 @@ +/*- + * #%L + * Elastic APM Java agent + * %% + * Copyright (C) 2018 Elastic and contributors + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package co.elastic.apm.agent.configuration.validation; + +import org.stagemonitor.configuration.ConfigurationOption; + +import javax.annotation.Nullable; + +public class RangeValidator implements ConfigurationOption.Validator { + + @Nullable + private final T min; + @Nullable + private final T max; + private final boolean mustBeInRange; + + private RangeValidator(@Nullable T min, @Nullable T max, boolean mustBeInRange) { + this.min = min; + this.max = max; + this.mustBeInRange = mustBeInRange; + } + + public static RangeValidator isInRange(T min, T max) { + return new RangeValidator<>(min, max, true); + } + + public static RangeValidator isNotInRange(T min, T max) { + return new RangeValidator<>(min, max, false); + } + + public static RangeValidator min(T min) { + return new RangeValidator<>(min, null, true); + } + + public static RangeValidator max(T max) { + return new RangeValidator<>(null, max, true); + } + + @Override + public void assertValid(@Nullable T value) { + if (value != null) { + boolean isInRange = true; + if (min != null) { + isInRange = min.compareTo(value) <= 0; + } + if (max != null) { + isInRange &= value.compareTo(max) <= 0; + } + + if (!isInRange && mustBeInRange) { + throw new IllegalArgumentException(value + " must be in the range [" + min + "," + max + "]"); + } + + if (isInRange && !mustBeInRange) { + throw new IllegalArgumentException(value + " must not be in the range [" + min + "," + max + "]"); + } + } + } +} diff --git a/apm-agent-core/src/main/java/co/elastic/apm/agent/impl/ElasticApmTracer.java b/apm-agent-core/src/main/java/co/elastic/apm/agent/impl/ElasticApmTracer.java index 1028b691fd..0d2b65ce02 100644 --- a/apm-agent-core/src/main/java/co/elastic/apm/agent/impl/ElasticApmTracer.java +++ b/apm-agent-core/src/main/java/co/elastic/apm/agent/impl/ElasticApmTracer.java @@ -29,8 +29,9 @@ import co.elastic.apm.agent.impl.transaction.Span; import co.elastic.apm.agent.impl.transaction.TraceContext; import co.elastic.apm.agent.impl.transaction.Transaction; -import co.elastic.apm.agent.objectpool.ObjectPool; +import co.elastic.apm.agent.metrics.MetricRegistry; import co.elastic.apm.agent.objectpool.Allocator; +import co.elastic.apm.agent.objectpool.ObjectPool; import co.elastic.apm.agent.objectpool.impl.QueueBasedObjectPool; import co.elastic.apm.agent.report.Reporter; import co.elastic.apm.agent.report.ReporterConfiguration; @@ -75,6 +76,7 @@ protected Deque initialValue() { }; private final CoreConfiguration coreConfiguration; private final List spanListeners; + private final MetricRegistry metricRegistry = new MetricRegistry(); private Sampler sampler; ElasticApmTracer(ConfigurationRegistry configurationRegistry, Reporter reporter, Iterable lifecycleListeners, List spanListeners) { @@ -120,6 +122,7 @@ public void onChange(ConfigurationOption configurationOption, Double oldValue for (SpanListener spanListener : spanListeners) { spanListener.init(this); } + reporter.scheduleMetricReporting(metricRegistry, configurationRegistry.getConfig(ReporterConfiguration.class).getMetricsIntervalMs()); } public Transaction startTransaction() { @@ -396,4 +399,8 @@ private void assertIsActive(Object span, @Nullable Object currentlyActive) { } assert span == currentlyActive; } + + public MetricRegistry getMetricRegistry() { + return metricRegistry; + } } diff --git a/apm-agent-core/src/main/java/co/elastic/apm/agent/report/PayloadSender.java b/apm-agent-core/src/main/java/co/elastic/apm/agent/metrics/DoubleSupplier.java similarity index 76% rename from apm-agent-core/src/main/java/co/elastic/apm/agent/report/PayloadSender.java rename to apm-agent-core/src/main/java/co/elastic/apm/agent/metrics/DoubleSupplier.java index 5cde3cbc17..6fd088bfa9 100644 --- a/apm-agent-core/src/main/java/co/elastic/apm/agent/report/PayloadSender.java +++ b/apm-agent-core/src/main/java/co/elastic/apm/agent/metrics/DoubleSupplier.java @@ -17,14 +17,9 @@ * limitations under the License. * #L% */ -package co.elastic.apm.agent.report; +package co.elastic.apm.agent.metrics; -import co.elastic.apm.agent.impl.payload.Payload; +public interface DoubleSupplier { -public interface PayloadSender { - void sendPayload(Payload payload); - - long getReported(); - - long getDropped(); + double get(); } diff --git a/apm-agent-core/src/main/java/co/elastic/apm/agent/metrics/MetricRegistry.java b/apm-agent-core/src/main/java/co/elastic/apm/agent/metrics/MetricRegistry.java new file mode 100644 index 0000000000..5b2e645fa1 --- /dev/null +++ b/apm-agent-core/src/main/java/co/elastic/apm/agent/metrics/MetricRegistry.java @@ -0,0 +1,110 @@ +/*- + * #%L + * Elastic APM Java agent + * %% + * Copyright (C) 2018 Elastic and contributors + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package co.elastic.apm.agent.metrics; + +import co.elastic.apm.agent.report.serialize.MetricRegistrySerializer; +import com.dslplatform.json.DslJson; +import com.dslplatform.json.JsonWriter; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + * A registry for metrics. + *

+ * Currently only holds gauges. + * There are plans to add support for histogram-based timers. + *

+ */ +public class MetricRegistry { + + /** + * Groups {@link MetricSet}s by their unique tags. + */ + private final ConcurrentMap, MetricSet> metricSets = new ConcurrentHashMap<>(); + + /** + * Same as {@link #add(String, Map, DoubleSupplier)} but only adds the metric + * if the {@link DoubleSupplier} does not return {@link Double#NaN} + * + * @param name the name of the metric + * @param tags tags for the metric. + * Tags can be used to create different graphs based for each value of a specific tag name, using a terms aggregation. + * Note that there will be a {@link MetricSet} created for each distinct set of tags. + * @param metric this supplier will be called for every reporting cycle + * ({@link co.elastic.apm.agent.report.ReporterConfiguration#metricsInterval metrics_interval)}) + * @see #add(String, Map, DoubleSupplier) + */ + public void addUnlessNan(String name, Map tags, DoubleSupplier metric) { + if (!Double.isNaN(metric.get())) { + add(name, tags, metric); + } + } + + /** + * Same as {@link #add(String, Map, DoubleSupplier)} but only adds the metric + * if the {@link DoubleSupplier} returns a positive number or zero. + * + * @param name the name of the metric + * @param tags tags for the metric. + * Tags can be used to create different graphs based for each value of a specific tag name, using a terms aggregation. + * Note that there will be a {@link MetricSet} created for each distinct set of tags. + * @param metric this supplier will be called for every reporting cycle + * ({@link co.elastic.apm.agent.report.ReporterConfiguration#metricsInterval metrics_interval)}) + * @see #add(String, Map, DoubleSupplier) + */ + public void addUnlessNegative(String name, Map tags, DoubleSupplier metric) { + if (metric.get() >= 0) { + add(name, tags, metric); + } + } + + /** + * Adds a gauge to the metric registry. + * + * @param name the name of the metric + * @param tags tags for the metric. + * Tags can be used to create different graphs based for each value of a specific tag name, using a terms aggregation. + * Note that there will be a {@link MetricSet} created for each distinct set of tags. + * @param metric this supplier will be called for every reporting cycle + * ({@link co.elastic.apm.agent.report.ReporterConfiguration#metricsInterval metrics_interval)}) + */ + public void add(String name, Map tags, DoubleSupplier metric) { + MetricSet metricSet = metricSets.get(tags); + if (metricSet == null) { + metricSets.putIfAbsent(tags, new MetricSet(tags)); + metricSet = metricSets.get(tags); + } + metricSet.add(name, metric); + } + + public double get(String name, Map tags) { + final MetricSet metricSet = metricSets.get(tags); + if (metricSet != null) { + return metricSet.get(name).get(); + } + return Double.NaN; + } + + public Map, MetricSet> getMetricSets() { + return metricSets; + } +} diff --git a/apm-agent-core/src/main/java/co/elastic/apm/agent/metrics/MetricSet.java b/apm-agent-core/src/main/java/co/elastic/apm/agent/metrics/MetricSet.java new file mode 100644 index 0000000000..ac2c9d9e1d --- /dev/null +++ b/apm-agent-core/src/main/java/co/elastic/apm/agent/metrics/MetricSet.java @@ -0,0 +1,63 @@ +/*- + * #%L + * Elastic APM Java agent + * %% + * Copyright (C) 2018 Elastic and contributors + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package co.elastic.apm.agent.metrics; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + * A metric set is a collection of metrics which have the same tags. + *

+ * A metric set corresponds to one document per + * {@link co.elastic.apm.agent.report.ReporterConfiguration#metricsInterval metrics_interval} in Elasticsearch. + * An alternative would be to have one document per metric but having one document for all metrics with the same tags saves disk space. + *

+ * Example of some serialized metric sets: + *
+ * {"metricset":{"timestamp":1545047730692000,"samples":{"jvm.gc.alloc":{"value":24089200.0}}}}
+ * {"metricset":{"timestamp":1545047730692000,"tags":{"name":"G1 Young Generation"},"samples":{"jvm.gc.time":{"value":0.0},"jvm.gc.count":{"value":0.0}}}}
+ * {"metricset":{"timestamp":1545047730692000,"tags":{"name":"G1 Old Generation"},  "samples":{"jvm.gc.time":{"value":0.0},"jvm.gc.count":{"value":0.0}}}}
+ * 
+ */ +public class MetricSet { + private final Map tags; + private final ConcurrentMap samples = new ConcurrentHashMap<>(); + + public MetricSet(Map tags) { + this.tags = tags; + } + + public void add(String name, DoubleSupplier metric) { + samples.putIfAbsent(name, metric); + } + + DoubleSupplier get(String name) { + return samples.get(name); + } + + public Map getTags() { + return tags; + } + + public Map getSamples() { + return samples; + } +} diff --git a/apm-agent-core/src/main/java/co/elastic/apm/agent/metrics/builtin/JvmGcMetrics.java b/apm-agent-core/src/main/java/co/elastic/apm/agent/metrics/builtin/JvmGcMetrics.java new file mode 100644 index 0000000000..a708252f94 --- /dev/null +++ b/apm-agent-core/src/main/java/co/elastic/apm/agent/metrics/builtin/JvmGcMetrics.java @@ -0,0 +1,92 @@ +/*- + * #%L + * Elastic APM Java agent + * %% + * Copyright (C) 2018 Elastic and contributors + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package co.elastic.apm.agent.metrics.builtin; + +import co.elastic.apm.agent.context.LifecycleListener; +import co.elastic.apm.agent.impl.ElasticApmTracer; +import co.elastic.apm.agent.metrics.DoubleSupplier; +import co.elastic.apm.agent.metrics.MetricRegistry; +import com.sun.management.ThreadMXBean; +import org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement; + +import java.lang.management.GarbageCollectorMXBean; +import java.lang.management.ManagementFactory; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +public class JvmGcMetrics implements LifecycleListener { + + private final List garbageCollectorMXBeans = ManagementFactory.getGarbageCollectorMXBeans(); + + @Override + public void start(ElasticApmTracer tracer) { + bindTo(tracer.getMetricRegistry()); + } + + void bindTo(final MetricRegistry registry) { + for (final GarbageCollectorMXBean garbageCollectorMXBean : garbageCollectorMXBeans) { + final Map tags = Collections.singletonMap("name", garbageCollectorMXBean.getName()); + registry.addUnlessNegative("jvm.gc.count", tags, new DoubleSupplier() { + @Override + public double get() { + return garbageCollectorMXBean.getCollectionCount(); + } + }); + registry.addUnlessNegative("jvm.gc.time", tags, new DoubleSupplier() { + @Override + public double get() { + return garbageCollectorMXBean.getCollectionTime(); + } + }); + } + + try { + // only refer to hotspot specific class via reflection to avoid linkage errors + Class.forName("com.sun.management.ThreadMXBean"); + // in reference to JMH's GC profiler (gc.alloc.rate) + registry.add("jvm.gc.alloc", Collections.emptyMap(), + (DoubleSupplier) Class.forName(getClass().getName() + "$HotspotAllocationSupplier").getEnumConstants()[0]); + } catch (ClassNotFoundException ignore) { + } + } + + @IgnoreJRERequirement + enum HotspotAllocationSupplier implements DoubleSupplier { + INSTANCE; + + final ThreadMXBean threadMXBean = (ThreadMXBean) ManagementFactory.getThreadMXBean(); + + @Override + public double get() { + long allocatedBytes = 0; + for (final long threadAllocatedBytes : threadMXBean.getThreadAllocatedBytes(threadMXBean.getAllThreadIds())) { + if (threadAllocatedBytes > 0) { + allocatedBytes += threadAllocatedBytes; + } + } + return allocatedBytes; + } + } + + @Override + public void stop() throws Exception { + } +} diff --git a/apm-agent-core/src/main/java/co/elastic/apm/agent/metrics/builtin/JvmMemoryMetrics.java b/apm-agent-core/src/main/java/co/elastic/apm/agent/metrics/builtin/JvmMemoryMetrics.java new file mode 100644 index 0000000000..4b099fe143 --- /dev/null +++ b/apm-agent-core/src/main/java/co/elastic/apm/agent/metrics/builtin/JvmMemoryMetrics.java @@ -0,0 +1,81 @@ +/*- + * #%L + * Elastic APM Java agent + * %% + * Copyright (C) 2018 Elastic and contributors + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package co.elastic.apm.agent.metrics.builtin; + +import co.elastic.apm.agent.context.LifecycleListener; +import co.elastic.apm.agent.impl.ElasticApmTracer; +import co.elastic.apm.agent.metrics.DoubleSupplier; +import co.elastic.apm.agent.metrics.MetricRegistry; + +import java.lang.management.ManagementFactory; +import java.lang.management.MemoryMXBean; +import java.util.Collections; + +public class JvmMemoryMetrics implements LifecycleListener { + + @Override + public void start(ElasticApmTracer tracer) { + bindTo(tracer.getMetricRegistry()); + } + + void bindTo(final MetricRegistry registry) { + final MemoryMXBean platformMXBean = ManagementFactory.getPlatformMXBean(MemoryMXBean.class); + registry.add("jvm.memory.heap.used", Collections.emptyMap(), new DoubleSupplier() { + @Override + public double get() { + return platformMXBean.getHeapMemoryUsage().getUsed(); + } + }); + registry.add("jvm.memory.heap.committed", Collections.emptyMap(), new DoubleSupplier() { + @Override + public double get() { + return platformMXBean.getHeapMemoryUsage().getCommitted(); + } + }); + registry.add("jvm.memory.heap.max", Collections.emptyMap(), new DoubleSupplier() { + @Override + public double get() { + return platformMXBean.getHeapMemoryUsage().getMax(); + } + }); + registry.add("jvm.memory.non_heap.used", Collections.emptyMap(), new DoubleSupplier() { + @Override + public double get() { + return platformMXBean.getNonHeapMemoryUsage().getUsed(); + } + }); + registry.add("jvm.memory.non_heap.committed", Collections.emptyMap(), new DoubleSupplier() { + @Override + public double get() { + return platformMXBean.getNonHeapMemoryUsage().getCommitted(); + } + }); + registry.add("jvm.memory.non_heap.max", Collections.emptyMap(), new DoubleSupplier() { + @Override + public double get() { + return platformMXBean.getNonHeapMemoryUsage().getMax(); + } + }); + } + + @Override + public void stop() throws Exception { + } +} diff --git a/apm-agent-core/src/main/java/co/elastic/apm/agent/metrics/builtin/SystemMetrics.java b/apm-agent-core/src/main/java/co/elastic/apm/agent/metrics/builtin/SystemMetrics.java new file mode 100644 index 0000000000..fe6301a160 --- /dev/null +++ b/apm-agent-core/src/main/java/co/elastic/apm/agent/metrics/builtin/SystemMetrics.java @@ -0,0 +1,167 @@ +/*- + * #%L + * Elastic APM Java agent + * %% + * Copyright (C) 2018 Elastic and contributors + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package co.elastic.apm.agent.metrics.builtin; + +import co.elastic.apm.agent.context.LifecycleListener; +import co.elastic.apm.agent.impl.ElasticApmTracer; +import co.elastic.apm.agent.metrics.DoubleSupplier; +import co.elastic.apm.agent.metrics.MetricRegistry; + +import javax.annotation.Nullable; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.lang.management.ManagementFactory; +import java.lang.management.OperatingSystemMXBean; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * Record metrics related to the CPU, gathered by the JVM. + *

+ * Supported JVM implementations: + *

    + *
  • HotSpot
  • + *
  • J9
  • + *
+ *

+ * This implementation is based on io.micrometer.core.instrument.binder.system.ProcessorMetrics, + * under Apache License 2.0 + */ +public class SystemMetrics implements LifecycleListener { + + /** + * List of public, exported interface class names from supported JVM implementations. + */ + private static final List OPERATING_SYSTEM_BEAN_CLASS_NAMES = Arrays.asList( + "com.sun.management.OperatingSystemMXBean", // HotSpot + "com.ibm.lang.management.OperatingSystemMXBean" // J9 + ); + + private final OperatingSystemMXBean operatingSystemBean; + + @Nullable + private final Class operatingSystemBeanClass; + + @Nullable + private final MethodHandle systemCpuUsage; + + @Nullable + private final MethodHandle processCpuUsage; + + @Nullable + private final MethodHandle freeMemory; + + @Nullable + private final MethodHandle totalMemory; + + @Nullable + private final MethodHandle virtualProcessMemory; + + public SystemMetrics() { + this.operatingSystemBean = ManagementFactory.getOperatingSystemMXBean(); + this.operatingSystemBeanClass = getFirstClassFound(OPERATING_SYSTEM_BEAN_CLASS_NAMES); + this.systemCpuUsage = detectMethod("getSystemCpuLoad", double.class); + this.processCpuUsage = detectMethod("getProcessCpuLoad", double.class); + this.freeMemory = detectMethod("getFreePhysicalMemorySize", long.class); + this.totalMemory = detectMethod("getTotalPhysicalMemorySize", long.class); + this.virtualProcessMemory = detectMethod("getCommittedVirtualMemorySize", long.class); + } + + @Override + public void start(ElasticApmTracer tracer) { + bindTo(tracer.getMetricRegistry()); + } + + void bindTo(MetricRegistry metricRegistry) { + metricRegistry.addUnlessNegative("system.cpu.total.norm.pct", Collections.emptyMap(), new DoubleSupplier() { + @Override + public double get() { + return invoke(systemCpuUsage); + } + }); + + metricRegistry.addUnlessNegative("system.process.cpu.total.norm.pct", Collections.emptyMap(), new DoubleSupplier() { + @Override + public double get() { + return invoke(processCpuUsage); + } + }); + + metricRegistry.addUnlessNan("system.memory.total", Collections.emptyMap(), new DoubleSupplier() { + @Override + public double get() { + return invoke(totalMemory); + } + }); + + metricRegistry.addUnlessNan("system.memory.actual.free", Collections.emptyMap(), new DoubleSupplier() { + @Override + public double get() { + return invoke(freeMemory); + } + }); + + metricRegistry.addUnlessNegative("system.process.memory.size", Collections.emptyMap(), new DoubleSupplier() { + @Override + public double get() { + return invoke(virtualProcessMemory); + } + }); + } + + private double invoke(@Nullable MethodHandle method) { + try { + return method != null ? (double) method.invoke(operatingSystemBean) : Double.NaN; + } catch (Throwable e) { + return Double.NaN; + } + } + + @Nullable + private MethodHandle detectMethod(String name, Class returnType) { + if (operatingSystemBeanClass == null) { + return null; + } + try { + // ensure the Bean we have is actually an instance of the interface + operatingSystemBeanClass.cast(operatingSystemBean); + return MethodHandles.lookup().findVirtual(operatingSystemBeanClass, name, MethodType.methodType(returnType)); + } catch (ClassCastException | NoSuchMethodException | SecurityException | IllegalAccessException e) { + return null; + } + } + + @Nullable + private Class getFirstClassFound(List classNames) { + for (String className : classNames) { + try { + return Class.forName(className); + } catch (ClassNotFoundException ignore) { + } + } + return null; + } + + @Override + public void stop() throws Exception { + } +} diff --git a/apm-agent-core/src/main/java/co/elastic/apm/agent/metrics/builtin/package-info.java b/apm-agent-core/src/main/java/co/elastic/apm/agent/metrics/builtin/package-info.java new file mode 100644 index 0000000000..384ed50bde --- /dev/null +++ b/apm-agent-core/src/main/java/co/elastic/apm/agent/metrics/builtin/package-info.java @@ -0,0 +1,28 @@ +/*- + * #%L + * Elastic APM Java agent + * %% + * Copyright (C) 2018 Elastic and contributors + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +/** + * We can't use micrometer directly as it does not support Java 7 which we want to support with the Java agent + * Also, micrometer does not have a native concept of {@link co.elastic.apm.agent.metrics.MetricSet}s, + * so converting to metricsets in a reporter (group metrics by tags) would introduce some overhead. + */ +@NonnullApi +package co.elastic.apm.agent.metrics.builtin; + +import co.elastic.apm.agent.annotation.NonnullApi; diff --git a/apm-agent-core/src/main/java/co/elastic/apm/agent/metrics/package-info.java b/apm-agent-core/src/main/java/co/elastic/apm/agent/metrics/package-info.java new file mode 100644 index 0000000000..4682641d70 --- /dev/null +++ b/apm-agent-core/src/main/java/co/elastic/apm/agent/metrics/package-info.java @@ -0,0 +1,23 @@ +/*- + * #%L + * Elastic APM Java agent + * %% + * Copyright (C) 2018 Elastic and contributors + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +@NonnullApi +package co.elastic.apm.agent.metrics; + +import co.elastic.apm.agent.annotation.NonnullApi; diff --git a/apm-agent-core/src/main/java/co/elastic/apm/agent/report/ApmServerReporter.java b/apm-agent-core/src/main/java/co/elastic/apm/agent/report/ApmServerReporter.java index a2089ea1a5..1acf0970a7 100644 --- a/apm-agent-core/src/main/java/co/elastic/apm/agent/report/ApmServerReporter.java +++ b/apm-agent-core/src/main/java/co/elastic/apm/agent/report/ApmServerReporter.java @@ -22,7 +22,8 @@ import co.elastic.apm.agent.impl.error.ErrorCapture; import co.elastic.apm.agent.impl.transaction.Span; import co.elastic.apm.agent.impl.transaction.Transaction; -import co.elastic.apm.agent.objectpool.Recyclable; +import co.elastic.apm.agent.metrics.MetricRegistry; +import co.elastic.apm.agent.util.ExecutorUtils; import co.elastic.apm.agent.util.MathUtils; import com.lmax.disruptor.EventFactory; import com.lmax.disruptor.EventTranslator; @@ -32,7 +33,9 @@ import com.lmax.disruptor.dsl.Disruptor; import com.lmax.disruptor.dsl.ProducerType; +import javax.annotation.Nullable; import java.util.concurrent.Future; +import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; @@ -76,6 +79,8 @@ public void translateTo(ReportingEvent event, long sequence, ErrorCapture error) private final boolean dropTransactionIfQueueFull; private final ReportingEventHandler reportingEventHandler; private final boolean syncReport; + @Nullable + private ScheduledThreadPoolExecutor metricsReportingScheduler; public ApmServerReporter(boolean dropTransactionIfQueueFull, ReporterConfiguration reporterConfiguration, ReportingEventHandler reportingEventHandler) { @@ -203,6 +208,9 @@ private boolean isEventProcessed(long sequence) { public void close() { disruptor.shutdown(); reportingEventHandler.close(); + if (metricsReportingScheduler != null) { + metricsReportingScheduler.shutdown(); + } } @Override @@ -215,7 +223,25 @@ public void report(ErrorCapture error) { } } - private boolean tryAddEventToRingBuffer(E event, EventTranslatorOneArg eventTranslator) { + @Override + public void scheduleMetricReporting(final MetricRegistry metricRegistry, long intervalMs) { + if (intervalMs > 0 && metricsReportingScheduler == null) { + metricsReportingScheduler = ExecutorUtils.createSingleThreadSchedulingDeamonPool("apm-metrics-reporter", 1); + metricsReportingScheduler.scheduleAtFixedRate(new Runnable() { + @Override + public void run() { + disruptor.publishEvent(new EventTranslatorOneArg() { + @Override + public void translateTo(ReportingEvent event, long sequence, MetricRegistry metricRegistry) { + event.reportMetrics(metricRegistry); + } + }, metricRegistry); + } + }, intervalMs, intervalMs, TimeUnit.MILLISECONDS); + } + } + + private boolean tryAddEventToRingBuffer(E event, EventTranslatorOneArg eventTranslator) { if (dropTransactionIfQueueFull) { boolean queueFull = !disruptor.getRingBuffer().tryPublishEvent(eventTranslator, event); if (queueFull) { diff --git a/apm-agent-core/src/main/java/co/elastic/apm/agent/report/IntakeV2ReportingEventHandler.java b/apm-agent-core/src/main/java/co/elastic/apm/agent/report/IntakeV2ReportingEventHandler.java index ae29163fb7..a7de06e3c4 100644 --- a/apm-agent-core/src/main/java/co/elastic/apm/agent/report/IntakeV2ReportingEventHandler.java +++ b/apm-agent-core/src/main/java/co/elastic/apm/agent/report/IntakeV2ReportingEventHandler.java @@ -191,6 +191,8 @@ private void writeEvent(ReportingEvent event) { currentlyTransmitting++; payloadSerializer.serializeErrorNdJson(event.getError()); event.getError().recycle(); + } else if (event.getMetricRegistry() != null) { + payloadSerializer.serializeMetrics(event.getMetricRegistry()); } } @@ -249,8 +251,8 @@ private HttpURLConnection startRequest() { URL getUrl() throws MalformedURLException { URL serverUrl = serverUrlIterator.get(); String path = serverUrl.getPath(); - if(path.endsWith("/")) { - path = path.substring(0, path.length()-1); + if (path.endsWith("/")) { + path = path.substring(0, path.length() - 1); } return new URL(serverUrl, path + INTAKE_V2_URL); } diff --git a/apm-agent-core/src/main/java/co/elastic/apm/agent/report/Reporter.java b/apm-agent-core/src/main/java/co/elastic/apm/agent/report/Reporter.java index ffe07eff35..7575c24efe 100644 --- a/apm-agent-core/src/main/java/co/elastic/apm/agent/report/Reporter.java +++ b/apm-agent-core/src/main/java/co/elastic/apm/agent/report/Reporter.java @@ -22,6 +22,7 @@ import co.elastic.apm.agent.impl.error.ErrorCapture; import co.elastic.apm.agent.impl.transaction.Span; import co.elastic.apm.agent.impl.transaction.Transaction; +import co.elastic.apm.agent.metrics.MetricRegistry; import java.io.Closeable; import java.util.concurrent.Future; @@ -41,4 +42,6 @@ public interface Reporter extends Closeable { void close(); void report(ErrorCapture error); + + void scheduleMetricReporting(MetricRegistry metricRegistry, long intervalMs); } diff --git a/apm-agent-core/src/main/java/co/elastic/apm/agent/report/ReporterConfiguration.java b/apm-agent-core/src/main/java/co/elastic/apm/agent/report/ReporterConfiguration.java index 27a0c0d9a2..f8220b8fe8 100644 --- a/apm-agent-core/src/main/java/co/elastic/apm/agent/report/ReporterConfiguration.java +++ b/apm-agent-core/src/main/java/co/elastic/apm/agent/report/ReporterConfiguration.java @@ -32,6 +32,8 @@ import java.util.Collections; import java.util.List; +import static co.elastic.apm.agent.configuration.validation.RangeValidator.isNotInRange; + public class ReporterConfiguration extends ConfigurationOptionProvider { public static final String REPORTER_CATEGORY = "Reporter"; private final ConfigurationOption secretToken = ConfigurationOption.stringOption() @@ -133,6 +135,15 @@ public class ReporterConfiguration extends ConfigurationOptionProvider { "Allowed byte units are `b`, `kb` and `mb`. `1kb` is equal to `1024b`.") .buildWithDefault(ByteValue.of("768kb")); + private final ConfigurationOption metricsInterval = TimeDurationValueConverter.durationOption("s") + .key("metrics_interval") + .configurationCategory(REPORTER_CATEGORY) + .description("The interval at which the agent sends metrics to the APM Server.\n" + + "Must be at least `1s`.\n" + + "Set to `0s` to deactivate.") + .addValidator(isNotInRange(TimeDuration.of("1ms"), TimeDuration.of("999ms"))) + .buildWithDefault(TimeDuration.of("30s")); + @Nullable public String getSecretToken() { return secretToken.get(); @@ -173,4 +184,8 @@ public TimeDuration getApiRequestTime() { public long getApiRequestSize() { return apiRequestSize.get().getBytes(); } + + public long getMetricsIntervalMs() { + return metricsInterval.get().getMillis(); + } } diff --git a/apm-agent-core/src/main/java/co/elastic/apm/agent/report/ReporterFactory.java b/apm-agent-core/src/main/java/co/elastic/apm/agent/report/ReporterFactory.java index d7a6a9df85..e64c196307 100644 --- a/apm-agent-core/src/main/java/co/elastic/apm/agent/report/ReporterFactory.java +++ b/apm-agent-core/src/main/java/co/elastic/apm/agent/report/ReporterFactory.java @@ -40,9 +40,6 @@ public class ReporterFactory { - private static final Logger logger = LoggerFactory.getLogger(ReporterFactory.class); - private final String userAgent = getUserAgent(); - public Reporter createReporter(ConfigurationRegistry configurationRegistry, @Nullable String frameworkName, @Nullable String frameworkVersion) { final ReporterConfiguration reporterConfiguration = configurationRegistry.getConfig(ReporterConfiguration.class); diff --git a/apm-agent-core/src/main/java/co/elastic/apm/agent/report/ReportingEvent.java b/apm-agent-core/src/main/java/co/elastic/apm/agent/report/ReportingEvent.java index fe85decf76..bd5c23f50e 100644 --- a/apm-agent-core/src/main/java/co/elastic/apm/agent/report/ReportingEvent.java +++ b/apm-agent-core/src/main/java/co/elastic/apm/agent/report/ReportingEvent.java @@ -22,11 +22,13 @@ import co.elastic.apm.agent.impl.error.ErrorCapture; import co.elastic.apm.agent.impl.transaction.Span; import co.elastic.apm.agent.impl.transaction.Transaction; +import co.elastic.apm.agent.metrics.MetricRegistry; import javax.annotation.Nullable; import static co.elastic.apm.agent.report.ReportingEvent.ReportingEventType.ERROR; import static co.elastic.apm.agent.report.ReportingEvent.ReportingEventType.FLUSH; +import static co.elastic.apm.agent.report.ReportingEvent.ReportingEventType.METRICS; import static co.elastic.apm.agent.report.ReportingEvent.ReportingEventType.SPAN; import static co.elastic.apm.agent.report.ReportingEvent.ReportingEventType.TRANSACTION; @@ -39,12 +41,15 @@ public class ReportingEvent { private ErrorCapture error; @Nullable private Span span; + @Nullable + private MetricRegistry metricRegistry; public void resetState() { this.transaction = null; this.type = null; this.error = null; this.span = null; + this.metricRegistry = null; } @Nullable @@ -86,7 +91,17 @@ public void setSpan(Span span) { this.type = SPAN; } + public void reportMetrics(MetricRegistry metricRegistry) { + this.metricRegistry = metricRegistry; + this.type = METRICS; + } + + @Nullable + public MetricRegistry getMetricRegistry() { + return metricRegistry; + } + enum ReportingEventType { - FLUSH, TRANSACTION, SPAN, ERROR + FLUSH, TRANSACTION, SPAN, ERROR, METRICS } } diff --git a/apm-agent-core/src/main/java/co/elastic/apm/agent/report/serialize/DslJsonSerializer.java b/apm-agent-core/src/main/java/co/elastic/apm/agent/report/serialize/DslJsonSerializer.java index 91a49a302f..3803fa833a 100644 --- a/apm-agent-core/src/main/java/co/elastic/apm/agent/report/serialize/DslJsonSerializer.java +++ b/apm-agent-core/src/main/java/co/elastic/apm/agent/report/serialize/DslJsonSerializer.java @@ -46,6 +46,7 @@ import co.elastic.apm.agent.impl.transaction.SpanCount; import co.elastic.apm.agent.impl.transaction.TraceContext; import co.elastic.apm.agent.impl.transaction.Transaction; +import co.elastic.apm.agent.metrics.MetricRegistry; import co.elastic.apm.agent.util.PotentiallyMultiValuedMap; import com.dslplatform.json.BoolConverter; import com.dslplatform.json.DslJson; @@ -190,6 +191,11 @@ public int getBufferSize() { return jw.size(); } + @Override + public void serializeMetrics(MetricRegistry metricRegistry) { + MetricRegistrySerializer.serialize(metricRegistry, replaceBuilder, jw); + } + private void serializeErrorPayload(ErrorPayload payload) { jw.writeByte(JsonWriter.OBJECT_START); serializeService(payload.getService()); @@ -616,35 +622,39 @@ private void serializeContext(final TransactionContext context) { // visible for testing void serializeTags(Map value) { + serializeTags(value, replaceBuilder, jw); + } + + public static void serializeTags(Map value, StringBuilder replaceBuilder, JsonWriter jw) { jw.writeByte(OBJECT_START); final int size = value.size(); if (size > 0) { final Iterator> iterator = value.entrySet().iterator(); Map.Entry kv = iterator.next(); - writeStringValue(sanitizeTagKey(kv.getKey())); + writeStringValue(sanitizeTagKey(kv.getKey(), replaceBuilder), replaceBuilder, jw); jw.writeByte(JsonWriter.SEMI); - writeStringValue(kv.getValue()); + writeStringValue(kv.getValue(), replaceBuilder, jw); for (int i = 1; i < size; i++) { jw.writeByte(COMMA); kv = iterator.next(); - writeStringValue(sanitizeTagKey(kv.getKey())); + writeStringValue(sanitizeTagKey(kv.getKey(), replaceBuilder), replaceBuilder, jw); jw.writeByte(JsonWriter.SEMI); - writeStringValue(kv.getValue()); + writeStringValue(kv.getValue(), replaceBuilder, jw); } } jw.writeByte(OBJECT_END); } - private CharSequence sanitizeTagKey(String key) { + private static CharSequence sanitizeTagKey(String key, StringBuilder replaceBuilder) { for (int i = 0; i < DISALLOWED_IN_TAG_KEY.length; i++) { if (key.contains(DISALLOWED_IN_TAG_KEY[i])) { - return replaceAll(key, DISALLOWED_IN_TAG_KEY, "_"); + return replaceAll(key, DISALLOWED_IN_TAG_KEY, "_", replaceBuilder); } } return key; } - private CharSequence replaceAll(String s, String[] stringsToReplace, String replacement) { + private static CharSequence replaceAll(String s, String[] stringsToReplace, String replacement, StringBuilder replaceBuilder) { // uses a instance variable StringBuilder to avoid allocations replaceBuilder.setLength(0); replaceBuilder.append(s); @@ -654,7 +664,7 @@ private CharSequence replaceAll(String s, String[] stringsToReplace, String repl return replaceBuilder; } - private void replace(StringBuilder replaceBuilder, String toReplace, String replacement) { + private static void replace(StringBuilder replaceBuilder, String toReplace, String replacement) { for (int i = replaceBuilder.indexOf(toReplace); i != -1; i = replaceBuilder.indexOf(toReplace)) { replaceBuilder.replace(i, i + replacement.length(), replacement); } @@ -789,6 +799,10 @@ void writeField(final String fieldName, @Nullable final String value) { } private void writeStringBuilderValue(StringBuilder value) { + writeStringBuilderValue(value, jw); + } + + private static void writeStringBuilderValue(StringBuilder value, JsonWriter jw) { if (value.length() > MAX_VALUE_LENGTH) { value.setLength(MAX_VALUE_LENGTH - 1); value.append('…'); @@ -797,10 +811,14 @@ private void writeStringBuilderValue(StringBuilder value) { } private void writeStringValue(CharSequence value) { + writeStringValue(value, replaceBuilder, jw); + } + + private static void writeStringValue(CharSequence value, StringBuilder replaceBuilder, JsonWriter jw) { if (value.length() > MAX_VALUE_LENGTH) { replaceBuilder.setLength(0); replaceBuilder.append(value, 0, Math.min(value.length(), MAX_VALUE_LENGTH + 1)); - writeStringBuilderValue(replaceBuilder); + writeStringBuilderValue(replaceBuilder, jw); } else { jw.writeString(value); } @@ -867,13 +885,17 @@ void writeLastField(final String fieldName, @Nullable final String value) { } } - private void writeFieldName(final String fieldName) { + public static void writeFieldName(final String fieldName, final JsonWriter jw) { jw.writeByte(JsonWriter.QUOTE); jw.writeAscii(fieldName); jw.writeByte(JsonWriter.QUOTE); jw.writeByte(JsonWriter.SEMI); } + private void writeFieldName(final String fieldName) { + writeFieldName(fieldName, jw); + } + private void writeField(final String fieldName, final List values) { if (values.size() > 0) { writeFieldName(fieldName); diff --git a/apm-agent-core/src/main/java/co/elastic/apm/agent/report/serialize/MetricRegistrySerializer.java b/apm-agent-core/src/main/java/co/elastic/apm/agent/report/serialize/MetricRegistrySerializer.java new file mode 100644 index 0000000000..e9c982aa97 --- /dev/null +++ b/apm-agent-core/src/main/java/co/elastic/apm/agent/report/serialize/MetricRegistrySerializer.java @@ -0,0 +1,93 @@ +/*- + * #%L + * Elastic APM Java agent + * %% + * Copyright (C) 2018 Elastic and contributors + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package co.elastic.apm.agent.report.serialize; + +import co.elastic.apm.agent.metrics.DoubleSupplier; +import co.elastic.apm.agent.metrics.MetricRegistry; +import co.elastic.apm.agent.metrics.MetricSet; +import com.dslplatform.json.JsonWriter; +import com.dslplatform.json.NumberConverter; + +import java.util.Iterator; +import java.util.Map; + +public class MetricRegistrySerializer { + + private static final byte NEW_LINE = '\n'; + + public static void serialize(MetricRegistry metricRegistry, StringBuilder replaceBuilder, JsonWriter jw) { + final long timestamp = System.currentTimeMillis() * 1000; + for (MetricSet metricSet : metricRegistry.getMetricSets().values()) { + serializeMetricSet(metricSet, timestamp, replaceBuilder, jw); + jw.writeByte(NEW_LINE); + } + } + + static void serializeMetricSet(MetricSet metricSet, long epochMicros, StringBuilder replaceBuilder, JsonWriter jw) { + jw.writeByte(JsonWriter.OBJECT_START); + { + DslJsonSerializer.writeFieldName("metricset", jw); + jw.writeByte(JsonWriter.OBJECT_START); + { + DslJsonSerializer.writeFieldName("timestamp", jw); + NumberConverter.serialize(epochMicros, jw); + jw.writeByte(JsonWriter.COMMA); + + if (!metricSet.getTags().isEmpty()) { + DslJsonSerializer.writeFieldName("tags", jw); + DslJsonSerializer.serializeTags(metricSet.getTags(), replaceBuilder, jw); + jw.writeByte(JsonWriter.COMMA); + } + + DslJsonSerializer.writeFieldName("samples", jw); + serializeSamples(metricSet.getSamples(), jw); + } + jw.writeByte(JsonWriter.OBJECT_END); + } + jw.writeByte(JsonWriter.OBJECT_END); + } + + private static void serializeSamples(Map samples, JsonWriter jw) { + jw.writeByte(JsonWriter.OBJECT_START); + final int size = samples.size(); + if (size > 0) { + final Iterator> iterator = samples.entrySet().iterator(); + Map.Entry kv = iterator.next(); + serializeSample(kv.getKey(), kv.getValue().get(), jw); + for (int i = 1; i < size; i++) { + jw.writeByte(JsonWriter.COMMA); + kv = iterator.next(); + serializeSample(kv.getKey(), kv.getValue().get(), jw); + } + } + jw.writeByte(JsonWriter.OBJECT_END); + } + + private static void serializeSample(String key, double value, JsonWriter jw) { + jw.writeString(key); + jw.writeByte(JsonWriter.SEMI); + jw.writeByte(JsonWriter.OBJECT_START); + { + DslJsonSerializer.writeFieldName("value", jw); + NumberConverter.serialize(value, jw); + } + jw.writeByte(JsonWriter.OBJECT_END); + } +} diff --git a/apm-agent-core/src/main/java/co/elastic/apm/agent/report/serialize/PayloadSerializer.java b/apm-agent-core/src/main/java/co/elastic/apm/agent/report/serialize/PayloadSerializer.java index 7a2b059354..b76e92d232 100644 --- a/apm-agent-core/src/main/java/co/elastic/apm/agent/report/serialize/PayloadSerializer.java +++ b/apm-agent-core/src/main/java/co/elastic/apm/agent/report/serialize/PayloadSerializer.java @@ -24,6 +24,7 @@ import co.elastic.apm.agent.impl.payload.Payload; import co.elastic.apm.agent.impl.transaction.Span; import co.elastic.apm.agent.impl.transaction.Transaction; +import co.elastic.apm.agent.metrics.MetricRegistry; import java.io.IOException; import java.io.OutputStream; @@ -59,4 +60,6 @@ public interface PayloadSerializer { * @return the number of bytes which are currently buffered */ int getBufferSize(); + + void serializeMetrics(MetricRegistry metricRegistry); } diff --git a/apm-agent-core/src/main/resources/META-INF/NOTICE b/apm-agent-core/src/main/resources/META-INF/NOTICE index f5aa88113d..7fde0822d1 100644 --- a/apm-agent-core/src/main/resources/META-INF/NOTICE +++ b/apm-agent-core/src/main/resources/META-INF/NOTICE @@ -4,3 +4,7 @@ under the Apache License 2.0. See: - co.elastic.apm.agent.configuration.source.SystemPropertyConfigurationSource - co.elastic.apm.agent.configuration.StartupInfo - co.elastic.apm.agent.bci.bytebuddy.MethodHierarchyMatcher + +This product includes software derived from micrometer, +under the Apache License 2.0. See: + - co.elastic.apm.agent.metrics.ProcessorMetrics diff --git a/apm-agent-core/src/main/resources/META-INF/services/co.elastic.apm.agent.context.LifecycleListener b/apm-agent-core/src/main/resources/META-INF/services/co.elastic.apm.agent.context.LifecycleListener index f4ceb91105..10f18aebe7 100644 --- a/apm-agent-core/src/main/resources/META-INF/services/co.elastic.apm.agent.context.LifecycleListener +++ b/apm-agent-core/src/main/resources/META-INF/services/co.elastic.apm.agent.context.LifecycleListener @@ -1,3 +1,6 @@ co.elastic.apm.agent.configuration.StartupInfo co.elastic.apm.agent.bci.OsgiBootDelegationEnabler co.elastic.apm.agent.bci.MatcherTimerLifecycleListener +co.elastic.apm.agent.metrics.builtin.JvmMemoryMetrics +co.elastic.apm.agent.metrics.builtin.SystemMetrics +co.elastic.apm.agent.metrics.builtin.JvmGcMetrics diff --git a/apm-agent-core/src/test/java/co/elastic/apm/agent/MockReporter.java b/apm-agent-core/src/test/java/co/elastic/apm/agent/MockReporter.java index 8a842de9c6..618efa99b3 100644 --- a/apm-agent-core/src/test/java/co/elastic/apm/agent/MockReporter.java +++ b/apm-agent-core/src/test/java/co/elastic/apm/agent/MockReporter.java @@ -26,6 +26,7 @@ import co.elastic.apm.agent.impl.stacktrace.StacktraceConfiguration; import co.elastic.apm.agent.impl.transaction.Span; import co.elastic.apm.agent.impl.transaction.Transaction; +import co.elastic.apm.agent.metrics.MetricRegistry; import co.elastic.apm.agent.report.Reporter; import co.elastic.apm.agent.report.serialize.DslJsonSerializer; import com.fasterxml.jackson.databind.JsonNode; @@ -143,6 +144,10 @@ public void report(ErrorCapture error) { errors.add(error); } + @Override + public void scheduleMetricReporting(MetricRegistry metricRegistry, long intervalMs) { + // noop + } public Span getFirstSpan() { return spans.get(0); diff --git a/apm-agent-core/src/test/java/co/elastic/apm/agent/configuration/validation/RangeValidatorTest.java b/apm-agent-core/src/test/java/co/elastic/apm/agent/configuration/validation/RangeValidatorTest.java new file mode 100644 index 0000000000..c6ca6d0eb7 --- /dev/null +++ b/apm-agent-core/src/test/java/co/elastic/apm/agent/configuration/validation/RangeValidatorTest.java @@ -0,0 +1,73 @@ +/*- + * #%L + * Elastic APM Java agent + * %% + * Copyright (C) 2018 Elastic and contributors + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package co.elastic.apm.agent.configuration.validation; + +import co.elastic.apm.agent.configuration.converter.TimeDuration; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class RangeValidatorTest { + + @Test + void testRange() { + final RangeValidator validator = RangeValidator.isInRange(1, 3); + assertThatThrownBy(() -> validator.assertValid(0)).isInstanceOf(IllegalArgumentException.class); + validator.assertValid(1); + validator.assertValid(2); + validator.assertValid(3); + assertThatThrownBy(() -> validator.assertValid(4)).isInstanceOf(IllegalArgumentException.class); + } + + @Test + void testNotInRange() { + final RangeValidator validator = RangeValidator.isNotInRange(1, 3); + validator.assertValid(0); + assertThatThrownBy(() -> validator.assertValid(1)).isInstanceOf(IllegalArgumentException.class); + assertThatThrownBy(() -> validator.assertValid(2)).isInstanceOf(IllegalArgumentException.class); + assertThatThrownBy(() -> validator.assertValid(3)).isInstanceOf(IllegalArgumentException.class); + validator.assertValid(4); + } + + @Test + void testMin() { + final RangeValidator validator = RangeValidator.min(1); + assertThatThrownBy(() -> validator.assertValid(0)).isInstanceOf(IllegalArgumentException.class); + validator.assertValid(1); + validator.assertValid(2); + } + + @Test + void testMax() { + final RangeValidator validator = RangeValidator.max(3); + validator.assertValid(1); + validator.assertValid(2); + validator.assertValid(3); + assertThatThrownBy(() -> validator.assertValid(4)).isInstanceOf(IllegalArgumentException.class); + } + + @Test + void testTimeDuration() { + final RangeValidator validator = RangeValidator.min(TimeDuration.of("1s")); + assertThatThrownBy(() -> validator.assertValid(TimeDuration.of("0s"))).isInstanceOf(IllegalArgumentException.class); + validator.assertValid(TimeDuration.of("1s")); + validator.assertValid(TimeDuration.of("2s")); + } +} diff --git a/apm-agent-core/src/test/java/co/elastic/apm/agent/metrics/builtin/JvmGcMetricsTest.java b/apm-agent-core/src/test/java/co/elastic/apm/agent/metrics/builtin/JvmGcMetricsTest.java new file mode 100644 index 0000000000..50c7bdb0ef --- /dev/null +++ b/apm-agent-core/src/test/java/co/elastic/apm/agent/metrics/builtin/JvmGcMetricsTest.java @@ -0,0 +1,52 @@ +/*- + * #%L + * Elastic APM Java agent + * %% + * Copyright (C) 2018 Elastic and contributors + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package co.elastic.apm.agent.metrics.builtin; + +import co.elastic.apm.agent.metrics.MetricRegistry; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +class JvmGcMetricsTest { + + private final JvmGcMetrics jvmGcMetrics = new JvmGcMetrics(); + private MetricRegistry registry = mock(MetricRegistry.class); + + @Test + void testGcMetrics() { + jvmGcMetrics.bindTo(registry); + verify(registry, atLeastOnce()).addUnlessNegative(eq("jvm.gc.count"), any(), any()); + verify(registry, atLeastOnce()).addUnlessNegative(eq("jvm.gc.time"), any(), any()); + verify(registry, atLeastOnce()).add(eq("jvm.gc.alloc"), any(), any()); + } + + @Test + void testGetAllocatedBytes() { + final double snapshot = JvmGcMetrics.HotspotAllocationSupplier.INSTANCE.get(); + assertThat(snapshot).isPositive(); + new Object(); + assertThat(JvmGcMetrics.HotspotAllocationSupplier.INSTANCE.get()).isGreaterThan(snapshot); + } +} diff --git a/apm-agent-core/src/test/java/co/elastic/apm/agent/metrics/builtin/JvmMemoryMetricsTest.java b/apm-agent-core/src/test/java/co/elastic/apm/agent/metrics/builtin/JvmMemoryMetricsTest.java new file mode 100644 index 0000000000..d309befd52 --- /dev/null +++ b/apm-agent-core/src/test/java/co/elastic/apm/agent/metrics/builtin/JvmMemoryMetricsTest.java @@ -0,0 +1,47 @@ +/*- + * #%L + * Elastic APM Java agent + * %% + * Copyright (C) 2018 Elastic and contributors + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package co.elastic.apm.agent.metrics.builtin; + +import co.elastic.apm.agent.metrics.MetricRegistry; +import org.junit.jupiter.api.Test; + +import java.util.Collections; + +import static org.assertj.core.api.Assertions.assertThat; + +class JvmMemoryMetricsTest { + + private final JvmMemoryMetrics jvmMemoryMetrics = new JvmMemoryMetrics(); + + @Test + void testMetrics() { + final MetricRegistry registry = new MetricRegistry(); + jvmMemoryMetrics.bindTo(registry); + System.out.println(registry.toString()); + assertThat(registry.get("jvm.memory.heap.used", Collections.emptyMap())).isNotZero(); + assertThat(registry.get("jvm.memory.heap.committed", Collections.emptyMap())).isNotZero(); + assertThat(registry.get("jvm.memory.heap.max", Collections.emptyMap())).isNotZero(); + assertThat(registry.get("jvm.memory.non_heap.used", Collections.emptyMap())).isNotZero(); + assertThat(registry.get("jvm.memory.non_heap.committed", Collections.emptyMap())).isNotZero(); + assertThat(registry.get("jvm.memory.non_heap.max", Collections.emptyMap())).isNotZero(); + final long[] longs = new long[1000000]; + System.out.println(registry.toString()); + } +} diff --git a/apm-agent-core/src/test/java/co/elastic/apm/agent/metrics/builtin/SystemMetricsTest.java b/apm-agent-core/src/test/java/co/elastic/apm/agent/metrics/builtin/SystemMetricsTest.java new file mode 100644 index 0000000000..68a28fd06b --- /dev/null +++ b/apm-agent-core/src/test/java/co/elastic/apm/agent/metrics/builtin/SystemMetricsTest.java @@ -0,0 +1,54 @@ +/*- + * #%L + * Elastic APM Java agent + * %% + * Copyright (C) 2018 Elastic and contributors + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package co.elastic.apm.agent.metrics.builtin; + +import co.elastic.apm.agent.metrics.MetricRegistry; +import org.junit.jupiter.api.Test; + +import java.util.Collections; + +import static org.assertj.core.api.Assertions.assertThat; + +class SystemMetricsTest { + + private MetricRegistry metricRegistry = new MetricRegistry(); + private SystemMetrics systemMetrics = new SystemMetrics(); + + @Test + void testSystemMetrics() { + systemMetrics.bindTo(metricRegistry); + // makes sure system.cpu.total.norm.pct does not return NaN + consumeCpu(); + assertThat(metricRegistry.get("system.cpu.total.norm.pct", Collections.emptyMap())).isBetween(0.0, 1.0); + assertThat(metricRegistry.get("system.process.cpu.total.norm.pct", Collections.emptyMap())).isBetween(0.0, 1.0); + assertThat(metricRegistry.get("system.memory.total", Collections.emptyMap())).isGreaterThan(0.0); + assertThat(metricRegistry.get("system.memory.actual.free", Collections.emptyMap())).isGreaterThan(0.0); + assertThat(metricRegistry.get("system.process.memory.size", Collections.emptyMap())).isGreaterThan(0.0); + } + + private void consumeCpu() { + int result = 1; + for (int i = 0; i < 10000; i++) { + result += Math.random() * i; + } + // forces a side-effect so that the JIT can't optimize away this code + System.out.println(result); + } +} diff --git a/apm-agent-core/src/test/java/co/elastic/apm/agent/report/serialize/MetricSetSerializationTest.java b/apm-agent-core/src/test/java/co/elastic/apm/agent/report/serialize/MetricSetSerializationTest.java new file mode 100644 index 0000000000..374cefab93 --- /dev/null +++ b/apm-agent-core/src/test/java/co/elastic/apm/agent/report/serialize/MetricSetSerializationTest.java @@ -0,0 +1,51 @@ +/*- + * #%L + * Elastic APM Java agent + * %% + * Copyright (C) 2018 Elastic and contributors + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package co.elastic.apm.agent.report.serialize; + +import co.elastic.apm.agent.metrics.MetricSet; +import co.elastic.apm.agent.report.serialize.MetricRegistrySerializer; +import com.dslplatform.json.DslJson; +import com.dslplatform.json.JsonWriter; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.util.Collections; + +import static org.assertj.core.api.Assertions.assertThat; + +class MetricSetSerializationTest { + + private JsonWriter jw = new DslJson<>().newWriter(); + private ObjectMapper objectMapper = new ObjectMapper(); + + @Test + void testSerialization() throws IOException { + final MetricSet metricSet = new MetricSet(Collections.singletonMap("foo.bar", "baz")); + metricSet.add("foo.bar", () -> 42); + metricSet.add("bar.baz", () -> 42); + MetricRegistrySerializer.serializeMetricSet(metricSet, System.currentTimeMillis() * 1000, new StringBuilder(), jw); + final String metricSetAsString = jw.toString(); + System.out.println(metricSetAsString); + final JsonNode jsonNode = objectMapper.readTree(metricSetAsString); + assertThat(jsonNode.get("metricset").get("samples").get("foo.bar").get("value").doubleValue()).isEqualTo(42); + } +} diff --git a/docs/configuration.asciidoc b/docs/configuration.asciidoc index 6d5005b8f4..c5dca204f2 100644 --- a/docs/configuration.asciidoc +++ b/docs/configuration.asciidoc @@ -768,6 +768,31 @@ Allowed byte units are `b`, `kb` and `mb`. `1kb` is equal to `1024b`. | `elastic.apm.api_request_size` | `api_request_size` | `ELASTIC_APM_API_REQUEST_SIZE` |============ +[float] +[[config-metrics-interval]] +==== `metrics_interval` + +The interval at which the agent sends metrics to the APM Server. +Must be at least `1s`. +Set to `0s` to deactivate. + +Supports the duration suffixes `ms`, `s` and `m`. +Example: `30s`. +The default unit for this option is `s` + +[options="header"] +|============ +| Default | Type | Dynamic +| `30s` | TimeDuration | false +|============ + + +[options="header"] +|============ +| Java System Properties | Property file | Environment +| `elastic.apm.metrics_interval` | `metrics_interval` | `ELASTIC_APM_METRICS_INTERVAL` +|============ + [[config-stacktrace]] === Stacktrace configuration options [float] @@ -1241,6 +1266,18 @@ The default unit for this option is `ms` # # api_request_size=768kb +# The interval at which the agent sends metrics to the APM Server. +# Must be at least `1s`. +# Set to `0s` to deactivate. +# +# This setting can not be changed at runtime. Changes require a restart of the application. +# Type: TimeDuration +# Supports the duration suffixes ms, s and m. Example: 30s. +# The default unit for this option is s. +# Default value: 30s +# +# metrics_interval=30s + ############################################ # Stacktrace # ############################################ diff --git a/docs/index.asciidoc b/docs/index.asciidoc index 105f8f6be9..b54e0f9f28 100644 --- a/docs/index.asciidoc +++ b/docs/index.asciidoc @@ -12,6 +12,7 @@ ifndef::env-github[] include::./intro.asciidoc[Introduction] include::./application-server-setup.asciidoc[Setup the agent with Application Servers] include::./supported-technologies.asciidoc[Supported Technologies] +include::./metrics.asciidoc[Metrics] include::./configuration.asciidoc[Configuration] include::./faq.asciidoc[Frequently Asked Questions] include::./public-api.asciidoc[API documentation] diff --git a/docs/metrics.asciidoc b/docs/metrics.asciidoc new file mode 100644 index 0000000000..73351b580b --- /dev/null +++ b/docs/metrics.asciidoc @@ -0,0 +1,198 @@ +ifdef::env-github[] +NOTE: For the best reading experience, +please view this documentation at https://www.elastic.co/guide/en/apm/agent/java[elastic.co] +endif::[] + +[[metrics]] +== Metrics + +The Java agent tracks certain system and application metrics. +Some of them are have built-in visualizations and some can only be visualized with custom Kibana dashboards. + +These metrics will be sent regularly to the APM Server and from there to Elasticsearch. +You can adjust the interval with the setting <>. + +The metrics will be stored in the `apm-*` index and have the `processor.event` property set to `metric`. + +[float] +[[metrics-system]] +=== System metrics + +As of version 6.6, these metrics will be visualized in the APM UI. + +For more system metrics, consider installing {metricbeat-ref}/index.html[metricbeat] on your hosts. + +*`system.cpu.total.norm.pct`*:: ++ +-- +type: scaled_float + +format: percent + +The percentage of CPU time in states other than Idle and IOWait, normalised by the number of cores. +-- + + +*`system.process.cpu.total.norm.pct`*:: ++ +-- +type: scaled_float + +format: percent + +The percentage of CPU time spent by the process since the last event. +This value is normalized by the number of CPU cores and it ranges from 0 to 100%. +-- + + +*`system.memory.total`*:: ++ +-- +type: long + +format: bytes + +Total memory. +-- + + +*`system.memory.actual.free`*:: ++ +-- +type: long + +format: bytes + +Actual free memory in bytes. It is calculated based on the OS. +On Linux it consists of the free memory plus caches and buffers. +On OSX it is a sum of free memory and the inactive memory. +On Windows, this value does not include memory consumed by system caches and buffers. +-- + + +*`system.process.memory.size`*:: ++ +-- +type: long + +format: bytes + +The total virtual memory the process has. +-- + +[float] +[[metrics-jvm]] +=== JVM Metrics + +NOTE: As of now, there are no built-in visualizations for these metrics, +so you will need to create custom Kibana dashboards for them. + +*`jvm.memory.heap.used`*:: ++ +-- +type: long + +format: bytes + +The amount of used heap memory in bytes +-- + + +*`jvm.memory.heap.committed`*:: ++ +-- +type: long + +format: bytes + +The amount of heap memory in bytes that is committed for the Java virtual machine to use. +This amount of memory is guaranteed for the Java virtual machine to use. +-- + + +*`jvm.memory.heap.max`*:: ++ +-- +type: long + +format: bytes + +The maximum amount of heap memory in bytes that can be used for memory management. +If the maximum memory size is undefined, the value is `-1`. +-- + + +*`jvm.memory.non_heap.used`*:: ++ +-- +type: long + +format: bytes + +The amount of used non-heap memory in bytes +-- + + +*`jvm.memory.non_heap.committed`*:: ++ +-- +type: long + +format: bytes + +The amount of non-heap memory in bytes that is committed for the Java virtual machine to use. +This amount of memory is guaranteed for the Java virtual machine to use. +-- + + +*`jvm.memory.non_heap.max`*:: ++ +-- +type: long + +format: bytes + +The maximum amount of non-heap memory in bytes that can be used for memory management. +If the maximum memory size is undefined, the value is `-1`. +-- + + +*`jvm.gc.count`*:: ++ +-- +type: long + +tags + +* name: The name representing this memory manager + +The total number of collections that have occurred. +-- + + +*`jvm.gc.time`*:: ++ +-- +type: long + +format: ms + +tags + +* name: The name representing this memory manager + +The approximate accumulated collection elapsed time in milliseconds. +-- + + +*`jvm.gc.alloc`*:: ++ +-- +type: long + +format: bytes + +An approximation of the total amount of memory, +in bytes, allocated in heap memory. +-- +