use tracing::{Metadata, Subscriber, field, metadata::LevelFilter, subscriber::Interest};
use tracing_subscriber::{
Layer,
filter::Filtered,
layer::{Context, Filter},
registry::LookupSpan,
};
use crate::{
job::JobLog,
runlist::{JobRunList, RunList},
};
#[derive(Clone, Debug)]
struct GitlabJob(u64);
#[derive(Debug)]
struct GitlabJobFinder(Option<GitlabJob>);
impl field::Visit for GitlabJobFinder {
fn record_u64(&mut self, field: &field::Field, value: u64) {
if field.name() == "gitlab.job" {
self.0 = Some(GitlabJob(value));
}
}
fn record_debug(&mut self, _field: &field::Field, _value: &dyn std::fmt::Debug) {}
}
#[derive(Debug, Default)]
struct GitlabOutput(bool);
impl field::Visit for GitlabOutput {
fn record_bool(&mut self, field: &field::Field, value: bool) {
if field.name() == "gitlab.output" {
self.0 = value
}
}
fn record_debug(&mut self, _field: &field::Field, _value: &dyn std::fmt::Debug) {}
}
#[derive(Debug)]
struct OutputToGitlab {
joblog: JobLog,
}
impl field::Visit for OutputToGitlab {
fn record_str(&mut self, field: &field::Field, value: &str) {
if field.name() == "message" {
self.joblog.trace(format!("{value}\n").as_bytes());
}
}
fn record_debug(&mut self, field: &field::Field, value: &dyn std::fmt::Debug) {
if field.name() == "message" {
self.joblog.trace(format!("{value:?}\n").as_bytes());
}
}
}
pub struct GitlabLayer {
run_list: RunList<u64, JobLog>,
}
impl GitlabLayer {
pub fn new<S>() -> (Filtered<Self, GitlabFilter, S>, JobRunList)
where
S: Subscriber + for<'span> LookupSpan<'span> + 'static,
{
let run_list = RunList::new();
let job_run_list = JobRunList::from(run_list.clone());
(
Filtered::new(GitlabLayer { run_list }, GitlabFilter {}),
job_run_list,
)
}
}
impl<S> Layer<S> for GitlabLayer
where
S: Subscriber + Send + Sync + 'static,
S: for<'a> LookupSpan<'a>,
{
fn on_event(&self, event: &tracing::Event<'_>, ctx: Context<'_, S>) {
let mut gitlab_output = GitlabOutput::default();
event.record(&mut gitlab_output);
if gitlab_output.0
&& let Some(scope) = ctx.event_scope(event)
&& let Some(jobinfo) = scope
.from_root()
.find_map(|span| span.extensions().get::<GitlabJob>().cloned())
&& let Some(joblog) = self.run_list.lookup(&jobinfo.0)
{
event.record(&mut OutputToGitlab { joblog });
}
}
fn enabled(&self, _metadata: &Metadata<'_>, _ctx: Context<'_, S>) -> bool {
true
}
fn max_level_hint(&self) -> Option<LevelFilter> {
Some(LevelFilter::TRACE)
}
fn on_layer(&mut self, subscriber: &mut S) {
subscriber.register_filter();
}
fn register_callsite(&self, _metadata: &'static Metadata<'static>) -> Interest {
Interest::always()
}
fn on_new_span(
&self,
attrs: &tracing::span::Attributes<'_>,
id: &tracing::Id,
ctx: Context<'_, S>,
) {
let mut f = GitlabJobFinder(None);
attrs.record(&mut f);
if let Some(job) = f.0 {
let span = ctx.span(id).unwrap();
let mut extensions = span.extensions_mut();
extensions.insert(job);
}
}
}
pub struct GitlabFilter {}
impl GitlabFilter {
fn is_enabled(&self, metadata: &Metadata) -> bool {
metadata
.fields()
.iter()
.any(|f| f.name().starts_with("gitlab."))
}
}
impl<S> Filter<S> for GitlabFilter {
fn enabled(&self, meta: &Metadata<'_>, _cx: &Context<'_, S>) -> bool {
self.is_enabled(meta)
}
fn callsite_enabled(&self, metadata: &'static Metadata<'static>) -> Interest {
if self.is_enabled(metadata) {
Interest::always()
} else {
Interest::never()
}
}
}