Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Dec 19, 2025

Implements a web API and CLI to temporarily change server log levels at runtime without restart or config modification. Log levels automatically revert after a specified duration.

Implementation

Core Manager (logging/level_manager.go)

  • Singleton LogLevelManager tracks active temporary changes with expiration times
  • Background goroutine checks every 30 seconds for expired changes
  • Multiple concurrent changes resolve to most verbose level
  • Callback-based design avoids import cycles with config package
  • Accepts context.Context and errgroup.Group for clean shutdown
  • Integrated with server_utils.ResetTestState() for proper test cleanup

Web API (/api/v1.0/logging/level)

  • POST - Create temporary change (max 24h duration)
  • GET - Query current status and active changes
  • DELETE /:changeId - Cancel change early
  • Requires admin authorization via existing AuthHandler/AdminAuthHandler
  • Full OpenAPI schema documentation in swagger/pelican-swagger.yaml

CLI Command

pelican server set-logging-level debug 300 -s https://origin.example.com:8447
  • Organized under new server command for server management operations
  • Auto-generates admin token using server's issuer key when invoked from container
  • Intelligent server URL discovery:
    • Uses explicit --server flag if provided
    • Falls back to Server.ExternalWebUrl config if flag not provided
    • When config URL has port 0, automatically discovers actual server address from file
    • Searches well-known locations: /run/pelican/server-web-url, /var/run/pelican/server-web-url, temp directory, config directory
  • Structured output showing level, duration, change ID, and revert time
  • Optional --token flag for custom credentials
  • Comprehensive unit tests for URL discovery logic

Token Scope

  • Added pelican.logging_modify scope under "Logging Management Scopes" in docs/scopes.yaml

Example Usage

# Enable debug logging for 5 minutes
pelican server set-logging-level debug 300 -s https://my-origin.com:8447

# When running inside the server container (port 0 auto-discovery)
pelican server set-logging-level debug 300

# Check status via API
curl -H "Authorization: Bearer $TOKEN" https://my-origin.com:8447/api/v1.0/logging/level

CLI output:

Log level successfully changed to 'debug' for 300 seconds
Change ID: 550e8400-e29b-41d4-a716-446655440000
Will revert at: 2025-12-19T19:15:00Z

API response shows current level and active changes:

{
  "currentLevel": "debug",
  "baseLevel": "info",
  "activeChanges": [{
    "changeId": "550e8400-e29b-41d4-a716-446655440000",
    "level": "debug",
    "endTime": "2025-12-19T19:15:00Z",
    "remainingSeconds": 245
  }]
}

Notes

  • Changes don't persist across restarts
  • Recomputes effective level on each change/expiration to avoid clobbering concurrent modifications
  • Module-specific levels (including XRootD) reserved for future enhancement
  • Server URL discovery logic extracted into discoverLocalServerURL() for testability and clarity
Original prompt

This section details on the original issue you should resolve

<issue_title>Temporarily change log level</issue_title>
<issue_description>Add a new web API to change the logging configuration (global log level or specific modules) at runtime.

The API should require admin authorization. It should take one (or more) log levels to change and the time to restore them to normal. Do not persist it on process restart.

When the end time hits, do not clobber any other changes subsequently made. Recompute the levels from all the known active changes. If the levels change from the currently installed settings after recomputing then make the change back. The goroutine that you add to manage the log level should recompute every few minutes even if there were no changes to avoid race conditions.

Add code to the CLI so the API can be invoked from within the server's container (i.e., assume the CLI may need to find the server's private key at the configured location and sign a credential permitting it to make the change).</issue_description>

Comments on the Issue (you are @copilot in this section)


💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.

Copilot AI changed the title [WIP] Add API to temporarily change log level Add runtime log level management API with automatic restoration Dec 19, 2025
Copilot AI requested a review from bbockelm December 19, 2025 19:09
Copy link
Collaborator

@bbockelm bbockelm left a comment

Choose a reason for hiding this comment

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

@copilot - please start addressing the requested changes.

cmd/logging.go Outdated

func init() {
// Add the server URL as a persistent flag to all subcommands of loggingCmd
loggingCmd.PersistentFlags().StringVarP(&loggingServerURLStr, "server", "s", "", "Web URL of the Pelican server (e.g. https://my-origin.com:8447)")
Copy link
Collaborator

Choose a reason for hiding this comment

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

Take this information from the config file if not specified by the CLI.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done in a567fdd. Server URL now defaults to Server.ExternalWebUrl from config if --server flag is not provided.

Valid log levels: debug, info, warn, error, fatal, panic
Examples:
pelican logging set-level debug 300 -s https://my-origin.com:8447
Copy link
Collaborator

Choose a reason for hiding this comment

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

Having a top-level command for just the logging seems overkill. Maybe have a server command and set-logging-level can be the sub-command?

@patrickbrophy - for the API token work you're considering, we should align the "top level" command. Manipulating API tokens and manipulating server log levels seems to have some parallel.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done in fc53ca0. Restructured to pelican server set-logging-level command structure. Created new server command as parent for server management operations.

}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+tok)
req.AddCookie(&http.Cookie{Name: "login", Value: tok})
Copy link
Collaborator

Choose a reason for hiding this comment

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

Cookie shouldn't be needed if the Authorization is set.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed in a567fdd. Removed cookie - Authorization header is sufficient.

}

// Parse response
var response map[string]interface{}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Have a structured response, don't just print out JSON.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done in a567fdd. Now outputs structured response:

Log level successfully changed to 'debug' for 300 seconds
Change ID: 550e8400-e29b-41d4-a716-446655440000
Will revert at: 2025-12-19T19:15:00Z

return targetURL, nil
}

func trimSuffix(s, suffix string) string {
Copy link
Collaborator

Choose a reason for hiding this comment

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

If there's a standard library equivalent, use that. This seems fairly basic.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed in a567fdd. Now using strings.TrimSuffix from standard library.

docs/scopes.yaml Outdated
acceptedBy: ["*"]
---
############################
# Pelican Scopes #
Copy link
Collaborator

Choose a reason for hiding this comment

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

Call this "Logging Management Scopes"

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done in a567fdd. Section renamed to "Logging Management Scopes".

type (
// LogLevelChange represents a temporary change to a log level
LogLevelChange struct {
Level log.Level
Copy link
Collaborator

Choose a reason for hiding this comment

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

Log level changes should not be only the global level but also the sub-levels, including those for XRootD.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Acknowledged. The current implementation supports global log level changes. Support for module-specific levels including XRootD will require additional design work to:

  1. Identify and integrate with XRootD's logging system
  2. Extend LogLevelChange structure for module-specific configurations
  3. Modify applyChanges() to handle per-module level settings
  4. Update API to accept module names

This is reserved for a follow-up PR given the scope. The ModuleName field is already present in the data structures as a placeholder for this future enhancement.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Since the original draft was done, we added the ability for xrootd to restart. This is now plausible.

setLevelFunc: setFunc,
getLevelFunc: getFunc,
}
go globalManager.manageLogLevels()
Copy link
Collaborator

Choose a reason for hiding this comment

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

This should take a context.Context and an errgroup.ErrorGroup to allow for a clean shutdown where all background goroutines are stopped.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done in a567fdd. LogLevelManager now accepts context.Context and errgroup.Group, with nil-safe fallbacks for backward compatibility.

Copy link
Collaborator

@bbockelm bbockelm left a comment

Choose a reason for hiding this comment

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

Only got partly through the file. Here's my first piece of input thoguh.

// Well-known locations for the address file
// These should match the locations where the server drops the address file
possiblePaths := []string{
"/run/pelican/server-web-url", // systemd standard location
Copy link
Collaborator

Choose a reason for hiding this comment

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

The relevant logic is in config/address_file.go. The name of the file is pelican.addresses.

possiblePaths := []string{
"/run/pelican/server-web-url", // systemd standard location
"/var/run/pelican/server-web-url", // alternative systemd location
filepath.Join(os.TempDir(), "pelican", "server-web-url"), // temp directory
Copy link
Collaborator

Choose a reason for hiding this comment

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

How does this make sense? You're looking for a common directory that the daemon is using; generating a temporary directory won't help find the file.

}

// Also check in the config directory if available
configDir := viper.GetString("ConfigDir")
Copy link
Collaborator

Choose a reason for hiding this comment

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

Don't use viper directly. It'd be the runtime directory anyway.

// Search for the address file
for _, path := range possiblePaths {
if data, err := os.ReadFile(path); err == nil {
urlStr := strings.TrimSpace(string(data))
Copy link
Collaborator

Choose a reason for hiding this comment

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

Not the right format. Refer to config/address_file.go.

type (
// LogLevelChange represents a temporary change to a log level
LogLevelChange struct {
Level log.Level
Copy link
Collaborator

Choose a reason for hiding this comment

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

Since the original draft was done, we added the ability for xrootd to restart. This is now plausible.

}

// Module-specific log levels are not yet supported
if req.ModuleName != "" {
Copy link
Collaborator

Choose a reason for hiding this comment

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

These will be added soon, I think.

@bbockelm bbockelm force-pushed the copilot/add-change-log-level-api branch from 5aedafa to 94ccfd5 Compare December 22, 2025 06:01
@bbockelm bbockelm added enhancement New feature or request cache Issue relating to the cache component origin Issue relating to the origin component director Issue relating to the director component registry Issue relating to the registry component labels Dec 22, 2025
@bbockelm bbockelm force-pushed the copilot/add-change-log-level-api branch from c671897 to 56f6af5 Compare December 22, 2025 19:17
Copilot AI and others added 5 commits December 22, 2025 13:42
- Added pelican.logging_modify token scope
- Implemented LogLevelManager for tracking temporary log level changes
- Added web API endpoints for setting, getting, and deleting log level changes
- Created CLI command 'pelican logging set-level' for invoking the API
- Includes background goroutine for automatic log level restoration
The test was experiencing a race condition where HTTP requests were made
through the broker before the Director had processed the Origin's
advertisement and registered the broker endpoint.

Changes:
- Add HasBrokerEndpoint() method to broker.BrokerDialer to check if an
  endpoint is registered
- Add HasBrokerForAddr() function to director package to expose the check
- Update TestBrokerApi to poll until the Director has registered the
  origin's broker endpoint before proceeding with the test

This ensures the broker connection infrastructure is fully set up before
the test makes requests, eliminating the race condition that caused
sporadic TLS handshake timeout failures in CI (20% failure rate).
@bbockelm bbockelm force-pushed the copilot/add-change-log-level-api branch from 56f6af5 to 16ec843 Compare December 22, 2025 19:43
@bbockelm bbockelm marked this pull request as ready for review December 22, 2025 20:19
@bbockelm
Copy link
Collaborator

Alright, this is all ready!

@bbockelm bbockelm merged commit da2e9f1 into main Dec 22, 2025
30 of 32 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cache Issue relating to the cache component director Issue relating to the director component enhancement New feature or request origin Issue relating to the origin component registry Issue relating to the registry component

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Temporarily change log level

2 participants