Skip to content

Commit 9655f2f

Browse files
committed
perf(otel): optimize log flattening with speculative JSON parsing
Avoid expensive serde_json::from_str on non-JSON string attributes by checking first/last bytes before attempting parse. This eliminates the lexer initialization, character scanning, and error unwind path for ~99% of string attributes. Also reduce clone operations by passing resource map as reference to flatten_scope_log instead of cloning per log record. Benchmarks (593k log records): - flatten_log_record: 28.73µs → 22.55µs (21.5% faster) - flatten_scope_log: 4.47ms → 3.83ms (14.3% faster) - Allocations: unchanged (same data, fewer operations)
1 parent 83c8e57 commit 9655f2f

1 file changed

Lines changed: 40 additions & 24 deletions

File tree

src/otel/logs.rs

Lines changed: 40 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,8 @@ fn category_from_body(body_str: &str) -> &'static str {
117117
/// and returns a `Map` of the flattened json
118118
/// this function is called recursively for each log record object in the otel logs
119119
pub fn flatten_log_record(log_record: &LogRecord) -> Map<String, Value> {
120-
let mut log_record_json: Map<String, Value> = Map::new();
120+
let mut log_record_json: Map<String, Value> =
121+
Map::with_capacity(24 + log_record.attributes.len());
121122
log_record_json.insert(
122123
"time_unix_nano".to_string(),
123124
Value::String(convert_epoch_nano_to_timestamp(
@@ -144,16 +145,26 @@ pub fn flatten_log_record(log_record: &LogRecord) -> Map<String, Value> {
144145
log_record_json.insert(key.clone(), value.clone());
145146

146147
// If value is a string that can be parsed as JSON object, extract its fields
147-
if let Value::String(s) = value
148-
&& let Ok(parsed) = serde_json::from_str::<Value>(s)
149-
&& parsed.is_object()
150-
&& let Ok(flattened_values) = generic_flattening(&parsed)
151-
{
152-
for flattened_value in flattened_values {
153-
if let Value::Object(flattened_obj) = flattened_value {
154-
for (inner_key, inner_value) in flattened_obj {
155-
let prefixed_key = format!("{key}_{inner_key}");
156-
log_record_json.insert(prefixed_key, inner_value);
148+
//
149+
if let Value::String(s) = value {
150+
let trimmed = s.trim();
151+
let looks_like_json = trimmed.len() >= 2
152+
&& matches!(
153+
(trimmed.as_bytes().first(), trimmed.as_bytes().last()),
154+
(Some(b'{'), Some(b'}')) | (Some(b'['), Some(b']'))
155+
);
156+
// Skip speculative JSON parsing unless the body looks like structured JSON
157+
if looks_like_json
158+
&& let Ok(parsed) = serde_json::from_str::<Value>(s)
159+
&& parsed.is_object()
160+
&& let Ok(flattened_values) = generic_flattening(&parsed)
161+
{
162+
for flattened_value in flattened_values {
163+
if let Value::Object(flattened_obj) = flattened_value {
164+
for (inner_key, inner_value) in flattened_obj {
165+
let prefixed_key = format!("{key}_{inner_key}");
166+
log_record_json.insert(prefixed_key, inner_value);
167+
}
157168
}
158169
}
159170
}
@@ -185,7 +196,6 @@ pub fn flatten_log_record(log_record: &LogRecord) -> Map<String, Value> {
185196
"log_record_dropped_attributes_count".to_string(),
186197
Value::Number(log_record.dropped_attributes_count.into()),
187198
);
188-
189199
log_record_json.insert(
190200
"flags".to_string(),
191201
Value::Number((log_record.flags).into()),
@@ -204,9 +214,14 @@ pub fn flatten_log_record(log_record: &LogRecord) -> Map<String, Value> {
204214

205215
/// this function flattens the `ScopeLogs` object
206216
/// and returns a `Vec` of `Map` of the flattened json
207-
fn flatten_scope_log(scope_log: &ScopeLogs, tenant_id: &str) -> Vec<Map<String, Value>> {
208-
let mut vec_scope_log_json = Vec::new();
209-
let mut scope_log_json = Map::new();
217+
fn flatten_scope_log(
218+
scope_log: &ScopeLogs,
219+
tenant_id: &str,
220+
resource_base_json: &Map<String, Value>,
221+
) -> Vec<Map<String, Value>> {
222+
let mut vec_scope_log_json = Vec::with_capacity(scope_log.log_records.len());
223+
// resources and scope merged once
224+
let mut scope_log_json = resource_base_json.clone();
210225
if let Some(scope) = &scope_log.scope {
211226
scope_log_json.insert("scope_name".to_string(), Value::String(scope.name.clone()));
212227
scope_log_json.insert(
@@ -219,11 +234,11 @@ fn flatten_scope_log(scope_log: &ScopeLogs, tenant_id: &str) -> Vec<Map<String,
219234
Value::Number(scope.dropped_attributes_count.into()),
220235
);
221236
}
237+
222238
scope_log_json.insert(
223239
"scope_log_schema_url".to_string(),
224240
Value::String(scope_log.schema_url.clone()),
225241
);
226-
227242
for log_record in &scope_log.log_records {
228243
let log_record_json = flatten_log_record(log_record);
229244
let mut combined_json = scope_log_json.clone();
@@ -262,20 +277,22 @@ where
262277
);
263278
}
264279

280+
resource_log_json.insert(
281+
"schema_url".to_string(),
282+
Value::String(get_schema_url(resource_log).to_string()),
283+
);
265284
let mut vec_resource_logs_json = Vec::new();
266285
let scope_logs = get_scope_logs(resource_log);
267286

268287
for scope_log in scope_logs {
269-
vec_resource_logs_json.extend(flatten_scope_log(scope_log, tenant_id));
288+
vec_resource_logs_json.extend(flatten_scope_log(
289+
scope_log,
290+
tenant_id,
291+
&resource_log_json,
292+
));
270293
}
271294

272-
resource_log_json.insert(
273-
"schema_url".to_string(),
274-
Value::String(get_schema_url(resource_log).to_string()),
275-
);
276-
277295
for resource_logs_json in &mut vec_resource_logs_json {
278-
resource_logs_json.extend(resource_log_json.clone());
279296
vec_otel_json.push(Value::Object(resource_logs_json.clone()));
280297
}
281298
}
@@ -292,7 +309,6 @@ pub fn flatten_otel_protobuf(message: &ExportLogsServiceRequest, tenant_id: &str
292309
tenant_id,
293310
)
294311
}
295-
296312
/// this function performs the custom flattening of the otel logs
297313
/// and returns a `Vec` of `Value::Object` of the flattened json
298314
pub fn flatten_otel_logs(message: &LogsData, tenant_id: &str) -> Vec<Value> {

0 commit comments

Comments
 (0)