gitlab-runner 0.0.5

Helper crate to build custom gitlab runners
Documentation
use tracing::{field, metadata::LevelFilter, Subscriber};
use tracing_subscriber::{registry::LookupSpan, Layer};

use crate::{job::JobData, runlist::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 {
    jobdata: JobData,
}

impl field::Visit for OutputToGitlab {
    fn record_str(&mut self, field: &field::Field, value: &str) {
        if field.name() == "message" {
            self.jobdata.trace(format!("{}\n", value).as_bytes());
        }
    }

    fn record_debug(&mut self, field: &field::Field, value: &dyn std::fmt::Debug) {
        if field.name() == "message" {
            self.jobdata.trace(format!("{:?}\n", value).as_bytes());
        }
    }
}

/// A [`Layer`] for gitlab
///
/// This tracing layer interfaces the tracing infrastructure with running gitlab jobs. It always
/// has to be registered in the current subscriber
pub struct GitlabLayer {
    run_list: RunList<u64, JobData>,
}

impl GitlabLayer {
    pub(crate) fn new(run_list: RunList<u64, JobData>) -> Self {
        GitlabLayer { 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: tracing_subscriber::layer::Context<'_, S>) {
        let mut gitlab_output = GitlabOutput::default();
        event.record(&mut gitlab_output);

        if gitlab_output.0 {
            if let Some(scope) = ctx.event_scope(event) {
                if let Some(jobinfo) = scope
                    .from_root()
                    .find_map(|span| span.extensions().get::<GitlabJob>().cloned())
                {
                    if let Some(jobdata) = self.run_list.lookup(&jobinfo.0) {
                        event.record(&mut OutputToGitlab { jobdata });
                    }
                }
            }
        }
    }

    fn enabled(
        &self,
        metadata: &tracing::Metadata<'_>,
        ctx: tracing_subscriber::layer::Context<'_, S>,
    ) -> bool {
        let _ = (metadata, ctx);
        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 tracing::Metadata<'static>,
    ) -> tracing::subscriber::Interest {
        tracing::subscriber::Interest::always()
    }

    fn on_new_span(
        &self,
        attrs: &tracing::span::Attributes<'_>,
        id: &tracing::Id,
        ctx: tracing_subscriber::layer::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);
        }
    }
}