github.com/alibaba/ilogtail/pkg@v0.0.0-20250526110833-c53b480d046c/helper/containercenter/container_center_file_discover.go (about)

     1  // Copyright 2021 iLogtail Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package containercenter
    16  
    17  import (
    18  	"context"
    19  	"encoding/json"
    20  	"fmt"
    21  	"io/fs"
    22  	"os"
    23  	"path/filepath"
    24  	"sort"
    25  	"strconv"
    26  	"strings"
    27  	"sync"
    28  	"time"
    29  
    30  	"github.com/alibaba/ilogtail/pkg/helper"
    31  	"github.com/alibaba/ilogtail/pkg/logger"
    32  	"github.com/alibaba/ilogtail/pkg/util"
    33  
    34  	"github.com/docker/docker/api/types"
    35  	"github.com/docker/docker/api/types/container"
    36  )
    37  
    38  const staticContainerInfoPathEnvKey = "ALIYUN_LOG_STATIC_CONTAINER_INFO"
    39  
    40  // const staticContainerType = "ALIYUN_LOG_STATIC_CONTAINER_TYPE"
    41  // const staticContainerDScanInterval = "ALIYUN_LOG_STATIC_CONTAINED_SCAN_INTERVAL"
    42  // const staticContainerTypeContainerD = "containerd"
    43  
    44  var containerdScanIntervalMs = 1000
    45  var staticDockerContainers []types.ContainerJSON
    46  var staticDockerContainerError error
    47  var staticDockerContainerLock sync.Mutex
    48  var loadStaticContainerOnce sync.Once
    49  var staticDockerContainerLastStat helper.StateOS
    50  var staticDockerContainerFile string
    51  var staticDockerContainerLastBody string
    52  
    53  type staticContainerMount struct {
    54  	Source      string
    55  	Destination string
    56  	Driver      string
    57  }
    58  
    59  type staticContainerState struct {
    60  	Status string
    61  	Pid    int
    62  }
    63  
    64  type staticContainerInfo struct {
    65  	ID       string
    66  	Name     string
    67  	Created  string
    68  	HostName string
    69  	IP       string
    70  	Image    string
    71  	LogPath  string
    72  	Labels   map[string]string
    73  	LogType  string
    74  	UpperDir string
    75  	Env      map[string]string
    76  	Mounts   []staticContainerMount
    77  	State    staticContainerState
    78  }
    79  
    80  func staticContainerInfoToStandard(staticInfo *staticContainerInfo, stat fs.FileInfo) types.ContainerJSON {
    81  	created, err := time.Parse(time.RFC3339Nano, staticInfo.Created)
    82  	if err != nil {
    83  		created = stat.ModTime()
    84  	}
    85  
    86  	allEnv := make([]string, 0, len(staticInfo.Env))
    87  	for key, val := range staticInfo.Env {
    88  		allEnv = append(allEnv, key+"="+val)
    89  	}
    90  
    91  	if staticInfo.HostName == "" {
    92  		staticInfo.HostName = util.GetHostName()
    93  	}
    94  
    95  	if staticInfo.IP == "" {
    96  		staticInfo.IP = util.GetIPAddress()
    97  	}
    98  
    99  	status := staticInfo.State.Status
   100  	if status != ContainerStatusExited {
   101  		if !ContainerProcessAlive(staticInfo.State.Pid) {
   102  			status = ContainerStatusExited
   103  		}
   104  	}
   105  
   106  	dockerContainer := types.ContainerJSON{
   107  		ContainerJSONBase: &types.ContainerJSONBase{
   108  			ID:      staticInfo.ID,
   109  			Name:    staticInfo.Name,
   110  			Created: created.Format(time.RFC3339Nano),
   111  			LogPath: staticInfo.LogPath,
   112  			HostConfig: &container.HostConfig{
   113  				LogConfig: container.LogConfig{
   114  					Type: staticInfo.LogType,
   115  				},
   116  			},
   117  			GraphDriver: types.GraphDriverData{
   118  				Name: "overlay",
   119  				Data: map[string]string{
   120  					"UpperDir": staticInfo.UpperDir,
   121  				},
   122  			},
   123  			State: &types.ContainerState{
   124  				Status: status,
   125  				Pid:    staticInfo.State.Pid,
   126  			},
   127  		},
   128  		Config: &container.Config{
   129  			Labels:   staticInfo.Labels,
   130  			Image:    staticInfo.Image,
   131  			Env:      allEnv,
   132  			Hostname: staticInfo.HostName,
   133  		},
   134  		NetworkSettings: &types.NetworkSettings{
   135  			DefaultNetworkSettings: types.DefaultNetworkSettings{
   136  				IPAddress: staticInfo.IP,
   137  			},
   138  		},
   139  	}
   140  
   141  	for _, mount := range staticInfo.Mounts {
   142  		dockerContainer.Mounts = append(dockerContainer.Mounts, types.MountPoint{
   143  			Source:      filepath.Clean(mount.Source),
   144  			Destination: filepath.Clean(mount.Destination),
   145  			Driver:      mount.Driver,
   146  		})
   147  	}
   148  	sortMounts := func(mounts []types.MountPoint) {
   149  		sort.Slice(mounts, func(i, j int) bool {
   150  			return mounts[i].Source < mounts[j].Source
   151  		})
   152  	}
   153  	sortMounts(dockerContainer.Mounts)
   154  	return dockerContainer
   155  }
   156  
   157  func overWriteSymLink(oldname, newname string) error {
   158  	if _, err := os.Lstat(newname); err == nil {
   159  		if err = os.Remove(newname); err != nil {
   160  			return fmt.Errorf("failed to unlink: %+v", err)
   161  		}
   162  	} else if !os.IsNotExist(err) {
   163  		return fmt.Errorf("failed to check symlink: %+v", err)
   164  	}
   165  	return os.Symlink(oldname, newname)
   166  }
   167  
   168  func scanContainerdFilesAndReLink(filePath string) {
   169  	const logSuffix = ".log"
   170  	dirPath := filepath.Dir(filePath)
   171  
   172  	maxFileNo := 0
   173  	if err := overWriteSymLink(filepath.Join(dirPath, strconv.Itoa(maxFileNo)+logSuffix), filePath); err != nil {
   174  		logger.Error(context.Background(), "overwrite symbol link error, from", maxFileNo, "to", filePath, "error", err)
   175  	} else {
   176  		logger.Info(context.Background(), "overwrite symbol link success, from", maxFileNo, "to", filePath)
   177  	}
   178  	for {
   179  		time.Sleep(time.Millisecond * time.Duration(containerdScanIntervalMs))
   180  		dir, err := os.ReadDir(dirPath)
   181  		if err != nil {
   182  			continue
   183  		}
   184  
   185  		for _, fi := range dir {
   186  			if fi.IsDir() {
   187  				continue
   188  			}
   189  
   190  			fileName := fi.Name()
   191  			if ok := strings.HasSuffix(fi.Name(), logSuffix); !ok {
   192  				continue
   193  			}
   194  			baseName := fileName[0 : len(fileName)-len(logSuffix)]
   195  			fileNo, err := strconv.Atoi(baseName)
   196  			// not x.log
   197  			if err != nil {
   198  				continue
   199  			}
   200  			if fileNo <= maxFileNo {
   201  				continue
   202  			}
   203  			if err := overWriteSymLink(filepath.Join(dirPath, strconv.Itoa(fileNo)+logSuffix), filePath); err == nil {
   204  				logger.Info(context.Background(), "overwrite symbol link success, from", fileNo, "to", filePath, "max", maxFileNo)
   205  				maxFileNo = fileNo
   206  			} else {
   207  				logger.Error(context.Background(), "overwrite symbol link error, from", fileNo, "to", filePath, "error", err, "max", maxFileNo)
   208  			}
   209  		}
   210  	}
   211  }
   212  
   213  func innerReadStatisContainerInfo(file string, lastContainerInfo []types.ContainerJSON, stat fs.FileInfo) (containers []types.ContainerJSON, removed []string, changed bool, err error) {
   214  	body, err := os.ReadFile(filepath.Clean(file))
   215  	if err != nil {
   216  		return nil, nil, false, err
   217  	}
   218  	var staticContainerInfos []staticContainerInfo
   219  	err = json.Unmarshal(body, &staticContainerInfos)
   220  	if err != nil {
   221  		return nil, nil, false, err
   222  	}
   223  
   224  	if staticDockerContainerLastBody == "" {
   225  		changed = true
   226  	} else if staticDockerContainerLastBody != string(body) {
   227  		changed = true
   228  	}
   229  	staticDockerContainerLastBody = string(body)
   230  
   231  	nowIDs := make(map[string]struct{})
   232  	for i := 0; i < len(staticContainerInfos); i++ {
   233  		containers = append(containers, staticContainerInfoToStandard(&staticContainerInfos[i], stat))
   234  		nowIDs[staticContainerInfos[i].ID] = struct{}{}
   235  	}
   236  
   237  	for _, lastContainer := range lastContainerInfo {
   238  		if _, ok := nowIDs[lastContainer.ID]; !ok {
   239  			removed = append(removed, lastContainer.ID)
   240  		}
   241  	}
   242  	logger.Infof(context.Background(), "read static container info: %#v", staticContainerInfos)
   243  	return containers, removed, changed, err
   244  }
   245  
   246  func isStaticContainerInfoEnabled() bool {
   247  	envPath := os.Getenv(staticContainerInfoPathEnvKey)
   248  	return len(envPath) != 0
   249  }
   250  
   251  func tryReadStaticContainerInfo() ([]types.ContainerJSON, []string, bool, error) {
   252  	statusChanged := false
   253  	loadStaticContainerOnce.Do(
   254  		func() {
   255  			envPath := os.Getenv(staticContainerInfoPathEnvKey)
   256  			if len(envPath) == 0 {
   257  				return
   258  			}
   259  			staticDockerContainerFile = envPath
   260  			stat, err := os.Stat(staticDockerContainerFile)
   261  			staticDockerContainers, _, statusChanged, staticDockerContainerError = innerReadStatisContainerInfo(staticDockerContainerFile, nil, stat)
   262  			if err == nil {
   263  				staticDockerContainerLastStat = helper.GetOSState(stat)
   264  			}
   265  			// not used yet
   266  			// if containerType := os.Getenv(staticContainerType); containerType == staticContainerTypeContainerD {
   267  			// 	if containerDScanInterval := os.Getenv(staticContainerDScanInterval); containerDScanInterval != "" {
   268  			// 		interval, err := strconv.Atoi(containerDScanInterval)
   269  			// 		if err != nil && interval > 0 && interval < 24*3600 {
   270  			// 			containerdScanIntervalMs = interval
   271  			// 		}
   272  			// 	}
   273  			// 	for i := 0; i < len(staticContainerInfos); i++ {
   274  			// 		go scanContainerdFilesAndReLink(GetMountedFilePath(staticContainerInfos[i].LogPath))
   275  			// 	}
   276  			// }
   277  		},
   278  	)
   279  
   280  	if staticDockerContainerFile == "" {
   281  		return nil, nil, false, nil
   282  	}
   283  
   284  	for _, container := range staticDockerContainers {
   285  		if container.State.Status == ContainerStatusRunning && !ContainerProcessAlive(container.State.Pid) {
   286  			container.State.Status = ContainerStatusExited
   287  			statusChanged = true
   288  		}
   289  	}
   290  
   291  	stat, err := os.Stat(staticDockerContainerFile)
   292  	if err != nil {
   293  		return staticDockerContainers, nil, statusChanged, err
   294  	}
   295  
   296  	osStat := helper.GetOSState(stat)
   297  	if !osStat.IsChange(staticDockerContainerLastStat) && staticDockerContainerError == nil {
   298  		return staticDockerContainers, nil, statusChanged, nil
   299  	}
   300  
   301  	newStaticDockerContainer, removedIDs, changed, staticDockerContainerError := innerReadStatisContainerInfo(staticDockerContainerFile, staticDockerContainers, stat)
   302  	changed = changed || statusChanged
   303  	if staticDockerContainerError == nil {
   304  		staticDockerContainers = newStaticDockerContainer
   305  	}
   306  	staticDockerContainerLastStat = osStat
   307  
   308  	logger.Info(context.Background(), "static docker container info file changed or last read error, last", staticDockerContainerLastStat,
   309  		"now", osStat, "removed ids", removedIDs, "error", staticDockerContainerError)
   310  	return staticDockerContainers, removedIDs, changed, staticDockerContainerError
   311  }