-
Notifications
You must be signed in to change notification settings - Fork 813
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[collector][emitter] Split metric payloads bigger than 2MB #3454
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -29,6 +29,11 @@ | |
control_char_re = re.compile('[%s]' % re.escape(control_chars)) | ||
|
||
|
||
# Only enforced for the metrics API on our end, for now | ||
MAX_COMPRESSED_SIZE = 2 << 20 # 2MB, the backend should accept up to 3MB but let's be conservative here | ||
MAX_SPLIT_DEPTH = 3 # maximum depth of recursive calls to payload splitting function | ||
|
||
|
||
def remove_control_chars(s, log): | ||
if isinstance(s, str): | ||
sanitized = control_char_re.sub('', s) | ||
|
@@ -72,49 +77,106 @@ def sanitize_payload(item, log, sanitize_func): | |
|
||
return item | ||
|
||
def post_payload(url, message, agentConfig, log): | ||
|
||
def post_payload(url, message, serialize_func, agentConfig, log): | ||
log.debug('http_emitter: attempting postback to ' + url) | ||
|
||
try: | ||
try: | ||
payload = json.dumps(message) | ||
except UnicodeDecodeError: | ||
newmessage = sanitize_payload(message, log, remove_control_chars) | ||
try: | ||
payload = json.dumps(newmessage) | ||
except UnicodeDecodeError: | ||
log.info('Removing undecodable characters from payload') | ||
newmessage = sanitize_payload(newmessage, log, remove_undecodable_chars) | ||
payload = json.dumps(newmessage) | ||
payloads = serialize_func(message, MAX_COMPRESSED_SIZE, 0, log) | ||
except UnicodeDecodeError as ude: | ||
log.error('http_emitter: Unable to convert message to json %s', ude) | ||
log.exception('http_emitter: Unable to convert message to json') | ||
# early return as we can't actually process the message | ||
return | ||
except RuntimeError as rte: | ||
log.error('http_emitter: runtime error dumping message to json %s', rte) | ||
log.exception('http_emitter: runtime error dumping message to json') | ||
# early return as we can't actually process the message | ||
return | ||
except Exception as e: | ||
log.error('http_emitter: unknown exception processing message %s', e) | ||
log.exception('http_emitter: unknown exception processing message') | ||
return | ||
|
||
zipped = zlib.compress(payload) | ||
for payload in payloads: | ||
try: | ||
headers = get_post_headers(agentConfig, payload) | ||
r = requests.post(url, data=payload, timeout=5, headers=headers) | ||
|
||
log.debug("payload_size=%d, compressed_size=%d, compression_ratio=%.3f" | ||
% (len(payload), len(zipped), float(len(payload))/float(len(zipped)))) | ||
r.raise_for_status() | ||
|
||
if r.status_code >= 200 and r.status_code < 205: | ||
log.debug("Payload accepted") | ||
|
||
except Exception: | ||
log.exception("Unable to post payload.") | ||
|
||
|
||
def serialize_payload(message, log): | ||
payload = "" | ||
try: | ||
headers = get_post_headers(agentConfig, zipped) | ||
r = requests.post(url, data=zipped, timeout=5, headers=headers) | ||
payload = json.dumps(message) | ||
except UnicodeDecodeError: | ||
newmessage = sanitize_payload(message, log, remove_control_chars) | ||
try: | ||
payload = json.dumps(newmessage) | ||
except UnicodeDecodeError: | ||
log.info('Removing undecodable characters from payload') | ||
newmessage = sanitize_payload(newmessage, log, remove_undecodable_chars) | ||
payload = json.dumps(newmessage) | ||
|
||
r.raise_for_status() | ||
return payload | ||
|
||
|
||
def serialize_and_compress_legacy_payload(legacy_payload, max_compressed_size, depth, log): | ||
""" | ||
Serialize and compress the legacy payload | ||
""" | ||
serialized_payload = serialize_payload(legacy_payload, log) | ||
zipped = zlib.compress(serialized_payload) | ||
log.debug("payload_size=%d, compressed_size=%d, compression_ratio=%.3f" | ||
% (len(serialized_payload), len(zipped), float(len(serialized_payload))/float(len(zipped)))) | ||
|
||
if r.status_code >= 200 and r.status_code < 205: | ||
log.debug("Payload accepted") | ||
|
||
except Exception: | ||
log.exception("Unable to post payload.") | ||
compressed_payloads = [zipped] | ||
|
||
if len(zipped) > max_compressed_size: | ||
# let's just log a warning for now, splitting the legacy payload is tricky | ||
log.warning("collector payload is above the limit of %dKB compressed", max_compressed_size/(1<<10)) | ||
|
||
return compressed_payloads | ||
|
||
|
||
def serialize_and_compress_metrics_payload(metrics_payload, max_compressed_size, depth, log): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a really cool, elegant solution. However, it took me a bit to understand everything that was going on in it. I think it could stand to use a few comments. I think it would be very easy to make a mistake in editing this function in the future without some added clarity, and I specified some of the places where I think it could stand to be clearer in my other comments. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. very valid comment, I'm going to document this more |
||
""" | ||
Serialize and compress the metrics payload | ||
If the compressed payload is too big, we attempt to split it into smaller payloads | ||
""" | ||
compressed_payloads = [] | ||
|
||
serialized_payload = serialize_payload(metrics_payload, log) | ||
zipped = zlib.compress(serialized_payload) | ||
compression_ratio = float(len(serialized_payload))/float(len(zipped)) | ||
log.debug("payload_size=%d, compressed_size=%d, compression_ratio=%.3f" | ||
% (len(serialized_payload), len(zipped), compression_ratio)) | ||
|
||
if len(zipped) < max_compressed_size: | ||
compressed_payloads.append(zipped) | ||
else: | ||
series = metrics_payload["series"] | ||
|
||
if depth > MAX_SPLIT_DEPTH: | ||
log.error("Maximum depth of payload splitting reached, dropping the %d metrics in this chunk", len(series)) | ||
return compressed_payloads | ||
|
||
nb_chunks = len(zipped)/max_compressed_size + 1 + int(compression_ratio/2) # try to account for the compression | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What does There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nb is number! ha that makes sense. ha. I kept thinking There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I understood what it did, the naming just threw me off |
||
log.debug("payload is too big (%d bytes), splitting it in %d chunks", len(zipped), nb_chunks) | ||
|
||
series_per_chunk = len(series)/nb_chunks + 1 | ||
|
||
for i in range(nb_chunks): | ||
compressed_payloads.extend( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🍰 |
||
serialize_and_compress_metrics_payload({"series": series[i*series_per_chunk:(i+1)*series_per_chunk]}, max_compressed_size, depth+1, log) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you add some comments here? There's a lot going on in this line. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. good comment, this definitely needs more explanations |
||
) | ||
|
||
return compressed_payloads | ||
|
||
|
||
def split_payload(legacy_payload): | ||
|
@@ -149,6 +211,7 @@ def split_payload(legacy_payload): | |
|
||
return legacy_payload, metrics_payload | ||
|
||
|
||
def http_emitter(message, log, agentConfig, endpoint): | ||
api_key = message.get('apiKey') | ||
|
||
|
@@ -164,10 +227,10 @@ def http_emitter(message, log, agentConfig, endpoint): | |
legacy_payload, metrics_payload = split_payload(message) | ||
|
||
# Post legacy payload | ||
post_payload(legacy_url, legacy_payload, agentConfig, log) | ||
post_payload(legacy_url, legacy_payload, serialize_and_compress_legacy_payload, agentConfig, log) | ||
|
||
# Post metrics payload | ||
post_payload(metrics_endpoint, metrics_payload, agentConfig, log) | ||
post_payload(metrics_endpoint, metrics_payload, serialize_and_compress_metrics_payload, agentConfig, log) | ||
|
||
|
||
def get_post_headers(agentConfig, payload): | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How did you arrive at this number? It's cool if it's arbitrary, clearly from the test it can split a giant object.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
arbitrary number yes