Skip to content

Commit a2dd0b6

Browse files
authored
Fluent builder API (#51)
* WIP: New fluent api * Test fixes
1 parent 170e495 commit a2dd0b6

File tree

18 files changed

+462
-625
lines changed

18 files changed

+462
-625
lines changed

Cargo.toml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ members = [
88
]
99

1010
[workspace.package]
11-
version = "0.0.13"
11+
version = "0.0.14"
1212
edition = "2021"
1313
license = "AGPL-3.0-only"
1414
repository = "https://github.com/Dig-Doug/observation-tools-client"
@@ -33,10 +33,10 @@ nom = "7"
3333
minijinja-autoreload = "2.12.0"
3434
minijinja-embed = "2.12.0"
3535
object_store = "0.12.4"
36-
observation-tools = { path = "crates/observation-tools-client", version = "0.0.13" }
37-
observation-tools-macros = { path = "crates/observation-tools-macros", version = "0.0.13" }
38-
observation-tools-server = { path = "crates/observation-tools-server", version = "0.0.13" }
39-
observation-tools-shared = { path = "crates/observation-tools-shared", version = "0.0.13" }
36+
observation-tools = { path = "crates/observation-tools-client", version = "0.0.14" }
37+
observation-tools-macros = { path = "crates/observation-tools-macros", version = "0.0.14" }
38+
observation-tools-server = { path = "crates/observation-tools-server", version = "0.0.14" }
39+
observation-tools-shared = { path = "crates/observation-tools-shared", version = "0.0.14" }
4040
openapiv3 = "2.2"
4141
prettyplease = "0.2"
4242
progenitor = "0.11.2"

crates/observation-tools-client/index.d.ts

Lines changed: 15 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,13 @@ export declare class ExecutionHandle {
3333
}
3434

3535
/**
36-
* Builder for creating observations (without payload set yet)
36+
* Builder for creating observations
3737
*
38-
* Call `.payload()` or `.custom_payload()` to get an
39-
* `ObservationBuilderWithPayload` that can be built.
38+
* Use the `observe!` macro or `ObservationBuilder::new()` to create a builder,
39+
* then chain methods to configure and send the observation.
40+
*
41+
* Payload methods (`.serde()`, `.debug()`, `.payload()`) send the observation
42+
* immediately and return `SendObservation` for optional waiting.
4043
*/
4144
export declare class ObservationBuilder {
4245
/** Create a new observation builder with the given name */
@@ -54,30 +57,17 @@ export declare class ObservationBuilder {
5457
metadata(key: string, value: string): this
5558
/** Set the source info for the observation */
5659
source(file: string, line: number): this
57-
/** Set the payload as JSON data */
58-
jsonPayload(jsonString: string): ObservationBuilderWithPayload
59-
/** Set the payload with custom data and MIME type */
60-
rawPayload(data: string, mimeType: string): ObservationBuilderWithPayload
61-
/** Set the payload as markdown content */
62-
markdownPayload(content: string): ObservationBuilderWithPayload
60+
/** Set the payload as JSON data, returning a builder that can be sent with an execution */
61+
jsonPayload(jsonString: string): ObservationBuilderWithPayloadNapi
62+
/** Set the payload with custom data and MIME type, returning a builder that can be sent */
63+
rawPayload(data: string, mimeType: string): ObservationBuilderWithPayloadNapi
64+
/** Set the payload as markdown content, returning a builder that can be sent */
65+
markdownPayload(content: string): ObservationBuilderWithPayloadNapi
6366
}
6467

65-
/**
66-
* Builder for creating observations (with payload set)
67-
*
68-
* This struct is returned by `ObservationBuilder::payload()` and
69-
* `ObservationBuilder::custom_payload()`. It has the `build()` methods
70-
* since a payload is required.
71-
*/
72-
export declare class ObservationBuilderWithPayload {
73-
/**
74-
* Build and send the observation
75-
*
76-
* Returns a SendObservation which allows you to wait for the upload to
77-
* complete or get the ObservationHandle immediately.
78-
*
79-
* If sending fails, returns a stub that will fail on `wait_for_upload()`.
80-
*/
68+
/** Intermediate NAPI type that holds a builder and payload, allowing `.send(exe)` pattern */
69+
export declare class ObservationBuilderWithPayloadNapi {
70+
/** Send the observation using the provided execution handle */
8171
send(execution: ExecutionHandle): SendObservation
8272
}
8373

crates/observation-tools-client/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -580,7 +580,7 @@ module.exports.Client = nativeBinding.Client
580580
module.exports.ClientBuilder = nativeBinding.ClientBuilder
581581
module.exports.ExecutionHandle = nativeBinding.ExecutionHandle
582582
module.exports.ObservationBuilder = nativeBinding.ObservationBuilder
583-
module.exports.ObservationBuilderWithPayload = nativeBinding.ObservationBuilderWithPayload
583+
module.exports.ObservationBuilderWithPayloadNapi = nativeBinding.ObservationBuilderWithPayloadNapi
584584
module.exports.ObservationHandle = nativeBinding.ObservationHandle
585585
module.exports.SendObservation = nativeBinding.SendObservation
586586
module.exports.generateExecutionId = nativeBinding.generateExecutionId

crates/observation-tools-client/src/axum/execution_layer.rs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ use std::task::Poll;
1010
use tower::Layer;
1111
use tower::Service;
1212

13-
/// A filter function that determines whether an execution should be created for a request.
13+
/// A filter function that determines whether an execution should be created for
14+
/// a request.
1415
///
1516
/// Returns `true` to create an execution, `false` to skip.
1617
pub type RequestFilter = Arc<dyn Fn(&Request) -> bool + Send + Sync>;
@@ -31,10 +32,11 @@ impl ExecutionLayer {
3132
}
3233
}
3334

34-
/// Set a filter function that determines whether an execution should be created.
35+
/// Set a filter function that determines whether an execution should be
36+
/// created.
3537
///
36-
/// The filter receives a reference to the incoming request and returns `true` to
37-
/// create an execution context, or `false` to skip execution creation.
38+
/// The filter receives a reference to the incoming request and returns `true`
39+
/// to create an execution context, or `false` to skip execution creation.
3840
/// When skipped, the request proceeds without an execution context.
3941
///
4042
/// # Example

crates/observation-tools-client/src/axum/request_observer.rs

Lines changed: 22 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ use crate::observation::ObservationBuilder;
44
use axum::body::Body;
55
use axum::extract::Request;
66
use axum::response::Response;
7-
use bytes::{Bytes, BytesMut};
7+
use bytes::Bytes;
8+
use bytes::BytesMut;
89
use http::header::HeaderMap;
910
use http::header::HeaderName;
1011
use http::header::AUTHORIZATION;
@@ -27,8 +28,9 @@ use tower::Layer;
2728
use tower::Service;
2829

2930
/// State shared between the streaming body and the observation emitter.
30-
/// This is used to collect data as it streams and emit the observation when complete.
31-
/// The observation is emitted in the Drop implementation when the body is finished.
31+
/// This is used to collect data as it streams and emit the observation when
32+
/// complete. The observation is emitted in the Drop implementation when the
33+
/// body is finished.
3234
struct StreamingObserverState {
3335
buffer: BytesMut,
3436
content_type: String,
@@ -71,14 +73,12 @@ impl Drop for StreamingObserverState {
7173
size: bytes.len(),
7274
};
7375

74-
let mut response_body_builder = ObservationBuilder::new("http/response/body");
75-
response_body_builder
76+
ObservationBuilder::new("http/response/body")
7677
.label("http/response")
7778
.label("http/response/body")
7879
.metadata("status", &self.status_code.to_string())
7980
.log_level(self.log_level)
80-
.payload(payload)
81-
.build_with_execution(&self.execution);
81+
.payload_with_execution(payload, &self.execution);
8282
}
8383
}
8484

@@ -246,31 +246,27 @@ where
246246
};
247247

248248
let (parts, body) = req.into_parts();
249-
let mut request_headers_builder = ObservationBuilder::new("http/request/headers");
250-
request_headers_builder
249+
ObservationBuilder::new("http/request/headers")
251250
.label("http/request")
252251
.label("http/request/headers")
253252
.metadata("method", parts.method.to_string())
254253
.metadata("uri", parts.uri.to_string())
255254
.serde(&json!(filter_headers(
256255
&parts.headers,
257256
&config.excluded_headers
258-
)))
259-
.build();
257+
)));
260258

261259
let request_body_bytes = body
262260
.collect()
263261
.await
264262
.map(|collected| collected.to_bytes())
265263
.unwrap_or_else(|_| Bytes::new());
266-
let mut request_body_builder = ObservationBuilder::new("http/request/body");
267-
request_body_builder
264+
ObservationBuilder::new("http/request/body")
268265
.label("http/request")
269266
.label("http/request/body")
270267
.metadata("method", parts.method.to_string())
271268
.metadata("uri", parts.uri.to_string())
272-
.payload(bytes_to_payload(&request_body_bytes, &parts.headers))
273-
.build();
269+
.payload(bytes_to_payload(&request_body_bytes, &parts.headers));
274270

275271
let response = inner
276272
.call(Request::from_parts(parts, Body::from(request_body_bytes)))
@@ -283,28 +279,31 @@ where
283279
500..=599 => LogLevel::Error,
284280
_ => LogLevel::Info,
285281
};
286-
let mut response_headers_builder = ObservationBuilder::new("http/response/headers");
287-
response_headers_builder
282+
ObservationBuilder::new("http/response/headers")
288283
.label("http/response")
289284
.label("http/response/headers")
290285
.metadata("status", &parts.status.as_u16().to_string())
291286
.log_level(log_level)
292287
.serde(&json!(filter_headers(
293288
&parts.headers,
294289
&config.excluded_headers
295-
)))
296-
.build();
290+
)));
297291

298-
// Wrap the response body in a streaming observer that captures data as it flows through
299-
// and emits the observation when the stream completes
292+
// Wrap the response body in a streaming observer that captures data as it flows
293+
// through and emits the observation when the stream completes
300294
let content_type = parts
301295
.headers
302296
.get(CONTENT_TYPE)
303297
.and_then(|v| v.to_str().ok())
304298
.unwrap_or("application/octet-stream")
305299
.to_string();
306-
let streaming_body =
307-
StreamingObserverBody::new(body, content_type, log_level, parts.status.as_u16(), execution);
300+
let streaming_body = StreamingObserverBody::new(
301+
body,
302+
content_type,
303+
log_level,
304+
parts.status.as_u16(),
305+
execution,
306+
);
308307

309308
Ok(Response::from_parts(parts, Body::new(streaming_body)))
310309
})

crates/observation-tools-client/src/context.rs

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -71,11 +71,11 @@ pub(crate) fn clear_global_execution() {
7171
/// // Run two tasks concurrently with different execution contexts
7272
/// let (result1, result2) = tokio::join!(
7373
/// with_execution(execution1, async {
74-
/// observe!("observation-1", "data from task 1")?;
74+
/// observe!("observation-1").serde(&"data from task 1");
7575
/// Ok::<_, Error>(())
7676
/// }),
7777
/// with_execution(execution2, async {
78-
/// observe!("observation-2", "data from task 2")?;
78+
/// observe!("observation-2").serde(&"data from task 2");
7979
/// Ok::<_, Error>(())
8080
/// })
8181
/// );
@@ -110,7 +110,7 @@ pub(crate) fn get_current_tracing_span_id() -> Option<String> {
110110
///
111111
/// // The spawned task will inherit the current execution context
112112
/// tokio::spawn(async move {
113-
/// observe!("spawned-task", "data from spawned task")?;
113+
/// observe!("spawned-task").serde(&"data from spawned task");
114114
/// Ok::<_, Error>(())
115115
/// }.with_observations());
116116
/// ```
@@ -126,9 +126,7 @@ pub trait WithObservations: Future + Sized {
126126
impl<F: Future + Send + 'static> WithObservations for F {
127127
fn with_observations(self) -> WithObservationsFuture<Self::Output> {
128128
match get_current_execution() {
129-
Some(execution) => {
130-
WithObservationsFuture(Box::pin(TASK_EXECUTION.scope(execution, self)))
131-
}
129+
Some(execution) => WithObservationsFuture(Box::pin(TASK_EXECUTION.scope(execution, self))),
132130
None => WithObservationsFuture(Box::pin(self)),
133131
}
134132
}

crates/observation-tools-client/src/lib.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ pub use execution::BeginExecution;
2828
pub use execution::ExecutionHandle;
2929
pub use logger::ObservationLogger;
3030
pub use observation::ObservationBuilder;
31-
pub use observation::ObservationBuilderWithPayload;
3231
pub use observation_handle::ObservationHandle;
3332
pub use observation_handle::SendObservation;
3433
// Re-export procedural macro

crates/observation-tools-client/src/logger.rs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,15 +37,16 @@ impl Log for ObservationLogger {
3737
return;
3838
}
3939

40-
let mut builder = ObservationBuilder::new("ObservationLogger");
41-
builder
40+
let builder = ObservationBuilder::new("ObservationLogger")
4241
.observation_type(ObservationType::LogEntry)
4342
.log_level(record.level().into())
4443
.label(format!("log/{}", record.target()));
45-
if let (Some(file), Some(line)) = (record.file(), record.line()) {
46-
builder.source(file, line);
47-
}
48-
let _ = builder.payload(&format!("{}", record.args())).build();
44+
let builder = if let (Some(file), Some(line)) = (record.file(), record.line()) {
45+
builder.source(file, line)
46+
} else {
47+
builder
48+
};
49+
let _ = builder.payload(format!("{}", record.args()));
4950
}
5051

5152
fn flush(&self) {

0 commit comments

Comments
 (0)