|
| 1 | +use gitlab_runner::GitlabLayer; |
| 2 | +use tracing::{ |
| 3 | + Event, Level, Metadata, Subscriber, |
| 4 | + field::{self, Field, FieldSet}, |
| 5 | + span::{Attributes, Id}, |
| 6 | + subscriber::Interest, |
| 7 | +}; |
| 8 | +use tracing_subscriber::{ |
| 9 | + Layer, |
| 10 | + filter::{Filtered, Targets}, |
| 11 | + layer::{Context, Filter}, |
| 12 | + registry::LookupSpan, |
| 13 | +}; |
| 14 | + |
| 15 | +struct OutputTester(bool); |
| 16 | + |
| 17 | +impl field::Visit for OutputTester { |
| 18 | + fn record_bool(&mut self, field: &field::Field, value: bool) { |
| 19 | + if field.name() == "obs_gitlab_runner.output" { |
| 20 | + self.0 = value |
| 21 | + } |
| 22 | + } |
| 23 | + |
| 24 | + fn record_debug(&mut self, _field: &field::Field, _value: &dyn std::fmt::Debug) {} |
| 25 | +} |
| 26 | + |
| 27 | +struct MessageExtractor(Option<String>); |
| 28 | + |
| 29 | +impl field::Visit for MessageExtractor { |
| 30 | + fn record_str(&mut self, field: &Field, value: &str) { |
| 31 | + if field.name() == "message" { |
| 32 | + self.0 = Some(value.to_owned()); |
| 33 | + } |
| 34 | + } |
| 35 | + |
| 36 | + fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) { |
| 37 | + if field.name() == "message" { |
| 38 | + self.0 = Some(format!("{value:?}")); |
| 39 | + } |
| 40 | + } |
| 41 | +} |
| 42 | + |
| 43 | +// This mostly wraps a standard GitlabLayer, but it bypasses the filter to pass |
| 44 | +// through any events with `obs_gitlab_runner.output` set, rewriting them to |
| 45 | +// instead use `gitlab.output`. |
| 46 | +pub struct GitLabForwarder<S: Subscriber, F: Filter<S>>(Filtered<GitlabLayer, F, S>); |
| 47 | + |
| 48 | +impl<S: Subscriber + Send + Sync + 'static + for<'span> LookupSpan<'span>, F: Filter<S> + 'static> |
| 49 | + GitLabForwarder<S, F> |
| 50 | +{ |
| 51 | + pub fn new(inner: Filtered<GitlabLayer, F, S>) -> Filtered<Self, Targets, S> { |
| 52 | + GitLabForwarder(inner).with_filter(Targets::new().with_targets([ |
| 53 | + ("obs_gitlab_runner", Level::TRACE), |
| 54 | + // This target is used to inject the current job ID, which |
| 55 | + // gitlab-runner needs to actually send the logs out. |
| 56 | + ("gitlab_runner::gitlab::job", Level::ERROR), |
| 57 | + ])) |
| 58 | + } |
| 59 | +} |
| 60 | + |
| 61 | +impl<S: Subscriber + Send + Sync + 'static + for<'span> LookupSpan<'span>, F: Filter<S> + 'static> |
| 62 | + Layer<S> for GitLabForwarder<S, F> |
| 63 | +{ |
| 64 | + fn on_register_dispatch(&self, subscriber: &tracing::Dispatch) { |
| 65 | + self.0.on_register_dispatch(subscriber); |
| 66 | + } |
| 67 | + |
| 68 | + fn on_layer(&mut self, subscriber: &mut S) { |
| 69 | + self.0.on_layer(subscriber); |
| 70 | + } |
| 71 | + |
| 72 | + fn register_callsite(&self, metadata: &'static Metadata<'static>) -> Interest { |
| 73 | + self.0.register_callsite(metadata) |
| 74 | + } |
| 75 | + |
| 76 | + fn enabled(&self, metadata: &Metadata<'_>, ctx: Context<'_, S>) -> bool { |
| 77 | + // This controls *global* event filtering, not local, so the inner filter |
| 78 | + // should always return `true`. But we need to call it anyway, because |
| 79 | + // `Filter` will *save internal state* that's needed for other API |
| 80 | + // calls, and thus otherwise the event will always be treated as |
| 81 | + // disabled. (Of course, events in the span we want to forward will |
| 82 | + // also be disabled by this, which is why bypassing the filter in |
| 83 | + // `on_event` is important.) |
| 84 | + let enabled = self.0.enabled(metadata, ctx.clone()); |
| 85 | + assert!(enabled); |
| 86 | + true |
| 87 | + } |
| 88 | + |
| 89 | + fn event_enabled(&self, event: &Event<'_>, ctx: Context<'_, S>) -> bool { |
| 90 | + self.0.event_enabled(event, ctx) |
| 91 | + } |
| 92 | + |
| 93 | + fn on_new_span(&self, attrs: &Attributes<'_>, id: &Id, ctx: Context<'_, S>) { |
| 94 | + self.0.on_new_span(attrs, id, ctx); |
| 95 | + } |
| 96 | + |
| 97 | + fn on_follows_from(&self, span: &Id, follows: &Id, ctx: Context<'_, S>) { |
| 98 | + self.0.on_follows_from(span, follows, ctx); |
| 99 | + } |
| 100 | + |
| 101 | + fn on_event(&self, event: &Event<'_>, ctx: Context<'_, S>) { |
| 102 | + let mut visitor = OutputTester(false); |
| 103 | + event.record(&mut visitor); |
| 104 | + if !visitor.0 { |
| 105 | + // No special behavior needed, so just forward it as-is. |
| 106 | + self.0.on_event(event, ctx); |
| 107 | + return; |
| 108 | + } |
| 109 | + |
| 110 | + let mut visitor = MessageExtractor(None); |
| 111 | + event.record(&mut visitor); |
| 112 | + let Some(message) = visitor.0 else { |
| 113 | + return; |
| 114 | + }; |
| 115 | + |
| 116 | + // Create a new event that contains the fields needed for gitlab-runner. |
| 117 | + let fields = FieldSet::new(&["gitlab.output", "message"], event.metadata().callsite()); |
| 118 | + let mut iter = fields.iter(); |
| 119 | + let values = [ |
| 120 | + // "gitlab.output = true" |
| 121 | + (&iter.next().unwrap(), Some(&true as &dyn tracing::Value)), |
| 122 | + // "message" |
| 123 | + (&iter.next().unwrap(), Some(&message as &dyn tracing::Value)), |
| 124 | + ]; |
| 125 | + |
| 126 | + let value_set = fields.value_set(&values); |
| 127 | + |
| 128 | + let event = if event.is_contextual() { |
| 129 | + // This event's parent is None, but if that's given to new_child_of, |
| 130 | + // then this will be treated as an event at the *root*, i.e. |
| 131 | + // completely parentless. By using `Event::new`, another contextual |
| 132 | + // event will be created, which can still be tied to the correct |
| 133 | + // `event_span`. |
| 134 | + Event::new(event.metadata(), &value_set) |
| 135 | + } else { |
| 136 | + Event::new_child_of(event.parent().cloned(), event.metadata(), &value_set) |
| 137 | + }; |
| 138 | + |
| 139 | + // Bypass the filter completely, because the event was almost certainly |
| 140 | + // filtered out in its `enabled` due to lacking `gitlab.output`. |
| 141 | + self.0.inner().on_event(&event, ctx); |
| 142 | + } |
| 143 | + |
| 144 | + fn on_enter(&self, id: &Id, ctx: Context<'_, S>) { |
| 145 | + self.0.on_enter(id, ctx) |
| 146 | + } |
| 147 | + |
| 148 | + fn on_exit(&self, id: &Id, ctx: Context<'_, S>) { |
| 149 | + self.0.on_exit(id, ctx) |
| 150 | + } |
| 151 | + |
| 152 | + fn on_close(&self, id: Id, ctx: Context<'_, S>) { |
| 153 | + self.0.on_close(id, ctx) |
| 154 | + } |
| 155 | + |
| 156 | + fn on_id_change(&self, old: &Id, new: &Id, ctx: Context<'_, S>) { |
| 157 | + self.0.on_id_change(old, new, ctx); |
| 158 | + } |
| 159 | +} |
| 160 | + |
| 161 | +#[macro_export] |
| 162 | +macro_rules! outputln { |
| 163 | + ($($args:tt)*) => { |
| 164 | + ::tracing::trace!(obs_gitlab_runner.output = true, $($args)*) |
| 165 | + }; |
| 166 | +} |
0 commit comments