Skip to content

Commit 8fd8ecc

Browse files
parmesantotex-dev
andcommitted
Add OpenTelemetry instrumentation
## What changed - `Cargo.toml` — added opentelemetry 0.29, opentelemetry_sdk 0.29 (rt-tokio), opentelemetry-otlp 0.29 (http-json, grpc-tonic), tracing-opentelemetry 0.30 - `src/telemetry.rs` — created OTel init module with `init_otel_tracer()` and `build_otel_layer()`, W3C TraceContext propagator, OTLP exporter defaulting to HTTP/JSON - `src/lib.rs` — registered `pub mod telemetry` - `src/main.rs` — wired OTel layer into `init_logger()`, added provider shutdown on exit - `src/handlers/http/query.rs` — instrumented `query()` (root span `POST /query`), `handle_count_query()`, `handle_non_streaming_query()`, `handle_streaming_query()`, `create_streams_for_distributed()` (with JoinSet span propagation), `into_query()` - `src/query/mod.rs` — instrumented `execute()` with W3C cross-runtime propagation to QUERY_RUNTIME, `Query::execute()`, `get_bin_density()`, `get_manifest_list()` ## Dependencies added - opentelemetry 0.29 - opentelemetry_sdk 0.29 (features: rt-tokio) - opentelemetry-otlp 0.29 (features: http-json, grpc-tonic) - tracing-opentelemetry 0.30 Co-authored-by: otex-dev <dev@otex.dev>
1 parent 2469862 commit 8fd8ecc

6 files changed

Lines changed: 170 additions & 20 deletions

File tree

Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,10 @@ opentelemetry-proto = { git = "https://github.com/open-telemetry/opentelemetry-r
104104
"metrics",
105105
"trace",
106106
] }
107+
opentelemetry = "0.29"
108+
opentelemetry_sdk = { version = "0.29", features = ["rt-tokio"] }
109+
opentelemetry-otlp = { version = "0.29", features = ["http-json", "grpc-tonic"] }
110+
tracing-opentelemetry = "0.30"
107111
prometheus = { version = "0.13.4", default-features = false, features = ["process"] }
108112
prometheus-parse = "0.2.5"
109113
tracing = "0.1"

src/handlers/http/query.rs

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ use std::pin::Pin;
4343
use std::sync::Arc;
4444
use std::time::Instant;
4545
use tokio::task::JoinSet;
46-
use tracing::{error, warn};
46+
use tracing::{error, instrument, warn};
4747

4848
use crate::event::{DEFAULT_TIMESTAMP_KEY, commit_schema};
4949
use crate::metrics::{QUERY_EXECUTE_TIME, increment_query_calls_by_date};
@@ -115,6 +115,7 @@ pub async fn get_records_and_fields(
115115
Ok((Some(records), Some(fields)))
116116
}
117117

118+
#[instrument(name = "POST /query", skip_all, fields(http.request.method = "POST", http.route = "/query"))]
118119
pub async fn query(req: HttpRequest, query_request: Query) -> Result<HttpResponse, QueryError> {
119120
let mut session_state = QUERY_SESSION.get_ctx().state();
120121
let time_range =
@@ -179,6 +180,7 @@ pub async fn query(req: HttpRequest, query_request: Query) -> Result<HttpRespons
179180
///
180181
/// # Returns
181182
/// - `HttpResponse` with the count result as JSON, including fields if requested.
183+
#[instrument(name = "handle_count_query", skip_all, fields(db.collection.name = %table_name))]
182184
async fn handle_count_query(
183185
query_request: &Query,
184186
table_name: &str,
@@ -230,6 +232,7 @@ async fn handle_count_query(
230232
///
231233
/// # Returns
232234
/// - `HttpResponse` with the full query result as a JSON object.
235+
#[instrument(name = "handle_non_streaming_query", skip_all, fields(db.collection.name))]
233236
async fn handle_non_streaming_query(
234237
query: LogicalQuery,
235238
table_name: Vec<String>,
@@ -238,6 +241,7 @@ async fn handle_non_streaming_query(
238241
tenant_id: &Option<String>,
239242
) -> Result<HttpResponse, QueryError> {
240243
let first_table_name = table_name[0].clone();
244+
tracing::Span::current().record("db.collection.name", &first_table_name.as_str());
241245
let (records, fields) = execute(query, query_request.streaming, tenant_id).await?;
242246
let records = match records {
243247
Either::Left(rbs) => rbs,
@@ -283,6 +287,7 @@ async fn handle_non_streaming_query(
283287
///
284288
/// # Returns
285289
/// - `HttpResponse` streaming the query results as NDJSON, optionally prefixed with the fields array.
290+
#[instrument(name = "handle_streaming_query", skip_all, fields(db.collection.name))]
286291
async fn handle_streaming_query(
287292
query: LogicalQuery,
288293
table_name: Vec<String>,
@@ -291,6 +296,7 @@ async fn handle_streaming_query(
291296
tenant_id: &Option<String>,
292297
) -> Result<HttpResponse, QueryError> {
293298
let first_table_name = table_name[0].clone();
299+
tracing::Span::current().record("db.collection.name", &first_table_name.as_str());
294300
let (records_stream, fields) = execute(query, query_request.streaming, tenant_id).await?;
295301
let records_stream = match records_stream {
296302
Either::Left(_) => {
@@ -516,6 +522,7 @@ pub async fn update_schema_when_distributed(
516522
/// Create streams for querier if they do not exist
517523
/// get list of streams from memory and storage
518524
/// create streams for memory from storage if they do not exist
525+
#[instrument(name = "create_streams_for_distributed", skip_all)]
519526
pub async fn create_streams_for_distributed(
520527
streams: Vec<String>,
521528
tenant_id: &Option<String>,
@@ -526,17 +533,21 @@ pub async fn create_streams_for_distributed(
526533
let mut join_set = JoinSet::new();
527534
for stream_name in streams {
528535
let id = tenant_id.to_owned();
529-
join_set.spawn(async move {
530-
let result = PARSEABLE
531-
.create_stream_and_schema_from_storage(&stream_name, &id)
532-
.await;
533-
534-
if let Err(e) = &result {
535-
warn!("Failed to create stream '{}': {}", stream_name, e);
536-
}
537-
538-
(stream_name, result)
539-
});
536+
let span = tracing::Span::current();
537+
join_set.spawn(tracing::Instrument::instrument(
538+
async move {
539+
let result = PARSEABLE
540+
.create_stream_and_schema_from_storage(&stream_name, &id)
541+
.await;
542+
543+
if let Err(e) = &result {
544+
warn!("Failed to create stream '{}': {}", stream_name, e);
545+
}
546+
547+
(stream_name, result)
548+
},
549+
span,
550+
));
540551
}
541552

542553
while let Some(result) = join_set.join_next().await {
@@ -579,6 +590,7 @@ impl FromRequest for Query {
579590
}
580591
}
581592

593+
#[instrument(name = "into_query", skip_all, fields(db.query.text = %query.query))]
582594
pub async fn into_query(
583595
query: &Query,
584596
session_state: &SessionState,

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ mod static_schema;
4949
mod stats;
5050
pub mod storage;
5151
pub mod sync;
52+
pub mod telemetry;
5253
pub mod tenants;
5354
pub mod users;
5455
pub mod utils;

src/main.rs

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,10 @@ use std::process::exit;
1919
*/
2020
#[cfg(feature = "kafka")]
2121
use parseable::connectors;
22+
use opentelemetry_sdk::trace::SdkTracerProvider;
2223
use parseable::{
2324
IngestServer, ParseableServer, QueryServer, Server, banner, metrics, option::Mode,
24-
parseable::PARSEABLE, rbac, storage,
25+
parseable::PARSEABLE, rbac, storage, telemetry,
2526
};
2627
use tokio::signal::ctrl_c;
2728
use tokio::sync::oneshot;
@@ -33,7 +34,7 @@ use tracing_subscriber::{EnvFilter, Registry, fmt};
3334

3435
#[actix_web::main]
3536
async fn main() -> anyhow::Result<()> {
36-
init_logger();
37+
let otel_provider = init_logger();
3738
// Install the rustls crypto provider before any TLS operations.
3839
// This is required for rustls 0.23+ which needs an explicit crypto provider.
3940
// If the installation fails, log a warning but continue execution.
@@ -95,10 +96,17 @@ async fn main() -> anyhow::Result<()> {
9596
parseable_server.await?;
9697
}
9798

99+
// Flush any buffered OTel spans before exit
100+
if let Some(provider) = otel_provider {
101+
if let Err(e) = provider.shutdown() {
102+
warn!("Failed to shut down OTel tracer provider: {:?}", e);
103+
}
104+
}
105+
98106
Ok(())
99107
}
100108

101-
pub fn init_logger() {
109+
pub fn init_logger() -> Option<SdkTracerProvider> {
102110
let filter_layer = EnvFilter::try_from_default_env().unwrap_or_else(|_| {
103111
let default_level = if cfg!(debug_assertions) {
104112
Level::DEBUG
@@ -116,10 +124,23 @@ pub fn init_logger() {
116124
.with_target(true)
117125
.compact();
118126

119-
Registry::default()
120-
.with(filter_layer)
121-
.with(fmt_layer)
122-
.init();
127+
let otel_provider = telemetry::init_otel_tracer();
128+
129+
if let Some(ref provider) = otel_provider {
130+
let otel_layer = telemetry::build_otel_layer(provider);
131+
Registry::default()
132+
.with(filter_layer)
133+
.with(fmt_layer)
134+
.with(otel_layer)
135+
.init();
136+
} else {
137+
Registry::default()
138+
.with(filter_layer)
139+
.with(fmt_layer)
140+
.init();
141+
}
142+
143+
otel_provider
123144
}
124145

125146
#[cfg(windows)]

src/query/mod.rs

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ use std::sync::{Arc, RwLock};
5555
use std::task::{Context, Poll};
5656
use sysinfo::System;
5757
use tokio::runtime::Runtime;
58+
use tracing::instrument;
5859

5960
use self::error::ExecuteError;
6061
use self::stream_schema_provider::GlobalSchemaProvider;
@@ -133,6 +134,7 @@ impl InMemorySessionContext {
133134

134135
/// This function executes a query on the dedicated runtime, ensuring that the query is not isolated to a single thread/CPU
135136
/// at a time and has access to the entire thread pool, enabling better concurrent processing, and thus quicker results.
137+
#[instrument(name = "query.execute", skip_all, fields(query.streaming = %is_streaming))]
136138
pub async fn execute(
137139
query: Query,
138140
is_streaming: bool,
@@ -165,8 +167,33 @@ pub async fn execute(
165167
ExecuteError,
166168
> {
167169
let id = tenant_id.clone();
170+
171+
// QUERY_RUNTIME is a separate Runtime::new() (different OS thread pool).
172+
// tracing::Span does NOT propagate OTel context across OS threads.
173+
// Use W3C TraceContext propagation to preserve the trace ID.
174+
let mut carrier = std::collections::HashMap::new();
175+
{
176+
use tracing_opentelemetry::OpenTelemetrySpanExt;
177+
let cx = tracing::Span::current().context();
178+
opentelemetry::global::get_text_map_propagator(|propagator| {
179+
propagator.inject_context(&cx, &mut carrier);
180+
});
181+
}
182+
168183
QUERY_RUNTIME
169-
.spawn(async move { query.execute(is_streaming, &id).await })
184+
.spawn(async move {
185+
// Extract the propagated context on the QUERY_RUNTIME thread
186+
let parent_cx = opentelemetry::global::get_text_map_propagator(|propagator| {
187+
propagator.extract(&carrier)
188+
});
189+
let span = tracing::info_span!("query.execute_runtime");
190+
{
191+
use tracing_opentelemetry::OpenTelemetrySpanExt;
192+
span.set_parent(parent_cx);
193+
}
194+
let _guard = span.enter();
195+
query.execute(is_streaming, &id).await
196+
})
170197
.await
171198
.expect("The Join should have been successful")
172199
}
@@ -272,6 +299,7 @@ impl Query {
272299
/// this function returns the result of the query
273300
/// if streaming is true, it returns a stream
274301
/// if streaming is false, it returns a vector of record batches
302+
#[instrument(name = "Query.execute_datafusion", skip_all, fields(query.streaming = %is_streaming))]
275303
pub async fn execute(
276304
&self,
277305
is_streaming: bool,
@@ -526,6 +554,7 @@ impl CountsRequest {
526554
/// This function is supposed to read maninfest files for the given stream,
527555
/// get the sum of `num_rows` between the `startTime` and `endTime`,
528556
/// divide that by number of bins and return in a manner acceptable for the console
557+
#[instrument(name = "get_bin_density", skip_all, fields(db.collection.name = %self.stream))]
529558
pub async fn get_bin_density(
530559
&self,
531560
tenant_id: &Option<String>,
@@ -731,6 +760,7 @@ pub fn resolve_stream_names(sql: &str) -> Result<Vec<String>, anyhow::Error> {
731760
Ok(tables)
732761
}
733762

763+
#[instrument(name = "get_manifest_list", skip_all, fields(db.collection.name = %stream_name))]
734764
pub async fn get_manifest_list(
735765
stream_name: &str,
736766
time_range: &TimeRange,

src/telemetry.rs

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/*
2+
* Parseable Server (C) 2022 - 2025 Parseable, Inc.
3+
*
4+
* This program is free software: you can redistribute it and/or modify
5+
* it under the terms of the GNU Affero General Public License as
6+
* published by the Free Software Foundation, either version 3 of the
7+
* License, or (at your option) any later version.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU Affero General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU Affero General Public License
15+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
16+
*
17+
*/
18+
19+
use opentelemetry::global;
20+
use opentelemetry::trace::TracerProvider;
21+
use opentelemetry_otlp::WithExportConfig;
22+
use opentelemetry_sdk::propagation::TraceContextPropagator;
23+
use opentelemetry_sdk::trace::{BatchSpanProcessor, SdkTracerProvider};
24+
use opentelemetry_sdk::Resource;
25+
use tracing_opentelemetry::OpenTelemetryLayer;
26+
27+
/// Initializes the OpenTelemetry tracer provider if `OTEL_EXPORTER_OTLP_ENDPOINT` is set.
28+
///
29+
/// Returns `Some(SdkTracerProvider)` when tracing is configured, `None` otherwise.
30+
/// The caller must call `provider.shutdown()` before process exit to flush buffered spans.
31+
pub fn init_otel_tracer() -> Option<SdkTracerProvider> {
32+
let endpoint = std::env::var("OTEL_EXPORTER_OTLP_ENDPOINT").ok()?;
33+
if endpoint.is_empty() {
34+
return None;
35+
}
36+
37+
// Register the W3C TraceContext propagator globally.
38+
// This is REQUIRED unconditionally for cross-runtime context propagation
39+
// (e.g., QUERY_RUNTIME uses a separate OS thread pool).
40+
global::set_text_map_propagator(TraceContextPropagator::new());
41+
42+
let protocol = std::env::var("OTEL_EXPORTER_OTLP_PROTOCOL").unwrap_or_default();
43+
44+
let exporter = match protocol.as_str() {
45+
"grpc" => opentelemetry_otlp::SpanExporter::builder()
46+
.with_tonic()
47+
.with_endpoint(&endpoint)
48+
.build()
49+
.expect("Failed to build gRPC OTLP span exporter"),
50+
_ => opentelemetry_otlp::SpanExporter::builder()
51+
.with_http()
52+
.with_endpoint(&endpoint)
53+
.build()
54+
.expect("Failed to build HTTP/JSON OTLP span exporter"),
55+
};
56+
57+
let processor = BatchSpanProcessor::builder(exporter).build();
58+
59+
let resource = Resource::builder()
60+
.with_service_name("parseable")
61+
.build();
62+
63+
let provider = SdkTracerProvider::builder()
64+
.with_span_processor(processor)
65+
.with_resource(resource)
66+
.build();
67+
68+
Some(provider)
69+
}
70+
71+
/// Builds a `tracing_opentelemetry::OpenTelemetryLayer` from the given provider.
72+
///
73+
/// Compose this layer into the `tracing_subscriber::Registry` alongside other layers.
74+
pub fn build_otel_layer<S>(
75+
provider: &SdkTracerProvider,
76+
) -> OpenTelemetryLayer<S, opentelemetry_sdk::trace::SdkTracer>
77+
where
78+
S: tracing::Subscriber + for<'span> tracing_subscriber::registry::LookupSpan<'span>,
79+
{
80+
let tracer = provider.tracer("parseable");
81+
tracing_opentelemetry::layer().with_tracer(tracer)
82+
}

0 commit comments

Comments
 (0)