Skip to main content

gitlab_runner/
logging.rs

1use tracing::{Metadata, Subscriber, field, metadata::LevelFilter, subscriber::Interest};
2use tracing_subscriber::{
3    Layer,
4    filter::Filtered,
5    layer::{Context, Filter},
6    registry::LookupSpan,
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!("{value}\n").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!("{value:?}\n").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            && let Some(scope) = ctx.event_scope(event)
103            && let Some(jobinfo) = scope
104                .from_root()
105                .find_map(|span| span.extensions().get::<GitlabJob>().cloned())
106            && let Some(joblog) = self.run_list.lookup(&jobinfo.0)
107        {
108            event.record(&mut OutputToGitlab { joblog });
109        }
110    }
111
112    fn enabled(&self, _metadata: &Metadata<'_>, _ctx: Context<'_, S>) -> bool {
113        // This only gets called if the filters enabled returns true, so no need for futher checks
114        true
115    }
116
117    fn max_level_hint(&self) -> Option<LevelFilter> {
118        Some(LevelFilter::TRACE)
119    }
120
121    fn on_layer(&mut self, subscriber: &mut S) {
122        subscriber.register_filter();
123    }
124
125    fn register_callsite(&self, _metadata: &'static Metadata<'static>) -> Interest {
126        // This only gets called if the filters callsite_enabled returned !never, so no need to
127        // check further
128        Interest::always()
129    }
130
131    fn on_new_span(
132        &self,
133        attrs: &tracing::span::Attributes<'_>,
134        id: &tracing::Id,
135        ctx: Context<'_, S>,
136    ) {
137        let mut f = GitlabJobFinder(None);
138        attrs.record(&mut f);
139        if let Some(job) = f.0 {
140            let span = ctx.span(id).unwrap();
141            let mut extensions = span.extensions_mut();
142            extensions.insert(job);
143        }
144    }
145}
146
147pub struct GitlabFilter {}
148
149impl GitlabFilter {
150    // Only spans and events with gitlab fields are of interest
151    fn is_enabled(&self, metadata: &Metadata) -> bool {
152        metadata
153            .fields()
154            .iter()
155            .any(|f| f.name().starts_with("gitlab."))
156    }
157}
158
159impl<S> Filter<S> for GitlabFilter {
160    fn enabled(&self, meta: &Metadata<'_>, _cx: &Context<'_, S>) -> bool {
161        self.is_enabled(meta)
162    }
163
164    fn callsite_enabled(&self, metadata: &'static Metadata<'static>) -> Interest {
165        if self.is_enabled(metadata) {
166            Interest::always()
167        } else {
168            Interest::never()
169        }
170    }
171}