This is an automated email from the ASF dual-hosted git repository.
Duansg pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/hertzbeat.git
The following commit(s) were added to refs/heads/master by this push:
new cbb62a3358 [fix] bound what one anonymous push request can consume
(#4273)
cbb62a3358 is described below
commit cbb62a335806c416e9f8883a14efa68f91b30874
Author: Duansg <[email protected]>
AuthorDate: Fri Aug 14 10:41:02 2026 +0800
[fix] bound what one anonymous push request can consume (#4273)
---
.../collect/prometheus/parser/OnlineParser.java | 35 +-
.../prometheus/parser/OnlineParserTest.java | 25 ++
.../push/service/impl/PushGatewayServiceImpl.java | 315 ++++++++++++++++--
.../service/impl/PushGatewayServiceImplTest.java | 367 +++++++++++++++++++++
.../src/main/resources/application.yml | 5 +
script/application.yml | 5 +
.../hertzbeat-mysql-iotdb/conf/application.yml | 13 +-
.../hertzbeat-mysql-tdengine/conf/application.yml | 13 +-
.../conf/application.yml | 13 +-
.../conf/application.yml | 11 +-
.../conf/application.yml | 11 +-
11 files changed, 767 insertions(+), 46 deletions(-)
diff --git
a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/prometheus/parser/OnlineParser.java
b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/prometheus/parser/OnlineParser.java
index de48f59656..d5f9fae07f 100644
---
a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/prometheus/parser/OnlineParser.java
+++
b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/prometheus/parser/OnlineParser.java
@@ -60,16 +60,37 @@ public class OnlineParser {
}
public static Map<String, MetricFamily> parseMetrics(InputStream
inputStream) throws IOException {
- Map<String, MetricFamily> metricFamilyMap = new
ConcurrentHashMap<>(10);
+ return parseMetrics(inputStream, Integer.MAX_VALUE);
+ }
+
+ /**
+ * Parses at most {@code maxSamples} samples from the supplied stream.
+ *
+ * @param inputStream The Prometheus text stream
+ * @param maxSamples The maximum number of samples to materialize
+ * @return The parsed metric families, or {@code null} when the text
format is invalid
+ * @throws IOException When the stream cannot be read
+ * @throws SampleLimitExceededException When another sample follows the
configured limit
+ */
+ public static Map<String, MetricFamily> parseMetrics(InputStream
inputStream, int maxSamples) throws IOException {
+ if (maxSamples < 0) {
+ throw new IllegalArgumentException("maxSamples must not be
negative");
+ }
+ final Map<String, MetricFamily> metricFamilyMap = new
ConcurrentHashMap<>(10);
+ int sampleCount = 0;
try {
int i = getChar(inputStream);
while (i != -1) {
if (i == '#' || i == '\n') {
skipToLineEnd(inputStream).maybeEol().maybeEof().noElse();
} else {
- StringBuilder stringBuilder = new StringBuilder();
+ if (sampleCount >= maxSamples) {
+ throw new SampleLimitExceededException(maxSamples);
+ }
+ final StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append((char) i);
parseMetric(inputStream, metricFamilyMap, stringBuilder);
+ sampleCount++;
}
i = getChar(inputStream);
// To address the `\n\r` scenario, it is necessary to skip
@@ -84,6 +105,16 @@ public class OnlineParser {
return metricFamilyMap;
}
+ /**
+ * Signals that parsing stopped before materializing a sample beyond the
configured limit.
+ */
+ public static final class SampleLimitExceededException extends IOException
{
+
+ public SampleLimitExceededException(int limit) {
+ super("prometheus payload exceeds the " + limit + " sample limit");
+ }
+ }
+
/**
* Parses Prometheus metrics from the given {@link InputStream}, but only
for the specified metric name.
* <p>
diff --git
a/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/prometheus/parser/OnlineParserTest.java
b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/prometheus/parser/OnlineParserTest.java
index 4f515b8696..370ac9a3bd 100644
---
a/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/prometheus/parser/OnlineParserTest.java
+++
b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/prometheus/parser/OnlineParserTest.java
@@ -30,6 +30,8 @@ import java.util.stream.Collectors;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
class OnlineParserTest {
@@ -459,4 +461,27 @@ class OnlineParserTest {
assertEquals("run_as",
metricFamily.getMetricList().get(0).getLabels().get(3).getName());
assertEquals("NT AUTHORITY\nLocalService",
metricFamily.getMetricList().get(0).getLabels().get(3).getValue());
}
+
+ @Test
+ void testParseMetricsStopsBeforeSampleBeyondLimit() {
+ final String metrics = "metric_a 1\nmetric_b 2\nmetric_c 3\nmetric_d
4\n";
+ final ByteArrayInputStream inputStream =
+ new
ByteArrayInputStream(metrics.getBytes(StandardCharsets.UTF_8));
+
+ assertThrows(OnlineParser.SampleLimitExceededException.class,
+ () -> OnlineParser.parseMetrics(inputStream, 2));
+
+ assertTrue(inputStream.available() > 0, "samples after the limit
should remain unread");
+ }
+
+ @Test
+ void testParseMetricsAllowsExactlyTheSampleLimit() throws Exception {
+ final String metrics = "metric_a 1\nmetric_b 2\n";
+ final InputStream inputStream = new
ByteArrayInputStream(metrics.getBytes(StandardCharsets.UTF_8));
+
+ final Map<String, MetricFamily> metricFamilyMap =
OnlineParser.parseMetrics(inputStream, 2);
+
+ assertNotNull(metricFamilyMap);
+ assertEquals(2, metricFamilyMap.size());
+ }
}
diff --git
a/hertzbeat-push/src/main/java/org/apache/hertzbeat/push/service/impl/PushGatewayServiceImpl.java
b/hertzbeat-push/src/main/java/org/apache/hertzbeat/push/service/impl/PushGatewayServiceImpl.java
index 1be1a9203f..e0b706bff9 100644
---
a/hertzbeat-push/src/main/java/org/apache/hertzbeat/push/service/impl/PushGatewayServiceImpl.java
+++
b/hertzbeat-push/src/main/java/org/apache/hertzbeat/push/service/impl/PushGatewayServiceImpl.java
@@ -19,13 +19,19 @@
package org.apache.hertzbeat.push.service.impl;
+import java.io.IOException;
import java.io.InputStream;
import java.time.Instant;
+import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
import java.util.concurrent.ConcurrentHashMap;
-import java.util.stream.Collectors;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import jakarta.annotation.Nullable;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.collector.collect.prometheus.parser.MetricFamily;
@@ -37,6 +43,7 @@ import org.apache.hertzbeat.common.queue.CommonDataQueue;
import org.apache.hertzbeat.common.util.SnowFlakeIdGenerator;
import org.apache.hertzbeat.push.dao.PushMonitorDao;
import org.apache.hertzbeat.push.service.PushGatewayService;
+import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
/**
@@ -46,26 +53,81 @@ import org.springframework.stereotype.Service;
@Slf4j
@Service
public class PushGatewayServiceImpl implements PushGatewayService {
-
+
+ private static final byte PUSH_MONITOR_TYPE = (byte) 1;
+
private final CommonDataQueue commonDataQueue;
private final PushMonitorDao pushMonitorDao;
- private final Map<String, Long> jobInstanceMap;
-
- public PushGatewayServiceImpl(CommonDataQueue commonDataQueue,
PushMonitorDao pushMonitorDao) {
+ private final Map<JobInstance, Long> jobInstanceMap;
+
+ /**
+ * Cap on push monitors created automatically from unknown job/instance
pairs.
+ *
+ * <p>The route is unauthenticated by design, and every new pair used to
persist a
+ * monitor row and add a `jobInstanceMap` entry that is never removed, so
a caller
+ * iterating over made up names could grow the database and the heap
without bound.
+ * Above the cap an unknown pair is refused while the pairs already known
keep working,
+ * which is why eviction is not used here: evicting a live entry would
make the next
+ * push for that pair create a second monitor for the same job and
instance.
+ */
+ private final int maxAutoCreatedMonitors;
+
+ /**
+ * Cap on how many bytes a single push body may carry.
+ *
+ * <p>The parser materializes its result in memory, and the servlet
container does not bound
+ * a non form request body, so an independent byte limit is still needed
alongside the sample
+ * limit to keep long names and label values from exhausting the heap.
+ */
+ private final long maxBodyBytes;
+
+ /**
+ * Cap on how many samples a single push body may carry. The parser stops
before allocating
+ * a sample beyond this limit, so a compact body cannot create an
unbounded object graph.
+ */
+ private final int maxSamples;
+
+ /**
+ * One entry per pair whose monitor is being created, so that concurrent
pushes naming the
+ * same unknown pair wait for one creation instead of each starting their
own. Persistence
+ * stays outside any shared lock: a slow database would otherwise hold
every request for an
+ * unknown pair, and each of those requests is already holding its parsed
samples.
+ */
+ private final Map<JobInstance, CompletableFuture<Long>> monitorCreationMap;
+
+ /**
+ * Successful and in flight creations together, so that the cap is claimed
before the
+ * database write rather than counted after it.
+ */
+ private final AtomicInteger trackedMonitorCount;
+
+ public PushGatewayServiceImpl(CommonDataQueue commonDataQueue,
PushMonitorDao pushMonitorDao,
+
@Value("${hertzbeat.push.max-auto-created-monitors:10000}") int
maxAutoCreatedMonitors,
+
@Value("${hertzbeat.push.max-body-bytes:5242880}") long maxBodyBytes,
+
@Value("${hertzbeat.push.max-samples:10000}") int maxSamples) {
+ if (maxAutoCreatedMonitors < 0 || maxBodyBytes < 0 || maxSamples < 0) {
+ throw new IllegalArgumentException("push gateway limits must not
be negative");
+ }
this.commonDataQueue = commonDataQueue;
this.pushMonitorDao = pushMonitorDao;
+ this.maxAutoCreatedMonitors = maxAutoCreatedMonitors;
+ this.maxBodyBytes = maxBodyBytes;
+ this.maxSamples = maxSamples;
jobInstanceMap = new ConcurrentHashMap<>();
- pushMonitorDao.findMonitorsByType((byte) 1).forEach(monitor ->
- jobInstanceMap.put(monitor.getApp() + "_" + monitor.getName(),
monitor.getId()));
+ pushMonitorDao.findMonitorsByType(PUSH_MONITOR_TYPE).forEach(monitor ->
+ jobInstanceMap.put(new JobInstance(monitor.getApp(),
monitor.getName()), monitor.getId()));
+ monitorCreationMap = new ConcurrentHashMap<>();
+ trackedMonitorCount = new AtomicInteger(jobInstanceMap.size());
}
@Override
public boolean pushPrometheusMetrics(InputStream inputStream, String job,
String instance) {
try {
- long curTime = Instant.now().toEpochMilli();
- Map<String, MetricFamily> metricFamilyMap =
OnlineParser.parseMetrics(inputStream);
+ final long curTime = Instant.now().toEpochMilli();
+ final Map<String, MetricFamily> metricFamilyMap =
OnlineParser.parseMetrics(
+ new BoundedInputStream(inputStream, maxBodyBytes),
maxSamples);
if (metricFamilyMap == null) {
log.error("parse prometheus metrics is null, job: {},
instance: {}", job, instance);
return false;
@@ -74,20 +136,11 @@ public class PushGatewayServiceImpl implements
PushGatewayService {
if (job != null && instance != null) {
// auto create monitor when job and instance not null
// job is app, instance is the name
- id = jobInstanceMap.computeIfAbsent(job + "_" + instance, key
-> {
- log.info("auto create monitor by prometheus push, job: {},
instance: {}", job, instance);
- long monitorId = SnowFlakeIdGenerator.generateId();
- Monitor monitor = Monitor.builder()
- .id(monitorId)
- .app(job)
- .name(instance)
- .instance(instance)
- .type((byte) 1)
- .status(CommonConstants.MONITOR_UP_CODE)
- .build();
- this.pushMonitorDao.save(monitor);
- return monitorId;
- });
+ final Long monitorId = resolveMonitorId(new JobInstance(job,
instance));
+ if (monitorId == null) {
+ return false;
+ }
+ id = monitorId;
}
for (Map.Entry<String, MetricFamily> entry :
metricFamilyMap.entrySet()) {
CollectRep.MetricsData.Builder builder =
CollectRep.MetricsData.newBuilder();
@@ -110,9 +163,18 @@ public class PushGatewayServiceImpl implements
PushGatewayService {
builder.addField(CollectRep.Field.newBuilder().setName("value")
.setType(CommonConstants.TYPE_NUMBER).setLabel(false).build());
}
- Map<String, String> labelMap = metric.getLabels()
- .stream()
-
.collect(Collectors.toMap(MetricFamily.Label::getName,
MetricFamily.Label::getValue));
+ // A repeated label name is refused rather than
resolved: the exposition
+ // format requires the names of a label set to be
unique, and keeping one
+ // of the values would emit a schema carrying that
name twice. Built by
+ // hand so the refusal is a rejection this method can
answer with a
+ // warning, not the error trace a collector's
exception would produce.
+ Map<String, String> labelMap = new
HashMap<>(metric.getLabels().size());
+ for (MetricFamily.Label label : metric.getLabels()) {
+ if (labelMap.containsKey(label.getName())) {
+ throw new
DuplicateLabelException(label.getName());
+ }
+ labelMap.put(label.getName(), label.getValue());
+ }
CollectRep.ValueRow.Builder valueRowBuilder =
CollectRep.ValueRow.newBuilder();
for (String field : metricsFields) {
String fieldValue = labelMap.get(field);
@@ -125,9 +187,210 @@ public class PushGatewayServiceImpl implements
PushGatewayService {
}
}
return true;
+ } catch (BodyTooLargeException e) {
+ // A rejection, not a failure: caught apart from the generic
handler below so that a
+ // caller repeating oversized bodies costs one warning line each,
not a stack trace
+ log.warn("reject prometheus push over the {} byte body limit, job:
{}, instance: {}",
+ maxBodyBytes, job, instance);
+ return false;
+ } catch (OnlineParser.SampleLimitExceededException e) {
+ log.warn("reject prometheus push over the {} sample limit, job:
{}, instance: {}",
+ maxSamples, job, instance);
+ return false;
+ } catch (DuplicateLabelException e) {
+ log.warn("reject prometheus push repeating a label name, job: {},
instance: {}: {}",
+ job, instance, e.getMessage());
+ return false;
} catch (Exception e) {
log.error("push prometheus metrics error", e);
return false;
}
}
+
+ /**
+ * Returns the monitor id a job/instance pair resolves to, creating the
monitor on first
+ * sight, or null once {@link #maxAutoCreatedMonitors} is reached.
+ *
+ * <p>Concurrent pushes naming the same unknown pair share one creation
through a future, and
+ * the cap is claimed by an atomic count before the database write.
Nothing here holds a lock
+ * across that write: unrelated pairs persist in parallel, and a slow
database delays only the
+ * requests naming the pair being created, which matters because every
waiting request is
+ * holding the samples it already parsed.
+ *
+ * @param pair Job and instance the push named
+ * @return The monitor id, or null when the cap leaves no room for a new
one
+ */
+ @Nullable
+ private Long resolveMonitorId(JobInstance pair) {
+ final Long known = jobInstanceMap.get(pair);
+ if (known != null) {
+ return known;
+ }
+
+ final CompletableFuture<Long> proposedCreation = new
CompletableFuture<>();
+ final CompletableFuture<Long> ongoingCreation =
monitorCreationMap.putIfAbsent(pair, proposedCreation);
+ if (ongoingCreation != null) {
+ try {
+ return ongoingCreation.join();
+ } catch (CompletionException e) {
+ // The request owning the creation reports the failure once;
joining its exception
+ // here would multiply a single database error by every
request that waited
+ return null;
+ }
+ }
+
+ try {
+ // Looked up again now that the creation is claimed: another
request may have finished
+ // this pair between the lookup above and this claim, and going on
to create it would
+ // leave two monitors for one pair and spend a second slot of the
cap
+ final Long createdMeanwhile = jobInstanceMap.get(pair);
+ if (createdMeanwhile != null) {
+ proposedCreation.complete(createdMeanwhile);
+ return createdMeanwhile;
+ }
+ if (!reserveMonitorSlot()) {
+ proposedCreation.complete(null);
+ log.warn("reject prometheus push for unknown job: {},
instance: {}, "
+ + "already tracking {} push monitors, limit is
{}",
+ pair.job(), pair.instance(),
trackedMonitorCount.get(), maxAutoCreatedMonitors);
+ return null;
+ }
+ boolean created = false;
+ try {
+ final long monitorId = createMonitor(pair);
+ jobInstanceMap.put(pair, monitorId);
+ created = true;
+ proposedCreation.complete(monitorId);
+ return monitorId;
+ } finally {
+ if (!created) {
+ trackedMonitorCount.decrementAndGet();
+ }
+ }
+ } catch (RuntimeException | Error e) {
+ proposedCreation.completeExceptionally(e);
+ throw e;
+ } finally {
+ monitorCreationMap.remove(pair, proposedCreation);
+ }
+ }
+
+ /**
+ * Claims one slot of the cap, or reports that none is left. Claiming
before the database
+ * write is what keeps concurrent creations from exceeding the cap
together.
+ */
+ private boolean reserveMonitorSlot() {
+ int tracked = trackedMonitorCount.get();
+ while (tracked < maxAutoCreatedMonitors) {
+ if (trackedMonitorCount.compareAndSet(tracked, tracked + 1)) {
+ return true;
+ }
+ tracked = trackedMonitorCount.get();
+ }
+ return false;
+ }
+
+ /**
+ * Persists a push monitor after its slot of the cap has been claimed.
+ */
+ private long createMonitor(JobInstance pair) {
+ final String job = pair.job();
+ final String instance = pair.instance();
+ log.info("auto create monitor by prometheus push, job: {}, instance:
{}", job, instance);
+ final long monitorId = SnowFlakeIdGenerator.generateId();
+ final Monitor monitor = Monitor.builder()
+ .id(monitorId)
+ .app(job)
+ .name(instance)
+ .instance(instance)
+ .type(PUSH_MONITOR_TYPE)
+ .status(CommonConstants.MONITOR_UP_CODE)
+ .build();
+ this.pushMonitorDao.save(monitor);
+ return monitorId;
+ }
+
+ /**
+ * Identifies the monitor a push belongs to.
+ *
+ * <p>The two names are kept apart instead of being joined into one
string: a separator
+ * carries no meaning in either name, so `job + "_" + instance` maps ("a",
"b_c") and
+ * ("a_b", "c") onto the same key. Colliding pairs would push their
samples into whichever
+ * monitor was created first, and at startup they would collapse into a
single map entry,
+ * making the cap count fewer monitors than the database actually holds.
+ */
+ private record JobInstance(String job, String instance) {
+ }
+
+ /**
+ * Raised when a sample repeats a label name, which the exposition format
does not allow.
+ * Kept apart from the generic handler so a malformed body costs one
warning line rather
+ * than an error trace on a route that takes its input from anyone.
+ */
+ static final class DuplicateLabelException extends IOException {
+
+ DuplicateLabelException(String name) {
+ super("sample repeats the label name " + name);
+ }
+ }
+
+ /**
+ * Raised when a body goes past {@link #maxBodyBytes}. It is kept apart
from the other read
+ * failures so the caller can answer a body that is merely too large
without an error trace.
+ */
+ static final class BodyTooLargeException extends IOException {
+
+ BodyTooLargeException(long limit) {
+ super("push body exceeds the " + limit + " byte limit");
+ }
+ }
+
+ /**
+ * Fails the read once the body has delivered more than {@code limit}
bytes, instead of
+ * letting the parser accumulate an unbounded body in memory. Reading
stops at the
+ * failure, so the bytes beyond the limit are never buffered.
+ */
+ static final class BoundedInputStream extends InputStream {
+
+ private final InputStream delegate;
+
+ private final long limit;
+
+ private long bytesRead;
+
+ BoundedInputStream(InputStream delegate, long limit) {
+ this.delegate = delegate;
+ this.limit = limit;
+ }
+
+ @Override
+ public int read() throws IOException {
+ final int value = delegate.read();
+ if (value != -1) {
+ recordBytesRead(1);
+ }
+ return value;
+ }
+
+ @Override
+ public int read(byte[] buffer, int offset, int length) throws
IOException {
+ final int bytesReadNow = delegate.read(buffer, offset, length);
+ if (bytesReadNow > 0) {
+ recordBytesRead(bytesReadNow);
+ }
+ return bytesReadNow;
+ }
+
+ private void recordBytesRead(int increment) throws IOException {
+ bytesRead += increment;
+ if (bytesRead > limit) {
+ throw new BodyTooLargeException(limit);
+ }
+ }
+
+ @Override
+ public void close() throws IOException {
+ delegate.close();
+ }
+ }
}
diff --git
a/hertzbeat-push/src/test/java/org/apache/hertzbeat/push/service/impl/PushGatewayServiceImplTest.java
b/hertzbeat-push/src/test/java/org/apache/hertzbeat/push/service/impl/PushGatewayServiceImplTest.java
new file mode 100644
index 0000000000..d1991824e4
--- /dev/null
+++
b/hertzbeat-push/src/test/java/org/apache/hertzbeat/push/service/impl/PushGatewayServiceImplTest.java
@@ -0,0 +1,367 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.push.service.impl;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.lenient;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CyclicBarrier;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.apache.hertzbeat.common.entity.manager.Monitor;
+import org.apache.hertzbeat.common.entity.message.CollectRep;
+import org.apache.hertzbeat.common.queue.CommonDataQueue;
+import org.apache.hertzbeat.push.dao.PushMonitorDao;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+/**
+ * Test case for {@link PushGatewayServiceImpl}.
+ *
+ * <p>`/api/push/prometheus/**` is unauthenticated by design, so the resource
a single
+ * anonymous request may consume has to be bounded: the body it may carry, the
samples it
+ * may enqueue, and the number of push monitors it may bring into existence.
+ */
+@ExtendWith(MockitoExtension.class)
+class PushGatewayServiceImplTest {
+
+ private static final String BODY = "sample_metric{label=\"a\"} 1\n";
+
+ @Mock
+ private CommonDataQueue commonDataQueue;
+
+ @Mock
+ private PushMonitorDao pushMonitorDao;
+
+ @BeforeEach
+ void setUp() {
+ // The stream test below builds no service, so this default must not
be strict
+ lenient().when(pushMonitorDao.findMonitorsByType((byte)
1)).thenReturn(List.of());
+ }
+
+ private PushGatewayServiceImpl createService(int maxMonitors, long
maxBodyBytes, int maxSamples) {
+ return new PushGatewayServiceImpl(commonDataQueue, pushMonitorDao,
maxMonitors, maxBodyBytes, maxSamples);
+ }
+
+ private static ByteArrayInputStream createBody(String content) {
+ return new
ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8));
+ }
+
+ /**
+ * The exposition format requires the names of a label set to be unique,
so a sample that
+ * repeats one is refused rather than resolved. It stays a rejection
though: answering it
+ * with an error trace would let a malformed body fill the log on an
anonymous route.
+ */
+ @Test
+ void testSampleRepeatingTheLabelNameIsRejected() {
+ final PushGatewayServiceImpl service = createService(10, 1024, 100);
+
+ assertFalse(service.pushPrometheusMetrics(
+ createBody("sample_metric{label=\"a\",label=\"b\"} 1\n"),
"job1", "instance1"));
+
+ verify(commonDataQueue, never()).sendMetricsData(any());
+ }
+
+ @Test
+ void testPushIsAcceptedWithinTheLimits() {
+ final PushGatewayServiceImpl service = createService(10, 1024, 100);
+
+ assertTrue(service.pushPrometheusMetrics(createBody(BODY), "job1",
"instance1"));
+
+ verify(pushMonitorDao).save(any(Monitor.class));
+ }
+
+ @Test
+ void testBodyBeyondTheByteLimitIsRejected() {
+ final PushGatewayServiceImpl service = createService(10, 16, 100);
+
+
assertFalse(service.pushPrometheusMetrics(createBody(BODY.repeat(100)), "job1",
"instance1"));
+
+ verify(pushMonitorDao, never()).save(any(Monitor.class));
+ }
+
+ @Test
+ void testBodyBeyondTheSampleLimitIsRejected() {
+ final PushGatewayServiceImpl service = createService(10, 1024 * 1024,
2);
+ final StringBuilder many = new StringBuilder();
+ for (int index = 0; index < 10; index++) {
+
many.append("sample_metric{label=\"value").append(index).append("\"} 1\n");
+ }
+ final ByteArrayInputStream inputStream = createBody(many.toString());
+
+ assertFalse(service.pushPrometheusMetrics(inputStream, "job1",
"instance1"));
+ assertTrue(inputStream.available() > 0, "the parser should stop before
consuming the remaining samples");
+
+ verify(pushMonitorDao, never()).save(any(Monitor.class));
+ }
+
+ @Test
+ void testBodyAtTheSampleLimitIsAccepted() {
+ final PushGatewayServiceImpl service = createService(10, 1024, 2);
+ final String twoSamples = "sample_metric{label=\"a\"} 1\n"
+ + "sample_metric{label=\"b\"} 2\n";
+
+ assertTrue(service.pushPrometheusMetrics(createBody(twoSamples),
"job1", "instance1"));
+ }
+
+ /**
+ * An unknown job/instance pair persists a monitor row and adds a map
entry that is
+ * never removed, so without a cap an anonymous caller iterating over made
up names
+ * grows the database and the heap without bound.
+ */
+ @Test
+ void testAutoCreationStopsAtTheMonitorLimit() {
+ final PushGatewayServiceImpl service = createService(2, 1024, 100);
+
+ assertTrue(service.pushPrometheusMetrics(createBody(BODY), "job1",
"instance1"));
+ assertTrue(service.pushPrometheusMetrics(createBody(BODY), "job2",
"instance2"));
+ assertFalse(service.pushPrometheusMetrics(createBody(BODY), "job3",
"instance3"));
+
+ verify(pushMonitorDao, times(2)).save(any(Monitor.class));
+ }
+
+ /**
+ * The cap must not turn into eviction: a pair already known has to keep
resolving to
+ * the monitor it created, otherwise a later push would create a second
monitor for the
+ * same job and instance.
+ */
+ @Test
+ void testKnownPairsKeepWorkingAtTheLimit() {
+ final PushGatewayServiceImpl service = createService(1, 1024, 100);
+
+ assertTrue(service.pushPrometheusMetrics(createBody(BODY), "job1",
"instance1"));
+ assertFalse(service.pushPrometheusMetrics(createBody(BODY), "other",
"instance"));
+ assertTrue(service.pushPrometheusMetrics(createBody(BODY), "job1",
"instance1"));
+
+ verify(pushMonitorDao, times(1)).save(any(Monitor.class));
+ }
+
+ /**
+ * The route is anonymous, so nothing stops a caller from sending its
unknown pairs all at
+ * once. Testing the cap and claiming the entry in two steps lets every
request that already
+ * passed the test create a monitor of its own, which is the cap being
exceeded by as many
+ * requests as the container serves in parallel.
+ */
+ @Test
+ void testConcurrentPushesForUnknownPairsStopAtTheMonitorLimit() throws
Exception {
+ final int callers = 16;
+ final PushGatewayServiceImpl service = createService(1, 1024, 100);
+ final CyclicBarrier startTogether = new CyclicBarrier(callers);
+ final ExecutorService pool = Executors.newFixedThreadPool(callers);
+ final AtomicInteger accepted = new AtomicInteger();
+ try {
+ final List<Future<?>> pushes = new ArrayList<>();
+ for (int index = 0; index < callers; index++) {
+ final String instance = "instance" + index;
+ pushes.add(pool.submit(() -> {
+ startTogether.await();
+ if (service.pushPrometheusMetrics(createBody(BODY), "job",
instance)) {
+ accepted.incrementAndGet();
+ }
+ return null;
+ }));
+ }
+ for (final Future<?> push : pushes) {
+ push.get(30, TimeUnit.SECONDS);
+ }
+ } finally {
+ pool.shutdownNow();
+ }
+
+ assertEquals(1, accepted.get());
+ verify(pushMonitorDao, times(1)).save(any(Monitor.class));
+ }
+
+ @Test
+ void testFailedSaveDoesNotConsumeTheMonitorLimit() {
+ when(pushMonitorDao.save(any(Monitor.class)))
+ .thenThrow(new IllegalStateException("database unavailable"))
+ .thenAnswer(invocation -> invocation.getArgument(0));
+ final PushGatewayServiceImpl service = createService(1, 1024, 100);
+
+ assertFalse(service.pushPrometheusMetrics(createBody(BODY), "job1",
"instance1"));
+ assertTrue(service.pushPrometheusMetrics(createBody(BODY), "job2",
"instance2"));
+
+ verify(pushMonitorDao, times(2)).save(any(Monitor.class));
+ }
+
+ @Test
+ void testNegativeLimitsAreRejectedAtConstruction() {
+ assertThrows(IllegalArgumentException.class, () -> createService(-1,
1024, 100));
+ assertThrows(IllegalArgumentException.class, () -> createService(1,
-1, 100));
+ assertThrows(IllegalArgumentException.class, () -> createService(1,
1024, -1));
+ }
+
+ @Test
+ void testConcurrentPushesForTheSamePairCreateOneMonitor() throws Exception
{
+ final int callers = 8;
+ final CyclicBarrier startTogether = new CyclicBarrier(callers);
+ final PushGatewayServiceImpl service = createService(1, 1024, 100);
+ final ExecutorService pool = Executors.newFixedThreadPool(callers);
+ try {
+ final List<Future<Boolean>> pushes = new ArrayList<>();
+ for (int index = 0; index < callers; index++) {
+ pushes.add(pool.submit(() -> {
+ startTogether.await();
+ return service.pushPrometheusMetrics(createBody(BODY),
"job", "instance");
+ }));
+ }
+ for (final Future<Boolean> push : pushes) {
+ assertTrue(push.get(30, TimeUnit.SECONDS));
+ }
+ } finally {
+ pool.shutdownNow();
+ }
+
+ verify(pushMonitorDao).save(any(Monitor.class));
+ }
+
+ /**
+ * A request may read no entry for a pair and only then claim the
creation, by which time
+ * another request may have created that pair and cleared its claim.
Without a second lookup
+ * once the claim is won, this request goes on to create the same pair
again, leaving two
+ * monitors for one pair and a slot of the cap spent for good.
+ *
+ * <p>The interleaving is forced rather than raced: the map hands back a
miss, and while it
+ * does, a competing push runs its whole creation.
+ */
+ @Test
+ @SuppressWarnings({"rawtypes", "unchecked"})
+ void testStaleMissDoesNotCreateTwoMonitorsForOnePair() throws Exception {
+ final PushGatewayServiceImpl service = createService(2, 1024, 100);
+ final Field trackedPairs =
PushGatewayServiceImpl.class.getDeclaredField("jobInstanceMap");
+ trackedPairs.setAccessible(true);
+ final AtomicBoolean competed = new AtomicBoolean();
+ final Map probing = new ConcurrentHashMap() {
+ @Override
+ public Object get(Object key) {
+ final Object value = super.get(key);
+ if (value == null && competed.compareAndSet(false, true)) {
+ service.pushPrometheusMetrics(createBody(BODY), "job1",
"instance1");
+ }
+ return value;
+ }
+ };
+ trackedPairs.set(service, probing);
+
+ assertTrue(service.pushPrometheusMetrics(createBody(BODY), "job1",
"instance1"));
+
+ verify(pushMonitorDao, times(1)).save(any(Monitor.class));
+ assertEquals(1, probing.size());
+ // The slot the duplicate would have taken is still there for another
pair
+ assertTrue(service.pushPrometheusMetrics(createBody(BODY), "job2",
"instance2"));
+ }
+
+ @Test
+ void testMonitorsLoadedAtStartupCountTowardsTheLimit() {
+ lenient().when(pushMonitorDao.findMonitorsByType((byte)
1)).thenReturn(List.of(
+
Monitor.builder().id(1L).app("job1").name("instance1").build()));
+ final PushGatewayServiceImpl service = createService(1, 1024, 100);
+
+ assertFalse(service.pushPrometheusMetrics(createBody(BODY), "job2",
"instance2"));
+ assertTrue(service.pushPrometheusMetrics(createBody(BODY), "job1",
"instance1"));
+
+ verify(pushMonitorDao, never()).save(any(Monitor.class));
+ }
+
+ /**
+ * A separator carries no meaning inside a job or an instance name, so the
two names must not
+ * be joined into a single key: ("job", "a_b") and ("job_a", "b") are
different monitors, and
+ * the second pair must not push its samples into the monitor the first
one created.
+ */
+ @Test
+ void testPairsSharingTheSeparatorAreDistinctMonitors() {
+ final PushGatewayServiceImpl service = createService(10, 1024, 100);
+
+ assertTrue(service.pushPrometheusMetrics(createBody(BODY), "job",
"a_b"));
+ assertTrue(service.pushPrometheusMetrics(createBody(BODY), "job_a",
"b"));
+
+ verify(pushMonitorDao, times(2)).save(any(Monitor.class));
+ final ArgumentCaptor<CollectRep.MetricsData> pushed =
+ ArgumentCaptor.forClass(CollectRep.MetricsData.class);
+ verify(commonDataQueue, times(2)).sendMetricsData(pushed.capture());
+ assertNotEquals(pushed.getAllValues().get(0).getId(),
pushed.getAllValues().get(1).getId());
+ }
+
+ /**
+ * Colliding pairs must not collapse into one entry while the monitors are
loaded either,
+ * which would let the cap count fewer monitors than the database actually
holds.
+ */
+ @Test
+ void testPairsSharingTheSeparatorCountSeparatelyAtStartup() {
+ lenient().when(pushMonitorDao.findMonitorsByType((byte)
1)).thenReturn(List.of(
+ Monitor.builder().id(1L).app("job").name("a_b").build(),
+ Monitor.builder().id(2L).app("job_a").name("b").build()));
+ final PushGatewayServiceImpl service = createService(2, 1024, 100);
+
+ assertFalse(service.pushPrometheusMetrics(createBody(BODY), "job3",
"instance3"));
+
+ verify(pushMonitorDao, never()).save(any(Monitor.class));
+ }
+
+ @Test
+ void testBoundedStreamStopsAtTheLimit() throws Exception {
+ final PushGatewayServiceImpl.BoundedInputStream stream =
+ new
PushGatewayServiceImpl.BoundedInputStream(createBody("abcdef"), 3);
+
+ assertEquals('a', stream.read());
+ assertEquals('b', stream.read());
+ assertEquals('c', stream.read());
+ assertThrows(PushGatewayServiceImpl.BodyTooLargeException.class,
stream::read);
+ }
+
+ /**
+ * A body over the limit is a rejection rather than a failure, so it must
be distinguishable
+ * from a read that genuinely broke: the route is anonymous, and answering
every oversized
+ * body with an error trace lets a caller fill the log at will.
+ */
+ @Test
+ void testBodyOverTheByteLimitIsRejectedNotFailed() {
+ final PushGatewayServiceImpl.BoundedInputStream stream =
+ new
PushGatewayServiceImpl.BoundedInputStream(createBody(BODY.repeat(100)), 16);
+
+ final IOException raised = assertThrows(IOException.class,
stream::readAllBytes);
+
+ assertInstanceOf(PushGatewayServiceImpl.BodyTooLargeException.class,
raised);
+ }
+}
diff --git a/hertzbeat-startup/src/main/resources/application.yml
b/hertzbeat-startup/src/main/resources/application.yml
index 90c0ea4bb8..fa6cda6e10 100644
--- a/hertzbeat-startup/src/main/resources/application.yml
+++ b/hertzbeat-startup/src/main/resources/application.yml
@@ -373,3 +373,8 @@ hertzbeat:
concurrency-limit: 256
reject-when-limit-reached: true
task-termination-timeout: 5000
+ # Bounds on what a single request to the push gateway may consume.
+ push:
+ max-auto-created-monitors:
${HERTZBEAT_PUSH_MAX_AUTO_CREATED_MONITORS:10000}
+ max-body-bytes: ${HERTZBEAT_PUSH_MAX_BODY_BYTES:5242880}
+ max-samples: ${HERTZBEAT_PUSH_MAX_SAMPLES:10000}
diff --git a/script/application.yml b/script/application.yml
index 90c0ea4bb8..fa6cda6e10 100644
--- a/script/application.yml
+++ b/script/application.yml
@@ -373,3 +373,8 @@ hertzbeat:
concurrency-limit: 256
reject-when-limit-reached: true
task-termination-timeout: 5000
+ # Bounds on what a single request to the push gateway may consume.
+ push:
+ max-auto-created-monitors:
${HERTZBEAT_PUSH_MAX_AUTO_CREATED_MONITORS:10000}
+ max-body-bytes: ${HERTZBEAT_PUSH_MAX_BODY_BYTES:5242880}
+ max-samples: ${HERTZBEAT_PUSH_MAX_SAMPLES:10000}
diff --git a/script/docker-compose/hertzbeat-mysql-iotdb/conf/application.yml
b/script/docker-compose/hertzbeat-mysql-iotdb/conf/application.yml
index 33b0537468..7695271d03 100644
--- a/script/docker-compose/hertzbeat-mysql-iotdb/conf/application.yml
+++ b/script/docker-compose/hertzbeat-mysql-iotdb/conf/application.yml
@@ -109,7 +109,7 @@ spring:
hibernate:
format_sql: true
dialect: org.hibernate.dialect.MySQLDialect
-
+
flyway:
enabled: true
clean-disabled: true
@@ -117,7 +117,7 @@ spring:
baseline-version: 1
locations:
- classpath:db/migration/mysql
-
+
# Not Require, Please config if you need email notify
mail:
# Attention: this is mail server address.
@@ -138,7 +138,7 @@ common:
queue:
# memory or kafka
type: memory
-
+
warehouse:
store:
# store history metrics data, enable only one below
@@ -222,7 +222,7 @@ alerter:
region: AWS_REGION_FOR_END_USER_MESSAGING
twilio:
account-sid: YOUR_ACCOUNT_SID
- auth-token: YOUR_AUTH_TOKEN
+ auth-token: YOUR_AUTH_TOKEN
twilio-phone-number: YOUR_TWILIO_PHONE_NUMBER
scheduler:
server:
@@ -273,3 +273,8 @@ hertzbeat:
concurrency-limit: 256
reject-when-limit-reached: true
task-termination-timeout: 5000
+ # Bounds on what a single request to the push gateway may consume
+ push:
+ max-auto-created-monitors:
${HERTZBEAT_PUSH_MAX_AUTO_CREATED_MONITORS:10000}
+ max-body-bytes: ${HERTZBEAT_PUSH_MAX_BODY_BYTES:5242880}
+ max-samples: ${HERTZBEAT_PUSH_MAX_SAMPLES:10000}
diff --git
a/script/docker-compose/hertzbeat-mysql-tdengine/conf/application.yml
b/script/docker-compose/hertzbeat-mysql-tdengine/conf/application.yml
index 8d795ed643..90220d0669 100644
--- a/script/docker-compose/hertzbeat-mysql-tdengine/conf/application.yml
+++ b/script/docker-compose/hertzbeat-mysql-tdengine/conf/application.yml
@@ -109,7 +109,7 @@ spring:
hibernate:
format_sql: true
dialect: org.hibernate.dialect.MySQLDialect
-
+
flyway:
enabled: true
clean-disabled: true
@@ -117,7 +117,7 @@ spring:
baseline-version: 1
locations:
- classpath:db/migration/mysql
-
+
# Not Require, Please config if you need email notify
mail:
# Attention: this is mail server address.
@@ -138,7 +138,7 @@ common:
queue:
# memory or kafka
type: memory
-
+
warehouse:
store:
# store history metrics data, enable only one below
@@ -219,7 +219,7 @@ alerter:
region: AWS_REGION_FOR_END_USER_MESSAGING
twilio:
account-sid: YOUR_ACCOUNT_SID
- auth-token: YOUR_AUTH_TOKEN
+ auth-token: YOUR_AUTH_TOKEN
twilio-phone-number: YOUR_TWILIO_PHONE_NUMBER
scheduler:
server:
@@ -270,3 +270,8 @@ hertzbeat:
concurrency-limit: 256
reject-when-limit-reached: true
task-termination-timeout: 5000
+ # Bounds on what a single request to the push gateway may consume.
+ push:
+ max-auto-created-monitors:
${HERTZBEAT_PUSH_MAX_AUTO_CREATED_MONITORS:10000}
+ max-body-bytes: ${HERTZBEAT_PUSH_MAX_BODY_BYTES:5242880}
+ max-samples: ${HERTZBEAT_PUSH_MAX_SAMPLES:10000}
diff --git
a/script/docker-compose/hertzbeat-mysql-victoria-metrics/conf/application.yml
b/script/docker-compose/hertzbeat-mysql-victoria-metrics/conf/application.yml
index f92abb9f98..0074a9dd3f 100644
---
a/script/docker-compose/hertzbeat-mysql-victoria-metrics/conf/application.yml
+++
b/script/docker-compose/hertzbeat-mysql-victoria-metrics/conf/application.yml
@@ -109,7 +109,7 @@ spring:
hibernate:
format_sql: true
dialect: org.hibernate.dialect.MySQLDialect
-
+
flyway:
enabled: true
clean-disabled: true
@@ -117,7 +117,7 @@ spring:
baseline-version: 1
locations:
- classpath:db/migration/mysql
-
+
# Not Require, Please config if you need email notify
mail:
# Attention: this is mail server address.
@@ -142,7 +142,7 @@ warehouse:
expire-time: 1h
victoria-metrics:
enabled: true
- url: http://victoria-metrics:8428
+ url: http://victoria-metrics:8428
username: root
password: root
insert:
@@ -222,7 +222,7 @@ alerter:
region: AWS_REGION_FOR_END_USER_MESSAGING
twilio:
account-sid: YOUR_ACCOUNT_SID
- auth-token: YOUR_AUTH_TOKEN
+ auth-token: YOUR_AUTH_TOKEN
twilio-phone-number: YOUR_TWILIO_PHONE_NUMBER
scheduler:
server:
@@ -273,3 +273,8 @@ hertzbeat:
concurrency-limit: 256
reject-when-limit-reached: true
task-termination-timeout: 5000
+ # Bounds on what a single request to the push gateway may consume.
+ push:
+ max-auto-created-monitors:
${HERTZBEAT_PUSH_MAX_AUTO_CREATED_MONITORS:10000}
+ max-body-bytes: ${HERTZBEAT_PUSH_MAX_BODY_BYTES:5242880}
+ max-samples: ${HERTZBEAT_PUSH_MAX_SAMPLES:10000}
diff --git
a/script/docker-compose/hertzbeat-postgresql-greptimedb/conf/application.yml
b/script/docker-compose/hertzbeat-postgresql-greptimedb/conf/application.yml
index 6cc1490030..c25b533a48 100644
--- a/script/docker-compose/hertzbeat-postgresql-greptimedb/conf/application.yml
+++ b/script/docker-compose/hertzbeat-postgresql-greptimedb/conf/application.yml
@@ -108,7 +108,7 @@ spring:
hibernate:
format_sql: true
dialect: org.hibernate.dialect.PostgreSQLDialect
-
+
flyway:
enabled: true
clean-disabled: true
@@ -116,7 +116,7 @@ spring:
baseline-version: 1
locations:
- classpath:db/migration/postgresql
-
+
# Not Require, Please config if you need email notify
mail:
# Attention: this is mail server address.
@@ -219,7 +219,7 @@ alerter:
region: AWS_REGION_FOR_END_USER_MESSAGING
twilio:
account-sid: YOUR_ACCOUNT_SID
- auth-token: YOUR_AUTH_TOKEN
+ auth-token: YOUR_AUTH_TOKEN
twilio-phone-number: YOUR_TWILIO_PHONE_NUMBER
scheduler:
server:
@@ -270,3 +270,8 @@ hertzbeat:
concurrency-limit: 256
reject-when-limit-reached: true
task-termination-timeout: 5000
+ # Bounds on what a single request to the push gateway may consume.
+ push:
+ max-auto-created-monitors:
${HERTZBEAT_PUSH_MAX_AUTO_CREATED_MONITORS:10000}
+ max-body-bytes: ${HERTZBEAT_PUSH_MAX_BODY_BYTES:5242880}
+ max-samples: ${HERTZBEAT_PUSH_MAX_SAMPLES:10000}
diff --git
a/script/docker-compose/hertzbeat-postgresql-victoria-metrics/conf/application.yml
b/script/docker-compose/hertzbeat-postgresql-victoria-metrics/conf/application.yml
index 3108f5b994..71d7fb2f58 100644
---
a/script/docker-compose/hertzbeat-postgresql-victoria-metrics/conf/application.yml
+++
b/script/docker-compose/hertzbeat-postgresql-victoria-metrics/conf/application.yml
@@ -108,7 +108,7 @@ spring:
hibernate:
format_sql: true
dialect: org.hibernate.dialect.PostgreSQLDialect
-
+
flyway:
enabled: true
clean-disabled: true
@@ -116,7 +116,7 @@ spring:
baseline-version: 1
locations:
- classpath:db/migration/postgresql
-
+
# Not Require, Please config if you need email notify
mail:
# Attention: this is mail server address.
@@ -221,7 +221,7 @@ alerter:
region: AWS_REGION_FOR_END_USER_MESSAGING
twilio:
account-sid: YOUR_ACCOUNT_SID
- auth-token: YOUR_AUTH_TOKEN
+ auth-token: YOUR_AUTH_TOKEN
twilio-phone-number: YOUR_TWILIO_PHONE_NUMBER
scheduler:
server:
@@ -272,3 +272,8 @@ hertzbeat:
concurrency-limit: 256
reject-when-limit-reached: true
task-termination-timeout: 5000
+ # Bounds on what a single request to the push gateway may consume.
+ push:
+ max-auto-created-monitors:
${HERTZBEAT_PUSH_MAX_AUTO_CREATED_MONITORS:10000}
+ max-body-bytes: ${HERTZBEAT_PUSH_MAX_BODY_BYTES:5242880}
+ max-samples: ${HERTZBEAT_PUSH_MAX_SAMPLES:10000}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]