Skip to content

Commit c6f1355

Browse files
committed
perf: improve otel metrics ingest path
Batch staging arrow writes per output file and prepare OTEL metric parquet row groups off-thread before sequential writes. This reduces request-path writer contention and hides row-group concat/sort work behind merge/write. Reduce JSON allocation churn in OTEL metric flattening and generic flattening by reusing owned maps, pre-sizing containers, inserting attributes/exemplars directly, and avoiding per-row known-field set construction for series hashing. Also guard shutdown local sync against concurrent local sync cycles and avoid panicking on transient arrow-file metadata races.
1 parent d436512 commit c6f1355

8 files changed

Lines changed: 361 additions & 254 deletions

File tree

src/handlers/http/health_check.rs

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ use once_cell::sync::Lazy;
3232
use tokio::{sync::Mutex, task::JoinSet};
3333
use tracing::{error, info};
3434

35+
use crate::sync::shutdown_local_sync_flush_and_convert;
3536
use crate::utils::get_tenant_id_from_request;
3637
use crate::{parseable::PARSEABLE, storage::object_storage::sync_all_streams};
3738

@@ -84,20 +85,7 @@ async fn perform_sync_operations() {
8485
}
8586

8687
async fn perform_local_sync() {
87-
let mut local_sync_joinset = JoinSet::new();
88-
89-
// Sync staging
90-
PARSEABLE
91-
.streams
92-
.flush_and_convert(&mut local_sync_joinset, false, true);
93-
94-
while let Some(res) = local_sync_joinset.join_next().await {
95-
match res {
96-
Ok(Ok(_)) => info!("Successfully converted arrow files to parquet."),
97-
Ok(Err(err)) => error!("Failed to convert arrow files to parquet. {err:?}"),
98-
Err(err) => error!("Failed to join async task: {err}"),
99-
}
100-
}
88+
shutdown_local_sync_flush_and_convert().await;
10189
}
10290

10391
async fn perform_object_store_sync() {

src/otel/logs.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ pub fn flatten_log_record(log_record: &LogRecord) -> Map<String, Value> {
138138

139139
if log_record.body.is_some() {
140140
let body = &log_record.body;
141-
let body_json = collect_json_from_values(body, &"body".to_string());
141+
let body_json = collect_json_from_values(body, "body");
142142
for (key, value) in &body_json {
143143
// Always insert the original body field as is
144144
log_record_json.insert(key.clone(), value.clone());

src/otel/metrics.rs

Lines changed: 115 additions & 156 deletions
Large diffs are not rendered by default.

src/otel/otel_utils.rs

Lines changed: 34 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -23,21 +23,26 @@ use opentelemetry_proto::tonic::common::v1::{
2323
use serde_json::{Map, Value};
2424

2525
// Value can be one of types - String, Bool, Int, Double, ArrayValue, AnyValue, KeyValueList, Byte
26-
pub fn collect_json_from_value(key: &String, value: OtelValue) -> Map<String, Value> {
26+
pub fn collect_json_from_value(key: &str, value: OtelValue) -> Map<String, Value> {
2727
let mut value_json: Map<String, Value> = Map::new();
28+
insert_json_from_value(&mut value_json, key, value);
29+
value_json
30+
}
31+
32+
fn insert_json_from_value(map: &mut Map<String, Value>, key: &str, value: OtelValue) {
2833
match value {
2934
OtelValue::StringValue(str_val) => {
30-
value_json.insert(key.to_string(), Value::String(str_val));
35+
map.insert(key.to_string(), Value::String(str_val));
3136
}
3237
OtelValue::BoolValue(bool_val) => {
33-
value_json.insert(key.to_string(), Value::Bool(bool_val));
38+
map.insert(key.to_string(), Value::Bool(bool_val));
3439
}
3540
OtelValue::IntValue(int_val) => {
36-
value_json.insert(key.to_string(), Value::String(int_val.to_string()));
41+
map.insert(key.to_string(), Value::String(int_val.to_string()));
3742
}
3843
OtelValue::DoubleValue(double_val) => {
3944
if let Some(number) = serde_json::Number::from_f64(double_val) {
40-
value_json.insert(key.to_string(), Value::Number(number));
45+
map.insert(key.to_string(), Value::Number(number));
4146
}
4247
}
4348
OtelValue::ArrayValue(array_val) => {
@@ -51,17 +56,15 @@ pub fn collect_json_from_value(key: &String, value: OtelValue) -> Map<String, Va
5156
}
5257
};
5358
// Insert the array into the result map
54-
value_json.insert(key.to_string(), Value::String(json_array_string));
59+
map.insert(key.to_string(), Value::String(json_array_string));
5560
}
5661
OtelValue::KvlistValue(kv_list_val) => {
5762
// Create a JSON object to store the key-value list
5863
let kv_object = collect_json_from_key_value_list(kv_list_val);
59-
for (key, value) in kv_object.iter() {
60-
value_json.insert(key.clone(), value.clone());
61-
}
64+
map.extend(kv_object);
6265
}
6366
OtelValue::BytesValue(bytes_val) => {
64-
value_json.insert(
67+
map.insert(
6568
key.to_string(),
6669
Value::String(String::from_utf8_lossy(&bytes_val).to_string()),
6770
);
@@ -72,15 +75,13 @@ pub fn collect_json_from_value(key: &String, value: OtelValue) -> Map<String, Va
7275
);
7376
}
7477
}
75-
76-
value_json
7778
}
7879

7980
/// Recursively converts an ArrayValue into a JSON Value
8081
/// This handles nested array values and key-value lists by recursively
8182
/// converting them to JSON
8283
fn collect_json_from_array_value(array_value: &ArrayValue) -> Value {
83-
let mut json_array = Vec::new();
84+
let mut json_array = Vec::with_capacity(array_value.values.len());
8485
for value in &array_value.values {
8586
if let Some(val) = &value.value {
8687
match val {
@@ -122,12 +123,11 @@ fn collect_json_from_array_value(array_value: &ArrayValue) -> Value {
122123
/// The function iterates through the key-value pairs in the list
123124
/// and collects their JSON representations into a single Map
124125
fn collect_json_from_key_value_list(key_value_list: KeyValueList) -> Map<String, Value> {
125-
let mut kv_list_json: Map<String, Value> = Map::new();
126+
let mut kv_list_json: Map<String, Value> = Map::with_capacity(key_value_list.values.len());
126127
for key_value in key_value_list.values {
127128
if let Some(val) = key_value.value {
128129
if let Some(val) = val.value {
129-
let json_value = collect_json_from_value(&key_value.key, val);
130-
kv_list_json.extend(json_value);
130+
insert_json_from_value(&mut kv_list_json, &key_value.key, val);
131131
} else {
132132
tracing::warn!("Key '{}' has no value in key-value list", key_value.key);
133133
}
@@ -136,37 +136,38 @@ fn collect_json_from_key_value_list(key_value_list: KeyValueList) -> Map<String,
136136
kv_list_json
137137
}
138138

139-
pub fn collect_json_from_anyvalue(key: &String, value: AnyValue) -> Map<String, Value> {
139+
pub fn collect_json_from_anyvalue(key: &str, value: AnyValue) -> Map<String, Value> {
140140
collect_json_from_value(key, value.value.unwrap())
141141
}
142142

143143
//traverse through Value by calling function ollect_json_from_any_value
144-
pub fn collect_json_from_values(values: &Option<AnyValue>, key: &String) -> Map<String, Value> {
145-
let mut value_json: Map<String, Value> = Map::new();
144+
pub fn collect_json_from_values(values: &Option<AnyValue>, key: &str) -> Map<String, Value> {
145+
let mut value_json: Map<String, Value> = Map::with_capacity(1);
146146

147147
for value in values.iter() {
148-
value_json = collect_json_from_anyvalue(key, value.clone());
148+
if let Some(value) = value.value.clone() {
149+
insert_json_from_value(&mut value_json, key, value);
150+
}
149151
}
150152

151153
value_json
152154
}
153155

154156
pub fn value_to_string(value: serde_json::Value) -> String {
155-
match value.clone() {
157+
match value {
156158
e @ Value::Number(_) | e @ Value::Bool(_) => e.to_string(),
157159
Value::String(s) => s,
158160
_ => "".to_string(),
159161
}
160162
}
161163

162164
pub fn flatten_attributes(attributes: &[KeyValue]) -> Map<String, Value> {
163-
let mut attributes_json: Map<String, Value> = Map::new();
165+
let mut attributes_json: Map<String, Value> = Map::with_capacity(attributes.len());
164166
for attribute in attributes {
165-
let key = &attribute.key;
166-
let value = &attribute.value;
167-
let value_json = collect_json_from_values(value, &key.to_string());
168-
for (attr_key, attr_val) in &value_json {
169-
attributes_json.insert(attr_key.clone(), attr_val.clone());
167+
if let Some(value) = &attribute.value
168+
&& let Some(value) = value.value.clone()
169+
{
170+
insert_json_from_value(&mut attributes_json, &attribute.key, value);
170171
}
171172
}
172173
attributes_json
@@ -193,9 +194,12 @@ pub fn insert_bool_if_some(map: &mut Map<String, Value>, key: &str, option: &Opt
193194
}
194195

195196
pub fn insert_attributes(map: &mut Map<String, Value>, attributes: &[KeyValue]) {
196-
let attributes_json = flatten_attributes(attributes);
197-
for (key, value) in attributes_json {
198-
map.insert(key, value);
197+
for attribute in attributes {
198+
if let Some(value) = &attribute.value
199+
&& let Some(value) = value.value.clone()
200+
{
201+
insert_json_from_value(map, &attribute.key, value);
202+
}
199203
}
200204
}
201205

src/parseable/staging/writer.rs

Lines changed: 73 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ use crate::{
4141

4242
use super::StagingError;
4343

44-
const DISK_WRITE_BATCH_ROWS: usize = 16_384;
44+
const DISK_WRITE_BATCH_ROWS: usize = 32_768;
4545

4646
#[derive(Default)]
4747
pub struct Writer {
@@ -83,43 +83,97 @@ impl Writer {
8383
return Ok(());
8484
}
8585

86-
let schema = pending.batches[0].schema();
87-
let batch = concat_batches(&schema, pending.batches.iter())?;
88-
match self.disk.get_mut(filename) {
89-
Some(writer) => writer.write(&batch)?,
90-
None => {
91-
let range = pending.range.expect("pending disk batch must have range");
92-
if let Some(parent) = file_path.parent() {
93-
fs::create_dir_all(parent)?;
94-
}
95-
let mut writer = DiskWriter::try_new(file_path, &schema, range)?;
96-
writer.write(&batch)?;
97-
self.disk.insert(filename.to_owned(), writer);
98-
}
99-
}
86+
write_pending_disk_batch(&mut self.disk, filename.to_owned(), pending, file_path)?;
10087

10188
Ok(())
10289
}
10390

104-
pub fn flush_all_pending_disk(
91+
pub fn take_flushable_disk(
10592
&mut self,
93+
forced: bool,
94+
) -> (HashMap<String, DiskWriter>, PendingDiskWrites) {
95+
let mut flushable_disk = HashMap::new();
96+
let old_disk = std::mem::take(&mut self.disk);
97+
for (filename, writer) in old_disk {
98+
if !forced && writer.is_current() {
99+
self.disk.insert(filename, writer);
100+
} else {
101+
flushable_disk.insert(filename, writer);
102+
}
103+
}
104+
105+
let mut flushable_pending = HashMap::new();
106+
let old_pending = std::mem::take(&mut self.disk_pending);
107+
for (filename, pending) in old_pending {
108+
if !forced && pending.is_current() {
109+
self.disk_pending.insert(filename, pending);
110+
} else {
111+
flushable_pending.insert(filename, pending);
112+
}
113+
}
114+
115+
(flushable_disk, PendingDiskWrites(flushable_pending))
116+
}
117+
}
118+
119+
pub struct PendingDiskWrites(HashMap<String, PendingDiskBatch>);
120+
121+
impl PendingDiskWrites {
122+
pub fn flush_into(
123+
self,
124+
disk: &mut HashMap<String, DiskWriter>,
106125
data_path: &std::path::Path,
107126
) -> Result<(), StagingError> {
108-
let filenames = self.disk_pending.keys().cloned().collect_vec();
109-
for filename in filenames {
110-
self.flush_pending_disk(&filename, data_path.join(&filename))?;
127+
for (filename, pending) in self.0 {
128+
write_pending_disk_batch(disk, filename.clone(), pending, data_path.join(filename))?;
111129
}
112130
Ok(())
113131
}
114132
}
115133

134+
fn write_pending_disk_batch(
135+
disk: &mut HashMap<String, DiskWriter>,
136+
filename: String,
137+
pending: PendingDiskBatch,
138+
file_path: PathBuf,
139+
) -> Result<(), StagingError> {
140+
if pending.batches.is_empty() {
141+
return Ok(());
142+
}
143+
144+
let schema = pending.batches[0].schema();
145+
let batch = concat_batches(&schema, pending.batches.iter())?;
146+
match disk.get_mut(&filename) {
147+
Some(writer) => writer.write(&batch)?,
148+
None => {
149+
let range = pending.range.expect("pending disk batch must have range");
150+
if let Some(parent) = file_path.parent() {
151+
fs::create_dir_all(parent)?;
152+
}
153+
let mut writer = DiskWriter::try_new(file_path, &schema, range)?;
154+
writer.write(&batch)?;
155+
disk.insert(filename, writer);
156+
}
157+
}
158+
159+
Ok(())
160+
}
161+
116162
#[derive(Default)]
117163
struct PendingDiskBatch {
118164
rows: usize,
119165
batches: Vec<RecordBatch>,
120166
range: Option<TimeRange>,
121167
}
122168

169+
impl PendingDiskBatch {
170+
fn is_current(&self) -> bool {
171+
self.range
172+
.as_ref()
173+
.is_some_and(|range| range.contains(Utc::now()))
174+
}
175+
}
176+
123177
pub struct DiskWriter {
124178
inner: StreamWriter<BufWriter<File>>,
125179
path: PathBuf,

0 commit comments

Comments
 (0)