gitlab_runner/
logging.rs

1use tracing::{field, metadata::LevelFilter, subscriber::Interest, Metadata, Subscriber};
2use tracing_subscriber::{
3    filter::Filtered,
4    layer::{Context, Filter},
5    registry::LookupSpan,
6    Layer,
7};
8
9use crate::{
10    job::JobLog,
11    runlist::{JobRunList, RunList},
12};
13
14#[derive(Clone, Debug)]
15struct GitlabJob(u64);
16
17#[derive(Debug)]
18struct GitlabJobFinder(Option<GitlabJob>);
19
20impl field::Visit for GitlabJobFinder {
21    fn record_u64(&mut self, field: &field::Field, value: u64) {
22        if field.name() == "gitlab.job" {
23            self.0 = Some(GitlabJob(value));
24        }
25    }
26
27    fn record_debug(&mut self, _field: &field::Field, _value: &dyn std::fmt::Debug) {}
28}
29
30#[derive(Debug, Default)]
31struct GitlabOutput(bool);
32impl field::Visit for GitlabOutput {
33    fn record_bool(&mut self, field: &field::Field, value: bool) {
34        if field.name() == "gitlab.output" {
35            self.0 = value
36        }
37    }
38
39    fn record_debug(&mut self, _field: &field::Field, _value: &dyn std::fmt::Debug) {}
40}
41
42#[derive(Debug)]
43struct OutputToGitlab {
44    joblog: JobLog,
45}
46
47impl field::Visit for OutputToGitlab {
48    fn record_str(&mut self, field: &field::Field, value: &str) {
49        if field.name() == "message" {
50            self.joblog.trace(format!("{}\n", value).as_bytes());
51        }
52    }
53
54    fn record_debug(&mut self, field: &field::Field, value: &dyn std::fmt::Debug) {
55        if field.name() == "message" {
56            self.joblog.trace(format!("{:?}\n", value).as_bytes());
57        }
58    }
59}
60
61/// A [`Layer`] for gitlab
62///
63/// This tracing layer interfaces the tracing infrastructure with running gitlab jobs. It always
64/// has to be registered in the current subscriber
65pub struct GitlabLayer {
66    run_list: RunList<u64, JobLog>,
67}
68
69impl GitlabLayer {
70    /// Create a new GitlabLayer which should be added to the global subscriber
71    /// and a jobs list which should be added to the runner
72    /// ```
73    /// # use gitlab_runner::GitlabLayer;
74    /// # use tracing_subscriber::{prelude::*, Registry};
75    /// #
76    /// let (layer, _jobs) = GitlabLayer::new();
77    /// let subscriber = Registry::default().with(layer).init();
78    /// ```
79    pub fn new<S>() -> (Filtered<Self, GitlabFilter, S>, JobRunList)
80    where
81        S: Subscriber + for<'span> LookupSpan<'span> + 'static,
82    {
83        let run_list = RunList::new();
84        let job_run_list = JobRunList::from(run_list.clone());
85        (
86            Filtered::new(GitlabLayer { run_list }, GitlabFilter {}),
87            job_run_list,
88        )
89    }
90}
91
92impl<S> Layer<S> for GitlabLayer
93where
94    S: Subscriber + Send + Sync + 'static,
95    S: for<'a> LookupSpan<'a>,
96{
97    fn on_event(&self, event: &tracing::Event<'_>, ctx: Context<'_, S>) {
98        let mut gitlab_output = GitlabOutput::default();
99        event.record(&mut gitlab_output);
100
101        if gitlab_output.0 {
102            if let Some(scope) = ctx.event_scope(event) {
103                if let Some(jobinfo) = scope
104                    .from_root()
105                    .find_map(|span| span.extensions().get::<GitlabJob>().cloned())
106                {
107                    if let Some(joblog) = self.run_list.lookup(&jobinfo.0) {
108                        event.record(&mut OutputToGitlab { joblog });
109                    }
110                }
111            }
112        }
113    }
114
115    fn enabled(&self, _metadata: &Metadata<'_>, _ctx: Context<'_, S>) -> bool {
116        // This only gets called if the filters enabled returns true, so no need for futher checks
117        true
118    }
119
120    fn max_level_hint(&self) -> Option<LevelFilter> {
121        Some(LevelFilter::TRACE)
122    }
123
124    fn on_layer(&mut self, subscriber: &mut S) {
125        subscriber.register_filter();
126    }
127
128    fn register_callsite(&self, _metadata: &'static Metadata<'static>) -> Interest {
129        // This only gets called if the filters callsite_enabled returned !never, so no need to
130        // check further
131        Interest::always()
132    }
133
134    fn on_new_span(
135        &self,
136        attrs: &tracing::span::Attributes<'_>,
137        id: &tracing::Id,
138        ctx: Context<'_, S>,
139    ) {
140        let mut f = GitlabJobFinder(None);
141        attrs.record(&mut f);
142        if let Some(job) = f.0 {
143            let span = ctx.span(id).unwrap();
144            let mut extensions = span.extensions_mut();
145            extensions.insert(job);
146        }
147    }
148}
149
150pub struct GitlabFilter {}
151
152impl GitlabFilter {
153    // Only spans and events with gitlab fields are of interest
154    fn is_enabled(&self, metadata: &Metadata) -> bool {
155        metadata
156            .fields()
157            .iter()
158            .any(|f| f.name().starts_with("gitlab."))
159    }
160}
161
162impl<S> Filter<S> for GitlabFilter {
163    fn enabled(&self, meta: &Metadata<'_>, _cx: &Context<'_, S>) -> bool {
164        self.is_enabled(meta)
165    }
166
167    fn callsite_enabled(&self, metadata: &'static Metadata<'static>) -> Interest {
168        if self.is_enabled(metadata) {
169            Interest::always()
170        } else {
171            Interest::never()
172        }
173    }
174}