Copyright 2023 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package util
import (
"context"
"fmt"
"html"
"net"
"net/http"
"os"
"path/filepath"
"strconv"
"sync"
"time"
"golang.org/x/sys/unix"
"k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/klog/v2"
)
const (
DefaultSocketDir = "/tmp/klog-socks"
SocketSuffix = "-klog.sock"
SocketDirEnvName = "DEBUG_SOCKET_DIR"
setLogLevelPath = "/setlevel"
getLogLevelPath = "/getlevel"
exampleSocketCli = "\"Failed to change klog log level, because got wrong value from level argument\\n\"+\n\t\t\t\t\"example: curl --unix-socket /tmp/klog-socks/componentName-klog.sock \\\"http://localhost/setlevel?level=8&duration=60s\\\"\\n\"+\n\t\t\t\t\"level=8 means changing klog log level to 8\\n\"+\n\t\t\t\t\"duration=60s means maintaining level=8 for 60 seconds[60m -> 60 minutes; 60h -> 60 hours]\""
)
var (
prevCtx context.Context
prevCtxCancelFunc context.CancelFunc
currentLogLevel string
startupLogLevel string
mutex sync.RWMutex
)
func responseOk(w *http.ResponseWriter, okMsg string) {
(*w).Header().Set("Content-Type", "text/plain; charset=utf-8")
(*w).Header().Set("X-Content-Type-Options", "nosniff")
_, err := fmt.Fprint(*w, okMsg)
if err != nil {
klog.Error(err)
return
}
}
func responseError(w *http.ResponseWriter, errMsg string, httpCode int) {
http.Error(*w, errMsg, httpCode)
}
func modifyLoglevel(newLogLevel string) error {
mutex.Lock()
defer mutex.Unlock()
var loglevel klog.Level
if err := loglevel.Set(newLogLevel); err != nil {
return err
}
currentLogLevel = newLogLevel
if prevCtxCancelFunc != nil {
prevCtxCancelFunc()
}
prevCtx, prevCtxCancelFunc = context.WithCancel(context.Background())
return nil
}
func reset(ctx context.Context, duration time.Duration) {
defer runtime.HandleCrash()
select {
case <-time.After(duration):
var loglevel klog.Level
mutex.Lock()
defer mutex.Unlock()
if err := loglevel.Set(startupLogLevel); err != nil {
klog.Error(err)
return
}
currentLogLevel = startupLogLevel
klog.InfoS("Klog recover to start-up log level successfully", "startupLogLevel", startupLogLevel)
case <-ctx.Done():
klog.InfoS("Cancel previous timer successfully")
}
}
func installKlogLogLevelHandler(mux *http.ServeMux, startup string) {
currentLogLevel, startupLogLevel = startup, startup
mux.Handle(setLogLevelPath, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
values := r.URL.Query()
rawLevel := values.Get("level")
rawDuration := values.Get("duration")
rawLevel = html.EscapeString(rawLevel)
rawDuration = html.EscapeString(rawDuration)
var duration time.Duration
var err error
if level, err := strconv.ParseInt(rawLevel, 10, 64); err != nil || level <= 0 {
responseError(&w, exampleSocketCli, http.StatusBadRequest)
return
}
if duration, err = time.ParseDuration(rawDuration); err != nil || duration.Milliseconds() <= 0 {
responseError(&w, exampleSocketCli, http.StatusBadRequest)
return
}
if err := modifyLoglevel(rawLevel); err != nil {
responseError(&w, fmt.Sprintf("Failed to change klog log level. Error: %v\n", err.Error()), http.StatusInternalServerError)
return
}
mutex.RLock()
go reset(prevCtx, duration)
responseOk(&w, fmt.Sprintf("Change klog log level to %s successfully and for %v\n", currentLogLevel, duration))
mutex.RUnlock()
}))
mux.Handle(getLogLevelPath, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
mutex.RLock()
responseOk(&w, fmt.Sprintf("Current klog log level: %s\n", currentLogLevel))
mutex.RUnlock()
}))
}
func listenUnix(componentName string, socketDir string) (net.Listener, error) {
if len(socketDir) == 0 {
socketDir = DefaultSocketDir
}
if _, err := os.Stat(socketDir); os.IsNotExist(err) {
if err = os.MkdirAll(socketDir, 0750); err != nil {
return nil, fmt.Errorf("error creating klog log level socket dir: %v", err)
}
}
socketFileFullName := componentName + SocketSuffix
socketFileFullPath := filepath.Join(socketDir, socketFileFullName)
fi, err := os.Stat(socketFileFullPath)
if err == nil && (fi.Mode()&os.ModeSocket) != 0 {
err := os.Remove(socketFileFullPath)
if err != nil {
klog.ErrorS(err, "failed to remote socket file", "file", socketFileFullPath)
return nil, err
}
}
oldmask := unix.Umask(0077)
l, err := net.Listen("unix", socketFileFullPath)
unix.Umask(oldmask)
return l, err
}
func serveOnListener(l net.Listener, m *http.ServeMux) error {
server := http.Server{
Handler: m,
}
return server.Serve(l)
}
func ListenAndServeKlogLogLevel(componentName string, startupLogLevel string, socketDir string) {
var err error
defer runtime.HandleCrash()
mux := http.NewServeMux()
installKlogLogLevelHandler(mux, startupLogLevel)
var listener net.Listener
listener, err = listenUnix(componentName, socketDir)
if err != nil {
return
}
if err = serveOnListener(listener, mux); err != nil {
return
}
}