Files
windmill/backend/windmill-common/src/tracing_init.rs
Ruben Fiszel 424ca59dfe feat: make WINDMILL_DIR configurable via environment variable (#8215)
* fix: auto-heal corrupted python runtime cache on remote workers

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Revert "fix: auto-heal corrupted python runtime cache on remote workers"

This reverts commit 0ea013a554.

* feat: make WINDMILL_DIR configurable via environment variable

Allow users to configure the base directory for Windmill's tmp/cache files
via the WINDMILL_DIR env var (default: /tmp/windmill). This fixes Python
runtime cache corruption on RHEL systems where systemd-tmpfiles-clean
removes files from /tmp.

Converts TMP_DIR (renamed to WINDMILL_DIR) and all derived cache directory
constants from compile-time const &str (concatcp!) to runtime lazy_static
String values.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: update ee ref

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: update ee ref

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: deref ERROR_DIR lazy_static for AsRef<Path> and Display traits

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: update ee ref to branch name for CI compatibility

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: deref lazy_static constants in all executor files

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: update ee ref

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: update ee ref

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: update ee ref

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: panic if WINDMILL_DIR has trailing slash

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: also reject trailing backslash in WINDMILL_DIR for Windows

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: deref GO_BIN_CACHE_DIR in test utils

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: replace remaining hardcoded /tmp/windmill paths and validate empty WINDMILL_DIR

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: nsjail powershell mount dst, Windows path assumptions, pwsh deref consistency

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: restore Windows /tmp path translation in go and bun executors

The Windows path translation replaces /tmp with the Windows temp dir
(e.g. C:\tmp) before normalizing slashes. Without this, the default
WINDMILL_DIR=/tmp/windmill produces paths without a drive letter on
Windows.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: update ee-repo-ref to 6fd5a2ce908235a17975ad4dbdf0051cd89334f3

This commit updates the EE repository reference after PR #436 was merged in windmill-ee-private.

Previous ee-repo-ref: e8c03e16720833230ebd1878b4c63642ecc6c80f

New ee-repo-ref: 6fd5a2ce908235a17975ad4dbdf0051cd89334f3

Automated by sync-ee-ref workflow.

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
2026-03-04 08:53:25 +00:00

272 lines
9.1 KiB
Rust

/*
* Author: Ruben Fiszel
* Copyright: Windmill Labs, Inc 2022
* This file and its contents are licensed under the AGPLv3 License.
* Please see the included NOTICE for copyright information and
* LICENSE-AGPL for a copy of the license.
*/
use std::{
collections::HashMap,
sync::{Arc, RwLock},
};
use tracing::{level_filters::LevelFilter, Event};
use tracing_appender::non_blocking::{NonBlockingBuilder, WorkerGuard};
use tracing_subscriber::layer::Context;
use tracing_subscriber::{
filter::Targets,
fmt::{format, Layer},
prelude::*,
EnvFilter,
};
use crate::utils::Mode;
fn json_layer<S>() -> Layer<S, format::JsonFields, format::Format<format::Json>> {
tracing_subscriber::fmt::layer()
.json()
.flatten_event(true)
.with_span_list(false)
.with_current_span(true)
}
fn compact_layer<S>() -> Layer<S, format::DefaultFields, format::Format<format::Compact>> {
tracing_subscriber::fmt::layer().compact()
}
lazy_static::lazy_static! {
pub static ref JSON_FMT: bool = std::env::var("JSON_FMT").map(|x| x == "true").unwrap_or(false);
pub static ref QUIET_MODE: bool = std::env::var("QUIET").map(|x| x == "true" || x == "1").unwrap_or(false);
}
/// Target name for verbose logs that should be filtered in quiet mode.
/// Use `tracing::info!(target: windmill_common::tracing_init::VERBOSE_TARGET, ...)` for logs that should be suppressed in quiet mode.
pub const VERBOSE_TARGET: &str = "windmill_verbose";
/// Creates a Targets filter that optionally filters out verbose logs when quiet mode is enabled.
fn create_targets_filter(default_env_filter: LevelFilter) -> Targets {
let targets =
Targets::new().with_target("windmill:job_log", tracing::level_filters::LevelFilter::OFF);
if *QUIET_MODE {
targets
.with_target(VERBOSE_TARGET, tracing::level_filters::LevelFilter::OFF)
.with_default(default_env_filter)
} else {
targets.with_default(default_env_filter)
}
}
pub const LOGS_SERVICE: &str = "logs/services/";
lazy_static::lazy_static! {
pub static ref TMP_WINDMILL_LOGS_SERVICE: String = format!("{}/{}", *crate::worker::WINDMILL_DIR, LOGS_SERVICE);
}
pub fn initialize_tracing(
hostname: &str,
mode: &Mode,
environment: &str,
) -> (WorkerGuard, crate::otel_oss::OtelProvider) {
let style = std::env::var("RUST_LOG_STYLE").unwrap_or_else(|_| "auto".into());
let rust_log_env = std::env::var("RUST_LOG");
let rust_log_stdout_env = std::env::var("RUST_LOG_STDOUT");
if rust_log_env
.as_ref()
.is_ok_and(|x| x == "debug" || x == "info")
{
unsafe {
std::env::set_var(
"RUST_LOG",
&format!("windmill={}", rust_log_env.as_ref().unwrap()),
)
}
} else if rust_log_env.as_ref().is_ok_and(|x| x == "sqlxdebug") {
unsafe {
std::env::set_var("RUST_LOG", "windmill=debug,sqlx=debug");
}
};
let default_env_filter = if rust_log_env.is_ok_and(|x| x == "debug" || x == "sqlxdebug") {
LevelFilter::DEBUG
} else {
LevelFilter::INFO
};
let meter_provider = crate::otel_oss::init_meter_provider(mode, hostname, environment);
#[cfg(all(feature = "otel", feature = "enterprise"))]
let opentelemetry = crate::otel_oss::init_otlp_tracer(mode, hostname, environment)
.map(|x| tracing_opentelemetry::layer().with_tracer(x));
#[cfg(not(all(feature = "otel", feature = "enterprise")))]
let opentelemetry: Option<EnvFilter> = None;
let logs_bridge = crate::otel_oss::init_logs_bridge(&mode, hostname, environment);
use tracing_appender::rolling::{RollingFileAppender, Rotation};
let log_dir = format!("{}/{}/", *TMP_WINDMILL_LOGS_SERVICE, hostname);
std::fs::create_dir_all(&log_dir).unwrap();
let file_appender = RollingFileAppender::builder()
.rotation(Rotation::MINUTELY)
.filename_prefix(format!("{}.log", hostname))
.max_log_files(20)
.build(log_dir)
.expect("Can build tracing file appender");
let (log_file_writer, _guard) = NonBlockingBuilder::default()
.lossy(false)
.finish(file_appender);
// let job_logs_filter = tracing_subscriber::filter::Targets::new()
// .with_target("windmill:job_log", tracing::Level::TRACE);
// Create the base filter for file writer (always uses RUST_LOG)
let file_env_filter = EnvFilter::builder()
.with_default_directive(tracing::level_filters::LevelFilter::ERROR.into())
.from_env_lossy();
// Create the filter for stdout (uses RUST_LOG_STDOUT if available, otherwise RUST_LOG)
let stdout_env_filter = if rust_log_stdout_env.is_ok() {
// Temporarily set RUST_LOG to RUST_LOG_STDOUT value to parse it
let original_rust_log = std::env::var("RUST_LOG").ok();
unsafe {
std::env::set_var("RUST_LOG", rust_log_stdout_env.unwrap());
}
let filter = EnvFilter::builder()
.with_default_directive(tracing::level_filters::LevelFilter::ERROR.into())
.from_env_lossy();
unsafe {
// Restore original RUST_LOG
match original_rust_log {
Some(val) => std::env::set_var("RUST_LOG", val),
None => std::env::remove_var("RUST_LOG"),
}
}
filter
} else {
file_env_filter.clone()
};
// Create a common filter for OTEL logs bridge and tracing layer to respect RUST_LOG
let otel_logs_filter = file_env_filter.clone();
// Apply filter to the opentelemetry tracing layer to prevent debug events from being attached to spans
#[cfg(all(feature = "otel", feature = "enterprise"))]
let opentelemetry_filtered = {
let otel_tracing_filter = file_env_filter.clone();
opentelemetry.map(|layer| layer.with_filter(otel_tracing_filter))
};
#[cfg(not(all(feature = "otel", feature = "enterprise")))]
let opentelemetry_filtered = opentelemetry;
let base_layer = tracing_subscriber::registry()
.with(logs_bridge.with_filter(otel_logs_filter))
.with(opentelemetry_filtered);
match *JSON_FMT {
true => {
// Stdout layer with its own filter
let stdout_layer = json_layer()
.with_writer(std::io::stdout)
.flatten_event(true)
.with_filter(stdout_env_filter)
.with_filter(create_targets_filter(default_env_filter));
// File layer with its own filter
let file_layer = json_layer()
.with_writer(log_file_writer)
.flatten_event(true)
.with_filter(file_env_filter)
.with_filter(create_targets_filter(default_env_filter));
base_layer
.with(stdout_layer)
.with(file_layer)
.with(CountingLayer::new())
.init()
}
false => {
// Stdout layer with its own filter
let stdout_layer = compact_layer()
.with_writer(std::io::stdout)
.with_ansi(style.to_lowercase() != "never")
.with_file(true)
.with_line_number(true)
.with_target(false)
.with_filter(stdout_env_filter)
.with_filter(create_targets_filter(default_env_filter));
// File layer with its own filter
let file_layer = compact_layer()
.with_writer(log_file_writer)
.with_ansi(false) // No ANSI codes in log files
.with_file(true)
.with_line_number(true)
.with_target(false)
.with_filter(file_env_filter)
.with_filter(create_targets_filter(default_env_filter));
base_layer
.with(stdout_layer)
.with(file_layer)
.with(CountingLayer::new())
.init()
}
}
(_guard, meter_provider)
}
lazy_static::lazy_static! {
pub static ref LOG_COUNTING_BY_MIN: Arc<RwLock<HashMap<String, LogCounter>>> = Arc::new(RwLock::new(HashMap::new()));
}
#[derive(Debug)]
pub struct LogCounter {
pub non_error_count: usize,
pub error_count: usize,
}
impl LogCounter {
fn new() -> Self {
LogCounter { non_error_count: 0, error_count: 0 }
}
}
#[derive(Debug)]
struct CountingLayer {}
impl CountingLayer {
pub fn new() -> Self {
CountingLayer {}
}
}
pub const LOG_TIMESTAMP_FMT: &str = "%Y-%m-%d-%H-%M";
impl<S> tracing_subscriber::Layer<S> for CountingLayer
where
S: tracing::Subscriber,
{
fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, S>) {
let level = *event.metadata().level();
let date_str = chrono::Utc::now().format(LOG_TIMESTAMP_FMT).to_string();
let counters = LOG_COUNTING_BY_MIN.write();
if let Ok(mut counters) = counters {
let counter = counters.entry(date_str).or_insert(LogCounter::new());
if level == tracing::Level::ERROR {
counter.error_count += 1;
} else {
counter.non_error_count += 1;
}
} else {
println!("Error getting lock for log counting");
}
}
}