How can we dynamically modify the LogLevel of a Go program at runtime?
A few days ago, we accidentally discovered that our WebPLB logs were flooded by a barrage of requests from our own control.
The name of our program that actually handles the traffic is WebPLB. Our API Service is named WebPPT. Can you guess what our other services are called? (Hint: They all follow the format
WebP[A-Z][A-Z]
.)
Having too many logs can cause a lot of trouble for online debugging. In addition to changing the corresponding unimportant logs to Debug, we are also considering how to change the logLevel at runtime. This way, when needed, we can change the log level, and the application can see the required logs without restarting.
Our logs use logrus, so you can easily change the log level with the following code:
log.SetLevel(log.DebugLevel)
But the question is, how do we call this code at runtime? The most naive idea is environment variables, but if we use environment variables, we need to consider the following issues:
- The application needs to run a task regularly to read environment variables and update the log level, so there will be a certain delay.
- The effectiveness of environment variables. The environment variables set in the current shell are not visible to already running processes.
Suddenly, I thought of a very important concept in UNIX - signals. Signals are a method of interprocess communication. When a process receives a signal, it can choose to handle and execute it. This happens to be our requirement, and using signals has the following advantages:
- No need for periodic tasks, no delay.
- Convenient to adjust at any time, no need to restart.
- Native support, maybe even for Windows.
By using kill -L
, we can see which signals are available, summarized as follows:
Signal Number | Signal Name | Default Behavior |
---|---|---|
1 | SIGHUP | Terminate process; can be handled, ignored, or caught |
2 | SIGINT | Interrupt process (like Ctrl+C); can be caught or ignored |
3 | SIGQUIT | Quit process and generate core dump |
4 | SIGILL | Illegal Instruction; generally leads to a core dump |
5 | SIGTRAP | Trap for debugging |
6 | SIGABRT | Abort signal from abort() system call |
7 | SIGBUS | Bus error (bad memory access) |
8 | SIGFPE | Floating-point exception |
9 | SIGKILL | Kill signal; cannot be caught, blocked, or ignored |
10 | SIGUSR1 | User-defined signal 1 |
11 | SIGSEGV | Invalid memory reference (segmentation fault) |
12 | SIGUSR2 | User-defined signal 2 |
13 | SIGPIPE | Write to a pipe with no reader; can be caught or ignored |
14 | SIGALRM | Timer signal from alarm() system call |
15 | SIGTERM | Termination signal; can be caught or ignored |
16 | SIGSTKFLT | Stack fault (not common on most Unix systems) |
17 | SIGCHLD | Child process terminated, stopped (or resumed) |
18 | SIGCONT | Continue executing, if stopped; cannot be blocked |
19 | SIGSTOP | Stop process; cannot be caught, blocked, or ignored |
20 | SIGTSTP | Terminal stop signal (like Ctrl+Z) |
21 | SIGTTIN | Background process attempting read |
22 | SIGTTOU | Background process attempting write |
23 | SIGURG | Urgent condition on socket (like out-of-band data) |
24 | SIGXCPU | CPU time limit exceeded (see setrlimit()) |
25 | SIGXFSZ | File size limit exceeded (see setrlimit()) |
26 | SIGVTALRM | Virtual alarm clock (from setitimer()) |
27 | SIGPROF | Profiling timer expired |
28 | SIGWINCH | Window resize signal |
29 | SIGIO | I/O now possible (from fcntl()) |
30 | SIGPWR | Power failure |
31 | SIGSYS | Bad system call |
34-64 | SIGRTMIN to SIGRTMAX | Real-time signals; behavior is application-specific |
We can use the custom signals SIGUSR1 and SIGUSR2. We just need to run them in the main through a goroutine. Here’s an example code:
package main
import (
"os"
"os/signal"
"syscall"
"time"
"github.com/sirupsen/logrus"
)
func main() {
log := logrus.New()
log.SetLevel(logrus.InfoLevel)
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGUSR1, syscall.SIGUSR2)
go func() {
for {
sig := <-sigChan
switch sig {
case syscall.SIGUSR1:
log.SetLevel(logrus.DebugLevel)
log.Warnln("Log level changed to DEBUG")
case syscall.SIGUSR2:
log.SetLevel(logrus.InfoLevel)
log.Warnln("Log level changed to INFO")
}
}
}()
// rest of your application
for {
log.Debugln("debug log")
log.Infoln("info log")
time.Sleep(time.Second * 2)
}
}
In another window, you can send signals through htop or kill -s SIGUSR1 <pid>
. As shown in the image, you can see that the program’s log level is changed at runtime.
The WebP Cloud Services team is a small team of three individuals from Shanghai and Helsingborg. Since we are not funded and have no profit pressure, we remain committed to doing what we believe is right. We strive to do our best within the scope of our resources and capabilities. We also engage in various activities without affecting the services we provide to the public, and we continuously explore novel ideas in our products.
If you find this service interesting, feel free to log in to the WebP Cloud Dashboard to experience it. If you’re curious about other magical features it offers, take a look at our WebP Cloud Services Docs. We hope everyone enjoys using it!
Discuss on Hacker News