Skip to content
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

Enable relaying log events via FFI #542

Merged
merged 32 commits into from
Dec 6, 2024
Merged

Conversation

nicklan
Copy link
Collaborator

@nicklan nicklan commented Nov 26, 2024

What changes are proposed in this pull request?

This PR provides 3 ways for engines to hook into our logging:

  1. getting a struct Event type with relevant information that they can format
  2. A very simple call that will just pick some defaults and give them a string
  3. A slightly more complex one that lets them control the string formatting a little, but still ultimately just hands over a string

Note that the Compact format seems a bit broken upstream. I've filled tokio-rs/tracing#3157 for that.

Adds 3 new public ffi APIs

How was this change tested?

Running in read_table, and unit tests

@github-actions github-actions bot added the breaking-change Change that will require a version bump label Nov 26, 2024
Copy link

codecov bot commented Nov 26, 2024

Codecov Report

Attention: Patch coverage is 85.15152% with 49 lines in your changes missing coverage. Please review.

Project coverage is 81.63%. Comparing base (695b841) to head (61a274a).
Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
ffi/src/ffi_tracing.rs 85.15% 40 Missing and 9 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #542      +/-   ##
==========================================
+ Coverage   81.54%   81.63%   +0.08%     
==========================================
  Files          67       68       +1     
  Lines       14544    14874     +330     
  Branches    14544    14874     +330     
==========================================
+ Hits        11860    12142     +282     
- Misses       2107     2145      +38     
- Partials      577      587      +10     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

ffi/examples/read-table/read_table.c Outdated Show resolved Hide resolved
ffi/src/ffi_tracing.rs Show resolved Hide resolved
ffi/src/ffi_tracing.rs Show resolved Hide resolved
ffi/examples/read-table/read_table.c Show resolved Hide resolved
Comment on lines +89 to +94
/// `2022-02-15T18:40:14.289898Z INFO fmt: preparing to shave yaks number_of_yaks=3`
FULL,
/// A variant of the FULL formatter, optimized for short line lengths. Fields from the context
/// are appended to the fields of the formatted event, and targets are not shown.
/// Example:
/// `2022-02-17T19:51:05.809287Z INFO fmt_compact: preparing to shave yaks number_of_yaks=3`
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think I understand the difference between these two?
(the example doesn't seem to show any meaningful difference)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I've filled an upstream issue: tokio-rs/tracing#3157

Comment on lines 214 to 217
None => KernelStringSlice {
ptr: std::ptr::null(),
len: 0,
},
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should ideally define a KernelStringSlice::empty() method and use that instead?
(apparently this is the first time we need an empty string slice in the code)

Doing that would allow simply:

let file = metadata.file().map_or(KernelSlice::empty(), |f| kernel_string_slice!(f));

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could also just do:

let file = metadata.file().unwrap_or("");

and then

let event = Event {
      ...
    file: kernel_string_slice!(file),
};

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think having empty is probably good

ffi/src/ffi_tracing.rs Outdated Show resolved Hide resolved
S: Subscriber + for<'a> LookupSpan<'a>,
{
fn on_event(&self, _: &TracingEvent<'_>, _context: Context<'_, S>) {
let mut buf = self.buf.lock().unwrap();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

panic in tracing code... not fun to debug
(again below)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good call, fixed. not sure if spamming the log of the engine with an error is good, but it feels better than silent failure.

Comment on lines +312 to +313
// This repeats some code, but avoids some insane generic wrangling if we try to abstract the
// type of `fmt_layer` over the formatter
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Problem being, that e.g. Layer::compact returns Format<Compact, _>, so we need a different return type for each call? (but everything else is the same)?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's also when we call things like without_time which effects the second generic for Format.

The real issue is, we want to return this. Really we just need an impl Subscriber, but because the concrete types all differ it gets really messy.

I can play around a bit more, but this felt overall more understandable and actually cleaner

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also note that the tracing_core crate introduces Dispatch exactly to abstract over all this where they erase the type. But we can't do that so easily because we want to retain methods like with_filter and pretty(), etc.

ffi/src/ffi_tracing.rs Outdated Show resolved Hide resolved
@nicklan
Copy link
Collaborator Author

nicklan commented Nov 27, 2024

Thanks for the review. I've updated some stuff to remove a bad panic when setting global subscriber. Will address the rest of the comments when I'm back from Thanksgiving.

@@ -176,7 +176,6 @@ mod private {
{
/// Obtains a mutable reference to the handle's underlying object. Unsafe equivalent to
/// [`AsMut::as_mut`].

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clippy fix

@nicklan nicklan requested a review from scovich December 3, 2024 00:57
Copy link
Collaborator

@scovich scovich left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally looks good, just a couple of small (but important) items:

  • NULL pointer in string slice is UB
  • Deceptively unprotective use of kernel_string_slice!

ffi/examples/read-table/read_table.c Show resolved Hide resolved
Comment on lines 29 to 31
fn is_valid(value: u32) -> bool {
static VALID_VALUES: &[u32] = &[0, 1, 2, 3, 4];
VALID_VALUES.contains(&value)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would this be easier to use?

Suggested change
fn is_valid(value: u32) -> bool {
static VALID_VALUES: &[u32] = &[0, 1, 2, 3, 4];
VALID_VALUES.contains(&value)
fn is_valid(self) -> bool {
static VALID_VALUES: &[u32] = &[0, 1, 2, 3, 4];
VALID_VALUES.contains(&(self as u32))

(e.g. max_level.is_valid()?)

(ok to consume self because Copy)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep, that's nicer, thanks

file,
file: metadata
.file()
.map_or(KernelStringSlice::empty(), |f| kernel_string_slice!(f)),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI: This kernel_string_slice invocation is not obviously (syntactically) safe, because the identifier f goes out of scope immediately after the slice is created. In this specific case, f has lifetime of metadata so we're ok... but the macro didn't help enforce that at all.

I still wonder why not just use "" as the empty string slice?

let file = metadata.file().unwrap_or("");
let event = Event {
      ...
    file: kernel_string_slice!(file),

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's fair. I liked the idea of empty for string slice, but you're probably right that just having it be the actual empty string makes the most sense. I'll switch to that

.expect("Failed to set global subscriber");
}
};
($($transform:ident()).*) => {{
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh clever -- I didn't realize we could capture () and . like that!

(only works for these no-arg invocations tho...)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, macros are pretty nice. You can handle arg cases too with nested and repeated args but the macro will get really ugly. I initially wrote a pretty complex macro because I was using the with_ansi(ansi) call, and then realized you can simplify a lot if you get rid of the arg and the need to have a call at all.

ffi/src/lib.rs Outdated
/// Create a new empty `KernelStringSlice`. `len` will be 0, and `ptr` will be `null`.
pub(crate) fn empty() -> Self {
Self {
ptr: std::ptr::null(),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The TryFromSlice docs specifically state:

The slice must be a valid (non-null) pointer

... which is required because we're going to turn the pointer into a reference, and a null reference is UB even if never accessed. We should really use NonNull and its associated dangling() instead -- see e.g. KernelBoolSlice). Tho I suspect that ptr: NonNull::from("") would work just as well for this specific case.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh, good catch! I've actually just removed this and we can just use "" since it avoids all the mess

ffi/examples/read-table/read_table.c Show resolved Hide resolved
}
}

impl From<&tracing::Level> for Level {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is tracing::Level not Copy, that we have to consume a ref instead?

Or does it just so happen all the call sites already have a reference and this avoids the need for (*level).into()?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, I use this only as metadata.level().into(), and metadata.level() returns &Level, so this makes it a little cleaner at the usage site.

Comment on lines +81 to +83
/// Note that setting up such a call back can only be done ONCE. Calling any of
/// `enable_event_tracing`, `enable_log_line_tracing`, or `enable_formatted_log_line_tracing` more
/// than once is a no-op.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this also be part of the safety contract?
(seems like an unusually sharp edge to have inherited from rust code)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I didn't read very carefully... the error case becomes a no-op, so no safety issues whatsoever.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah. We should probably discuss if we should do anything other than just "return false" if you call twice, but for now it's safe. I could turn it into a proper ExternResult with more info, but had the feeling that was just more overhead for the engine which probably just wants to know if it set up logging okay or not.

@nicklan nicklan requested a review from scovich December 5, 2024 01:35
ffi/src/ffi_tracing.rs Outdated Show resolved Hide resolved
ffi/src/ffi_tracing.rs Outdated Show resolved Hide resolved
@nicklan nicklan removed the breaking-change Change that will require a version bump label Dec 6, 2024
Copy link
Collaborator

@zachschuermann zachschuermann left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just a little question but LGTM and i ran locally to take all those pretty colors for a spin! looks awesome!!


// utility code below for setting up the tracing subscriber for events

pub fn set_global_default(dispatch: tracing_core::Dispatch) -> DeltaResult<()> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this supposed to be pub? is it just if consumers want to do something different than enable_event_tracing?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nope, good catch, removed

@nicklan nicklan merged commit eb95c5b into delta-io:main Dec 6, 2024
20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants