Skip to content

feat: adding a log file support #200

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
Open

Conversation

alocay
Copy link
Contributor

@alocay alocay commented Jul 16, 2025

Adding support for logging to a file via config option logging.path. When provided the server will attempt to build the log path and then initialize a RollingFileAppender log writer. If any of this failed or if not provided, logging defaults to stderr.

Note: Decided to default to stderr (when not provided) since the server will only be able to log out to specific places which it has permissions for. Since this isn't not easily known, the easiest thing is to defualt to stderr and let the user setup file logging, pointing to a location that it can log to.

@alocay alocay requested a review from a team as a code owner July 16, 2025 13:19
@apollo-librarian
Copy link

apollo-librarian bot commented Jul 16, 2025

✅ Docs preview ready

The preview is ready to be viewed. View the preview

File Changes

0 new, 1 changed, 0 removed
* (developer-tools)/apollo-mcp-server/(latest)/quickstart.mdx

Build ID: 580029315949ed2b60d33392

URL: https://www.apollographql.com/docs/deploy-preview/580029315949ed2b60d33392

@alocay alocay changed the title feat: adding a log file support when configured feat: adding a log file support Jul 16, 2025
@alocay
Copy link
Contributor Author

alocay commented Jul 22, 2025

Moved logging.rs to src/ and combined with logging setup functions that were added

Copy link
Contributor

@nicholascioli nicholascioli left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good! I left a few nits.

@@ -1,19 +1,18 @@
use std::path::PathBuf;

use crate::runtime::logging::init::setup_logging;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Re-export the setup_logging method under the logging module and rename it to setup so that it doesn't embed the name of the module into it. Also merge it with the existing use on line 14.

Then you could use it like:

// On line 14
use runtime::{logging, IdOrDefault};

// Wherever you used `setup_logging`
logging::setup(...);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, refer to the comment on the logging struct. This should probably be a member function of logging.

//!
//! This module is only used by the main binary and provides helper code
//! related to runtime configuration.

mod config;
mod graphos;
mod introspection;
mod logging;
pub(crate) mod logging;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: This can probably be not public if the setup function is a member on the logging struct.

Comment on lines +3 to +7
use super::{
OperationSource, SchemaSource, graphos::GraphOSConfig, introspection::Introspection,
overrides::Overrides,
};
use crate::runtime::logging::Logging;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: These can be merged by just using super::logging:Logging

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, any reason why the super imports were moved here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm I was altering things but I don't remember moving any imports myself. Maybe it was RustRover or clippy after I made some change (which was removed later).

//!
//! This module is only used by the main binary and provides logging config structures and setup
//! helper functions
pub(crate) mod init;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: This can be private.

Comment on lines +11 to +25
pub fn setup_logging(config: &Config) -> Result<Option<WorkerGuard>, anyhow::Error> {
let mut env_filter = EnvFilter::from_default_env().add_directive(config.logging.level.into());

if config.logging.level == Level::INFO {
env_filter = env_filter
.add_directive("rmcp=warn".parse()?)
.add_directive("tantivy=warn".parse()?);
}

if let Some(path) = &config.logging.path {
setup_file_logging(path, env_filter, config.logging.rotation.clone())
} else {
setup_stderr_logging(env_filter)
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method should probably be a member function on Logging. You can either have an impl Logging block in this file or merge it with the parent module.

Comment on lines +83 to +85
fn ensure_log_dir_exists(dir: PathBuf) -> std::io::Result<()> {
std::fs::create_dir_all(dir)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: This can probably be deleted.

Comment on lines +33 to +64
match ensure_log_dir_exists(log_path.clone()) {
Ok(..) => {}
Err(_err) => {
eprintln!("Could not build log path - falling back to stderr");
return setup_stderr_logging(env_filter);
}
}

let (non_blocking_writer, guard) = match RollingFileAppender::builder()
.rotation(log_rotation.into())
.filename_prefix("apollo_mcp_server")
.filename_suffix("log")
.build(log_path)
{
Ok(appender) => tracing_appender::non_blocking(appender),
Err(_error) => {
eprintln!("Log file setup failed - falling back to stderr");
return setup_stderr_logging(env_filter);
}
};

tracing_subscriber::registry()
.with(env_filter)
.with(
tracing_subscriber::fmt::layer()
.with_writer(non_blocking_writer)
.with_ansi(false)
.with_target(false),
)
.init();

Ok(Some(guard))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: This is mostly to show that this could be simplified slightly by functional chaining, but feel free to ignore.

Suggested change
match ensure_log_dir_exists(log_path.clone()) {
Ok(..) => {}
Err(_err) => {
eprintln!("Could not build log path - falling back to stderr");
return setup_stderr_logging(env_filter);
}
}
let (non_blocking_writer, guard) = match RollingFileAppender::builder()
.rotation(log_rotation.into())
.filename_prefix("apollo_mcp_server")
.filename_suffix("log")
.build(log_path)
{
Ok(appender) => tracing_appender::non_blocking(appender),
Err(_error) => {
eprintln!("Log file setup failed - falling back to stderr");
return setup_stderr_logging(env_filter);
}
};
tracing_subscriber::registry()
.with(env_filter)
.with(
tracing_subscriber::fmt::layer()
.with_writer(non_blocking_writer)
.with_ansi(false)
.with_target(false),
)
.init();
Ok(Some(guard))
let (writer, guard) = std::fs::create_dir_all(log_path)
.and_then(RollingFileAppender::builder()
.rotation(log_rotation.into())
.filename_prefix("apollo_mcp_server")
.filename_suffix("log")
.build())
.and_then(|(non_blocking, guard)| (non_blocking, Some(guard)))
.unwrap_or_else(|| {
eprintln!("Log file setup failed - falling back to stderr");
(std::io::stderr, None)
})
tracing_subscriber::registry()
.with(env_filter)
.with(
tracing_subscriber::fmt::layer()
.with_writer(writer)
.with_ansi(true)
.with_target(false),
)
.init();
Ok(guard)

}

/// Sets up stderr logging
fn setup_stderr_logging(env_filter: EnvFilter) -> Result<Option<WorkerGuard>, anyhow::Error> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: this is probably not needed.

Comment on lines +30 to +41
#[rstest]
#[case(LogRotationKind::Minutely, Rotation::MINUTELY)]
#[case(LogRotationKind::Hourly, Rotation::HOURLY)]
#[case(LogRotationKind::Daily, Rotation::DAILY)]
#[case(LogRotationKind::Never, Rotation::NEVER)]
fn it_maps_to_rotation_correctly(
#[case] log_rotation_kind: LogRotationKind,
#[case] expected: Rotation,
) {
let actual: Rotation = log_rotation_kind.into();
assert_eq!(expected, actual);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: This test seems weird to me since the From impl is just a straight mapping, but feel free to leave it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added it mainly for coverage and to break in case someone changes something with the mapping. The test will then at least alert you that you broke some expected mapping between the two.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants