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
61pub struct GitlabLayer {
66 run_list: RunList<u64, JobLog>,
67}
68
69impl GitlabLayer {
70 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 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 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 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}