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
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 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 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 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 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}