diff --git a/.gitignore b/.gitignore index c288e0a6cb..d0a8940254 100644 --- a/.gitignore +++ b/.gitignore @@ -109,4 +109,3 @@ internal/plugins/externals/ebpf/demo/ internal/plugins/externals/ebpf/internal/testuitls/mysqlins/mysqlins internal/export/doc/zh/inputs/imgs/tracing.png /git -/build diff --git a/Dockerfile_dev b/Dockerfile_dev new file mode 100644 index 0000000000..39c8478a90 --- /dev/null +++ b/Dockerfile_dev @@ -0,0 +1,7 @@ +FROM ubuntu:20.04 + +ARG TARGETARCH + +RUN export DEBIAN_FRONTEND=noninteractive \ + && sed -i 's/\(archive\|security\|ports\).ubuntu.com/mirrors.aliyun.com/' /etc/apt/sources.list \ + && apt-get update && apt-get install -y make curl tree tzdata gcc diff --git a/Dockerfile_dev_musl b/Dockerfile_dev_musl new file mode 100644 index 0000000000..b84b4d5ce5 --- /dev/null +++ b/Dockerfile_dev_musl @@ -0,0 +1,4 @@ +FROM alpine:3.14 + +RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories \ + && apk add make build-base alpine-sdk diff --git a/cmd/installer/installer/dkconf.go b/cmd/installer/installer/dkconf.go index 3198b99475..017f0e11df 100644 --- a/cmd/installer/installer/dkconf.go +++ b/cmd/installer/installer/dkconf.go @@ -90,6 +90,8 @@ var ( HostName, IPDBType string + InstrumentationEnabled string + ConfdBackend, ConfdBasicAuth, ConfdClientCaKeys, diff --git a/cmd/installer/main.go b/cmd/installer/main.go index b98183bedb..946eac7a10 100644 --- a/cmd/installer/main.go +++ b/cmd/installer/main.go @@ -30,9 +30,11 @@ import ( "gitlab.jiagouyun.com/cloudcare-tools/datakit/cmd/installer/installer" "gitlab.jiagouyun.com/cloudcare-tools/datakit/cmd/upgrader/upgrader" + apminj "gitlab.jiagouyun.com/cloudcare-tools/datakit/internal/apminject/utils" "gitlab.jiagouyun.com/cloudcare-tools/datakit/internal/cmds" cp "gitlab.jiagouyun.com/cloudcare-tools/datakit/internal/colorprint" "gitlab.jiagouyun.com/cloudcare-tools/datakit/internal/config" + "gitlab.jiagouyun.com/cloudcare-tools/datakit/internal/datakit" dl "gitlab.jiagouyun.com/cloudcare-tools/datakit/internal/downloader" "gitlab.jiagouyun.com/cloudcare-tools/datakit/internal/git" @@ -70,6 +72,12 @@ var ( runtime.GOARCH, DataKitVersion)) + datakitAPMInjectURL = "https://" + path.Join(DataKitBaseURL, + fmt.Sprintf("datakit-apm-inject-%s-%s-%s.tar.gz", + runtime.GOOS, + runtime.GOARCH, + DataKitVersion)) + InstallerBaseURL = "" l = logger.DefaultSLogger("installer") @@ -119,6 +127,10 @@ func init() { flag.StringVar(&flagLite, "lite", "", "install datakit lite") flag.StringVar(&flagELinker, "elinker", "", "install datakit eBPF span linker") + flag.StringVar(&installer.InstrumentationEnabled, "apm-instrumentation-enabled", "", "enable APM instrumentation") + // flag.StringVar(&flagAPMInstrumentationLibraries, "apm-instrumentation-libraries", "datadog|java,python", + // "install and use the APM library of the specified provider") + flag.StringVar(&installer.Dataway, "dataway", "", "DataWay host(https://guance.openway.com?token=xxx)") flag.StringVar(&installer.Proxy, "proxy", "", "http proxy http://ip:port for datakit") flag.StringVar(&installer.DatakitName, "name", "", "specify DataKit name, example: prod-env-datakit") @@ -260,7 +272,6 @@ func downloadFiles(to string) error { if err := dl.Download(cli, dkURL, to, true, flagDownloadOnly); err != nil { return err } - fmt.Printf("\n") dl.CurDownloading = "data" @@ -272,16 +283,29 @@ func downloadFiles(to string) error { // We will not upgrade dk-upgrader default when upgrading Datakit except for setting flagUpgradeManagerService flag if !flagDKUpgrade || (flagDKUpgrade && flagUpgraderEnabled == 1) || flagDownloadOnly { + toUpgrader := to if !flagDownloadOnly { - to = upgrader.InstallDir + toUpgrader = upgrader.InstallDir } dl.CurDownloading = upgrader.BuildBinName - cp.Infof("Downloading %s => %s\n", dkUpgraderURL, to) - if err := dl.Download(cli, dkUpgraderURL, to, true, flagDownloadOnly); err != nil { + cp.Infof("Downloading %s => %s\n", dkUpgraderURL, toUpgrader) + if err := dl.Download(cli, dkUpgraderURL, toUpgrader, true, flagDownloadOnly); err != nil { l.Warnf("unable to download %s from [%s]: %s", upgrader.BuildBinName, dkUpgraderURL, err) } } + if installer.InstrumentationEnabled != "" && runtime.GOOS == "linux" && + (runtime.GOARCH == "amd64" || runtime.GOARCH == "arm64") { + if err := apminj.Download(l, apminj.WithOnline(cli, datakitAPMInjectURL), + apminj.WithInstallDir(to), + apminj.WithInstrumentationEnabled(installer.InstrumentationEnabled), + ); err != nil { + l.Warnf("download apm inject failed: %s", err.Error()) + } else { + config.Cfg.APMInject.InstrumentationEnabled = installer.InstrumentationEnabled + } + } + if installer.IPDBType != "" { fmt.Printf("\n") baseURL := "https://" + DataKitBaseURL diff --git a/cmd/make/build/build.go b/cmd/make/build/build.go index 734c0c7df5..c9fa142583 100644 --- a/cmd/make/build/build.go +++ b/cmd/make/build/build.go @@ -234,6 +234,10 @@ func Compile() error { return err } + if err := compileAPMInject(goos, goarch, BuildDir); err != nil { + return err + } + upgraderDir := fmt.Sprintf("%s/%s-%s-%s", BuildDir, upgrader.BuildBinName, goos, goarch) l.Debugf("upgraderDir = %s", upgraderDir) if err := compileArch(upgrader.BuildBinName, @@ -340,6 +344,42 @@ func compileArch(bin, goos, goarch, dir, mainEntranceFile, tags string) error { return nil } +func compileAPMInject(goos, goarch, dir string) error { + if goos != "linux" { + l.Warnf("skip building apm auto-inject launcher: unsupported os %s", goos) + return nil + } + + if goarch != "amd64" && goarch != "arm64" { + l.Warnf("skip building apm auto-inject launcher: unsupported arch %s", goarch) + return nil + } + + _, err := exec.LookPath("docker") + if err != nil { + l.Warnf("skip building apm auto-inject launcher: %s", + err.Error()) + return nil + } + + cmdArgs := []string{ + "sh", "internal/apminject/build_lib.sh", + goarch, fmt.Sprintf("%s/datakit-apm-inject-linux-%s", dir, goarch), + } + + l.Debugf("building %v with %v", fmt.Sprintf("%s-%s/%s", + goos, goarch, "apm-auto-inject-launcher"), cmdArgs) + + var envs []string + msg, err := runEnv(cmdArgs, envs) + if err != nil { + return fmt.Errorf("failed to run %v, envs: %v: %w, msg: %s", + cmdArgs, envs, err, string(msg)) + } + + return nil +} + // is_extra_lite check whether to build lite datakit. func isExtraLite() bool { extraLite := true diff --git a/cmd/make/build/pub.go b/cmd/make/build/pub.go index ef3e98ed6f..ec26f456ac 100644 --- a/cmd/make/build/pub.go +++ b/cmd/make/build/pub.go @@ -215,6 +215,13 @@ func PubDatakit() error { } } + // apm-auto-inject-launcher + if goos == Linux && (goarch == AMD64 || goarch == ARM64) { + gzName, gzPath := tarFiles( + PubDir, BuildDir, "datakit-apm-inject", goos, goarch, TarWithRlsVer) + basics[gzName] = gzPath + } + upgraderGZFile, upgraderGZPath := tarFiles(PubDir, BuildDir, upgrader.BuildBinName, parts[0], parts[1], TarNoRlsVer) installerExe := fmt.Sprintf("installer-%s-%s", goos, goarch) diff --git a/install.template.sh b/install.template.sh index a1909273fa..37abff5751 100755 --- a/install.template.sh +++ b/install.template.sh @@ -156,6 +156,12 @@ if [ -n "$DK_ELINKER" ]; then printf "* Set elinker => $DK_ELINKER\n" fi +apmInstrumentation= +if [ -n "$DK_APM_INSTRUMENTATION_ENABLED" ]; then + apmInstrumentation=$DK_APM_INSTRUMENTATION_ENABLED + printf "* Set apm-instrumentation-enabled => $DK_APM_INSTRUMENTATION_ENABLED\n" +fi + global_customer_keys= if [ -n "$DK_SINKER_GLOBAL_CUSTOMER_KEYS" ]; then global_customer_keys=$DK_SINKER_GLOBAL_CUSTOMER_KEYS @@ -550,6 +556,7 @@ if [ "$upgrade" ]; then --install-log="$install_log" \ --upgrade --lite="${lite}" \ --elinker="${elinker}" \ + --apm-instrumentation-enabled="${apmInstrumentation}" \ --upgrade-manager="${upgrade_manager}" \ --upgrade-ip-whitelist="${upgrade_ip_whitelist}" \ --upgrade-listen="${upgrade_listen}" \ @@ -575,6 +582,7 @@ $sudo_cmd $installer \ --proxy="${proxy}" \ --lite="${lite}" \ --elinker="${elinker}" \ + --apm-instrumentation-enabled="${apmInstrumentation}" \ --env_hostname="${env_hostname}" \ --dca-enable="${dca_enable}" \ --dca-listen="${dca_listen}" \ diff --git a/internal/apminject/Makefile b/internal/apminject/Makefile new file mode 100644 index 0000000000..78c9d3a391 --- /dev/null +++ b/internal/apminject/Makefile @@ -0,0 +1,52 @@ +.PHONY: all debug rewriter test + +ARCH ?= $(shell uname -m | sed -e s/x86_64/x86_64/ \ + -e s/aarch64.\*/aarch64/) + +ifeq ($(ARCH), x86_64) + ARCH = amd64 +endif +ifeq ($(ARCH), aarch64) + ARCH = arm64 +endif + +REPO_PATH ?= ${HOME}/go/src/gitlab.jiagouyun.com/cloudcare-tools/datakit +SOURCE_PATH := $(REPO_PATH)/internal/apminject +DIST_DIR ?= $(REPO_PATH)/internal/apminject/dist/datakit-apm-inject-linux-$(ARCH) + +DATAKIT_INJ_REWRITE_PROC ?= \"/usr/local/datakit/apm_inject/inject/rewriter\" + +all: rewriter launcher + +rewriter: + CGO_ENABLED=0 GOOS=linux GOARCH=$(ARCH) go build -o $(DIST_DIR)/rewriter \ + $(SOURCE_PATH)/rewriter/rewriter.go + +launcher: + mkdir -p $(DIST_DIR) + gcc -DDATAKIT_INJ_REWRITE_PROC=${DATAKIT_INJ_REWRITE_PROC} \ + $(SOURCE_PATH)/apm_launcher.c -fPIC -shared \ + -o $(DIST_DIR)/apm_launcher.so + +launcher_musl: + mkdir -p $(DIST_DIR) + gcc -DDATAKIT_INJ_REWRITE_PROC=${DATAKIT_INJ_REWRITE_PROC} \ + $(SOURCE_PATH)/apm_launcher.c -fPIC -shared \ + -o $(DIST_DIR)/apm_launcher_musl.so + +launcher_debug: + mkdir -p ${DIST_DIR} + gcc -DDATAKIT_DEBUG -DDATAKIT_INJ_REWRITE_PROC=${DATAKIT_INJ_REWRITE_PROC} \ + $(SOURCE_PATH)/apm_launcher.c -fPIC -shared \ + -o $(DIST_DIR)/apm_launcher_debug.so + +launcher_musl_debug: + mkdir -p ${DIST_DIR} + gcc -DDATAKIT_DEBUG -DDATAKIT_INJ_REWRITE_PROC=${DATAKIT_INJ_REWRITE_PROC} \ + $(SOURCE_PATH)/apm_launcher.c -fPIC -shared \ + -o $(DIST_DIR)/apm_launcher_musl_debug.so + +test: + mkdir -p ${DIST_DIR} + gcc $(SOURCE_PATH)/launcher_test.c -o $(DIST_DIR)/launcher_test \ + && $(DIST_DIR)/launcher_test && rm $(DIST_DIR)/launcher_test diff --git a/internal/apminject/apm_launcher.c b/internal/apminject/apm_launcher.c new file mode 100644 index 0000000000..09b7933b85 --- /dev/null +++ b/internal/apminject/apm_launcher.c @@ -0,0 +1,354 @@ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef DATAKIT_DEBUG +#define debug_perror(msg) perror(msg) +#define debug_log(msg, ...) fprintf(stderr, "%s:%d: %s: " msg, \ + __FILE__, __LINE__, __func__, ##__VA_ARGS__) +#else +#define debug_perror(msg) +#define debug_log(msg, ...) +#endif + +#ifndef DATAKIT_INJ_RAND_BYTES +#define DATAKIT_INJ_RAND_BYTES 16 +#endif + +#ifndef DATAKIT_INJ_FILENAME_PREFIX +#define DATAKIT_INJ_FILENAME_PREFIX "/tmp/dk_inject_rewrite_" +#endif + +#ifndef DATAKIT_INJ_REWRITE_PROC +#define DATAKIT_INJ_REWRITE_PROC "/usr/local/datakit/apm_inject/inject/rewriter" +#endif + +#ifdef __GLIBC__ +#define SYM_VER_GLIBC(sym, ver) __asm__(".symver " #sym "," #sym "@GLIBC_" #ver) +#if defined(__x86_64__) +#define SYM_VER_GLIBC2_2_5(sym) SYM_VER_GLIBC(sym, 2.2.5) +SYM_VER_GLIBC2_2_5(atoi); +SYM_VER_GLIBC2_2_5(strlen); +SYM_VER_GLIBC2_2_5(strdup); +SYM_VER_GLIBC2_2_5(strcat); +SYM_VER_GLIBC2_2_5(getdelim); +#undef SYM_VER_GLIBC2_2_5 +#elif defined(__aarch64__) +#define SYM_VER_GLIBC2_17(sym) SYM_VER_GLIBC(sym, 2.17) +SYM_VER_GLIBC2_17(atoi); +SYM_VER_GLIBC2_17(strlen); +SYM_VER_GLIBC2_17(strdup); +SYM_VER_GLIBC2_17(strcat); +SYM_VER_GLIBC2_17(getdelim); +#undef SYM_VER_GLIBC2_17 +#endif +#undef SYM_VER_GLIBC +#endif + +static const char launcher_version[] = "1.0"; + +static inline int old_execve(char const *path, char *const *argv, char *const *envp) +{ + return syscall(SYS_execve, path, argv, envp); +} + +static ssize_t read_rewrite_record(char **line, size_t *n, FILE *fp) +{ + return getdelim(line, n, 0x1E, fp); +} + +static char *const *read_args(FILE *fp) +{ + char **arr = NULL; + int total_lines = 0; + + // total_linesarg_1...arg_n + for (int ln = 0;; ln++) + { + size_t len = 0; + char *line = NULL; + // 0x1E, record separator + ssize_t nread = read_rewrite_record(&line, &len, fp); + if (nread == -1) + { + if (line != NULL) + { + free(line); + } + goto err_check; + } + if (nread == 2) + { + // 0x1D, group separator + if (line[0] == 0x1D && line[1] == 0x1E) + { + free(line); + break; + } + } + + if (ln == 0) + { + line[nread - 1] = '\0'; + total_lines = atoi(line); + free(line); + + size_t nsize = (total_lines + 1) * sizeof(char *); + if (arr = malloc(nsize), arr == NULL) + { + debug_perror("malloc rewrite record"); + return NULL; + } + __builtin_memset(arr, 0, nsize); + continue; + } + + if (ln > total_lines) + { + free(line); + return NULL; // error, need check content format + } + line[nread - 1] = '\0'; + arr[ln - 1] = line; + } + + return arr; + +err_check: + if (feof(fp)) + { + return arr; + } + else + { + debug_perror("read rewrite record"); + return NULL; + } +} + +static void multi_args_free(char *const *ptr[], int len) +{ + if (ptr == NULL) + { + return; + } + for (int i = 0; i < len; i++) + { + if (ptr[i] == NULL) + { + continue; + } + for (int j = 0; ptr[i][j] != NULL; j++) + { + free(ptr[i][j]); + } + free((void *)ptr[i]); + } + return; +} + +static bool gen_inject_tmpid_b16(char *tmpid) +{ + uint8_t buf[DATAKIT_INJ_RAND_BYTES] = {0}; + int fd = open("/dev/urandom", O_RDONLY); + if (fd < 0) + { + return false; + } + read(fd, buf, DATAKIT_INJ_RAND_BYTES); + close(fd); + + for (int i = 0; i < DATAKIT_INJ_RAND_BYTES; i++) + { + uint8_t c = buf[i]; + tmpid[(i * 2)] = (c & 0xf); + tmpid[(i * 2) + 1] = (c >> 4); + } + for (int i = 0; i < DATAKIT_INJ_RAND_BYTES * 2; i++) + { + if (tmpid[i] > 9) + { + tmpid[i] += ('A' - 10); + } + else + { + tmpid[i] += '0'; + } + } + + return true; +} + +static bool try_apm_inject_process(char *const argv[], char *const envp[]) +{ + pid_t c1_pid = fork(); + + switch (c1_pid) + { + case -1: + debug_perror("fork error"); + return false; + case 0: // child process + ; + // a label can only be part of a statement + // and a declaration is not a statement + pid_t g1_rewrite_pid = fork(); + switch (g1_rewrite_pid) + { + case -1: + debug_perror("fork rewrite process"); + _exit(1); + case 0: // grandson process 1 + ; + int ret = old_execve(DATAKIT_INJ_REWRITE_PROC, argv, envp); + if (ret != 0) + { + debug_perror("run rewrite process `" DATAKIT_INJ_REWRITE_PROC "`"); + } + _exit(0); + default: + break; + } + + pid_t g2_timer_pid = fork(); + switch (g2_timer_pid) + { + case -1: + debug_perror("fork watchdog timer"); + kill(g1_rewrite_pid, SIGKILL); + _exit(1); + case 0: // grandson process 2 + // software watchdog timer + sleep(1); + _exit(0); + default: + break; + } + + pid_t g_x = wait(0); // skip check `errno` + int stat = 0; + if (g_x == g2_timer_pid) + { + // timeout + debug_log("rewrite process timeout\n"); + kill(g1_rewrite_pid, SIGKILL); + stat = 1; + } + else + { + kill(g2_timer_pid, SIGKILL); + } + + _exit(stat); + default: + break; + } + + // parent process + int stat = 0; + waitpid(c1_pid, &stat, 0); + + return WEXITSTATUS(stat) == 0 ? true : false; +} + +static int varb_array_len(char *const arr[]) +{ + if (arr == NULL) + { + return 0; + } + int len = 0; + for (int i = 0; arr[i] != NULL; i++) + { + len++; + } + return len; +} + +int execve(const char *path, char *const argv[], char *const envp[]) +{ + char tmpid[DATAKIT_INJ_RAND_BYTES * 2 + 1] = {0}; + // retrieve modified launch parameters from file + + if (!gen_inject_tmpid_b16(tmpid)) + { + return old_execve(path, argv, envp); + } + + int count_argv = varb_array_len(argv); + char **dup_argv = malloc((count_argv + 2 + 1) * sizeof(char const *)); + dup_argv[0] = strdup(tmpid); + dup_argv[1] = strdup(path); + dup_argv[count_argv + 2] = NULL; + + for (int i = 0; i < count_argv; i++) + { + dup_argv[i + 2] = strdup(argv[i]); + } + + // MT-Unsafe + bool ok = try_apm_inject_process(dup_argv, envp); + + for (int i = 0; i < count_argv + 2; i++) + { + free((void *)dup_argv[i]); + } + free(dup_argv); + + if (!ok) + { + return old_execve(path, argv, envp); + } + + char rewrite_args_fname[sizeof(DATAKIT_INJ_FILENAME_PREFIX) + sizeof(tmpid)] = {0}; + strcat(rewrite_args_fname, DATAKIT_INJ_FILENAME_PREFIX); + strcat(rewrite_args_fname, tmpid); + + FILE *fp = fopen(rewrite_args_fname, "r"); + if (fp == NULL) + { + debug_perror(rewrite_args_fname); + return old_execve(path, argv, envp); + } + + char *const *multi_args[3] = {0}; + for (int i = 0; i < 3; i++) + { + char *const *val = read_args(fp); + if ((val == NULL) || + (i == 0 && val[0] == NULL)) + { + multi_args_free(multi_args, i); + fclose(fp); +#ifdef DATAKIT_DEBUG +#else + remove(rewrite_args_fname); +#endif + return old_execve(path, argv, envp); + } + multi_args[i] = val; + } + fclose(fp); +#ifdef DATAKIT_DEBUG +#else + remove(rewrite_args_fname); +#endif + + debug_log("rewrite args: %s %s %s\n", multi_args[0][0], + multi_args[1][0], multi_args[2][0]); + + int ret = old_execve(multi_args[0][0], multi_args[1], multi_args[2]); + multi_args_free(multi_args, 3); + return ret; +} diff --git a/internal/apminject/build_lib.sh b/internal/apminject/build_lib.sh new file mode 100644 index 0000000000..4fd4d5de93 --- /dev/null +++ b/internal/apminject/build_lib.sh @@ -0,0 +1,43 @@ +#!/bin/sh + +# set -x +docker run --privileged --rm pubrepo.jiagouyun.com/ebpf-dev/binfmt:qemu-v7.0.0 \ + --install all + +repo_name="gitlab.jiagouyun.com/cloudcare-tools/datakit" +container_repo_dir=/root/go/src/$repo_name + +target_arch=$1 +if [ -z "$target_arch" ]; then + target_arch=$(uname -m | sed -e s/x86_64/amd64/ \ + -e s/aarch64.\*/arm64/) +fi + +dist_rela_dir=dist/datakit-apm-inject-linux-$target_arch +if [ -n "$2" ]; then + dist_rela_dir=$2 +fi + +docker_build() { + arch=$1 + libc=$2 + image=pubrepo.jiagouyun.com/ebpf-dev/apm-inject-dev:1.1 + target=launcher + + if [ "$libc" = "musl" ]; then + image=pubrepo.jiagouyun.com/ebpf-dev/apm-inject-dev-musl:1.1 + target=launcher_musl + fi + + docker run --rm --platform "$arch" \ + -v "$(go env GOPATH)"/src/$repo_name:$container_repo_dir \ + -w$container_repo_dir/internal/apminject \ + $image make $target DIST_DIR="$container_repo_dir"/"$dist_rela_dir" || exit $? +} + +make -f "$(go env GOPATH)"/src/gitlab.jiagouyun.com/cloudcare-tools/datakit/internal/apminject/Makefile \ + rewriter DIST_DIR="$(go env GOPATH)"/src/gitlab.jiagouyun.com/cloudcare-tools/datakit/"$dist_rela_dir" \ + ARCH="$target_arch" + +docker_build "$target_arch" glibc +docker_build "$target_arch" musl diff --git a/internal/apminject/launcher_test.c b/internal/apminject/launcher_test.c new file mode 100644 index 0000000000..522f972b5e --- /dev/null +++ b/internal/apminject/launcher_test.c @@ -0,0 +1,42 @@ +#include + +#include "apm_launcher.c" + +void test_gen_inject_tmpid_b16(char *tmpid) +{ + assert(gen_inject_tmpid_b16(tmpid)); + assert(strlen(tmpid) == DATAKIT_INJ_RAND_BYTES * 2); +} + +void test_malloc(void) +{ + void *arr = NULL; + if (arr = malloc(16), arr == NULL) + { + assert(false); + } + assert(arr!= NULL); +} + +int main(void) +{ + test_malloc(); + + char tmpid[DATAKIT_INJ_RAND_BYTES * 2 + 1] = {0}; + test_gen_inject_tmpid_b16(tmpid); + + char *const args[] = { + tmpid, + "/bin/ldd", + "ldd", + "--verson", + NULL, + }; + char *const *envp[] = { + NULL, + }; + // try_apm_inject_process(args, envp); + + printf("PASS\n"); + return 0; +} diff --git a/internal/apminject/rewriter/rewriter.go b/internal/apminject/rewriter/rewriter.go new file mode 100644 index 0000000000..629659ff9d --- /dev/null +++ b/internal/apminject/rewriter/rewriter.go @@ -0,0 +1,327 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the MIT License. +// This product includes software developed at Guance Cloud (https://www.guance.com/). +// Copyright 2021-present Guance, Inc. + +package main + +import ( + "bytes" + "errors" + "fmt" + "net" + "os" + "os/exec" + "path/filepath" + "regexp" + "strconv" + "strings" + + "github.com/BurntSushi/toml" +) + +const ( + recSep = "\x1E" + groupSep = "\x1D" + + filePrefix = "/tmp/dk_inject_rewrite_" + + datakitConfPath = "/usr/local/datakit/conf.d/datakit.conf" + + ddtraceRun = "ddtrace-run" + ddtraceJarPath = "/usr/local/datakit/apm_inject/lib/java/dd-java-agent.jar" + + defaultDKHost = "127.0.0.1" + defaultDKPort = "9529" +) + +var ( + pyRegexp = regexp.MustCompile(`^python(?:3(?:[\.\d]+)*)*$`) + javaRegexp = regexp.MustCompile(`^java$`) +) + +var ( + errPyLibNotFound = errors.New("ddtrace-run not found") + errParseJavaVersion = errors.New(("failed to parse java version")) + errJavaLibNotFound = errors.New("dd-java-agent.jar not found") + errUnsupportedJava = errors.New(("unsupported java version")) + errAlreadyInjected = errors.New(("already injected")) +) + +func main() { + argv := os.Args // include tmpid, path and args + envs := os.Environ() + if len(argv) < 3 { + return // error + } + + tmpid := argv[0] + ret, err := rewrite(&reArgs{ + path: argv[1], + argv: argv[2:], + envp: envs, + }) + if err != nil { + // fmt.Fprintf(os.Stderr, "error: %s\n", err.Error()) + return + } + // fmt.Fprintf(os.Stderr, "new cmd: %s %v, %s\n", ret.path, ret.argv, ret.envp[0]) + content := marshal(ret.path, ret.argv, ret.envp) + + //nolint:gosec + if err := os.WriteFile(filePrefix+tmpid, []byte(content), 0o644); err != nil { + _ = err + return + } +} + +type reArgs struct { + path string + argv []string + envp []string +} + +func pyScriptWhitelist(path string) bool { + _, exeName := filepath.Split(path) + return strings.EqualFold(exeName, "flask") || strings.Contains(exeName, "gunicorn") +} + +func rewrite(param *reArgs) (*reArgs, error) { + exePath := filepath.Clean(param.path) + _, exeName := filepath.Split(exePath) + + var pyScript bool + if pyScriptWhitelist(exePath) { + if s, err := pythonScriptMagic(exePath); err == nil { + pyScript = true + // python only + exePath = s + } + } + switch { + case pyScript, pyRegexp.MatchString(exeName): // skip flask + ddrun, err := checkPython(exePath, param.argv) + if err != nil { + return nil, err + } + + ret := &reArgs{ + path: ddrun, + } + + host, port := dkAddr() + ret.argv = append(ret.argv, ddtraceRun) + ret.argv = append(ret.argv, param.argv...) + ret.envp = append(ret.envp, + fmt.Sprintf("DD_AGENT_HOST=%s", host), + fmt.Sprintf("DD_AGENT_PORT=%s", port)) + ret.envp = append(ret.envp, param.envp...) + return ret, nil + case javaRegexp.MatchString(exeName): + //nolint:gosec + cmd := exec.Command(exePath, "-version") + o, err := cmd.CombinedOutput() + if err != nil { + // fmt.Fprintf(os.Stderr, "%s \n", err.Error()) + return nil, err + } + // fmt.Fprintf(os.Stderr, "java\n") + + ver, err := getJavaVersion(string(o)) + if err != nil { + return nil, err + } + + if ver < 8 { + return nil, errUnsupportedJava + } + + for i := 1; i < len(param.argv); i++ { + p := strings.TrimSpace(param.argv[i]) + p = strings.ToLower(p) + if strings.HasPrefix(p, "-javaagent:") { + return nil, errAlreadyInjected + } + } + + finf, err := os.Stat(ddtraceJarPath) + if err != nil { + return nil, fmt.Errorf("stat dd-java-agent.jar: %w", err) + } + + if finf.IsDir() || (finf.Mode().Perm()&0b100) != 0b100 { + return nil, errJavaLibNotFound + } + + ret := &reArgs{ + path: param.path, + } + + host, port := dkAddr() + ret.argv = append(ret.argv, param.argv[0]) + ret.argv = append(ret.argv, + fmt.Sprintf("-javaagent:%s", ddtraceJarPath), + fmt.Sprintf("-Ddd.agent.host=%s", host), + fmt.Sprintf("-Ddd.trace.agent.port=%s", port)) + ret.argv = append(ret.argv, param.argv[1:]...) + ret.envp = param.envp + + return ret, nil + } + + return nil, fmt.Errorf("skip rewrite exec %s", exePath) +} + +func marshal(path string, args, envp []string) string { + var s string + s += "1" + recSep + s += path + recSep + s += groupSep + recSep + + s += strconv.Itoa(len(args)) + recSep + for _, v := range args { + s += v + recSep + } + s += groupSep + recSep + + s += strconv.Itoa(len(envp)) + recSep + for _, v := range envp { + s += v + recSep + } + s += groupSep + recSep + + return s +} + +func dkAddr() (string, string) { + confPath := datakitConfPath + + v := map[string]any{} + if _, err := toml.DecodeFile(confPath, &v); err != nil { + return defaultDKHost, defaultDKPort + } + + if httpAPI, ok := v["http_api"]; ok { + if v, ok := httpAPI.(map[string]any); ok { + if l, ok := v["listen"]; ok { + if l, ok := l.(string); ok { + if h, p, e := net.SplitHostPort(l); e == nil { + return h, p + } + } + } + } + } + return defaultDKHost, defaultDKPort +} + +func getJavaVersion(s string) (int, error) { + lines := strings.Split(s, "\n") + if len(lines) < 2 { + return 0, errParseJavaVersion + } + + idx := strings.Index(lines[0], "\"") + if idx == -1 { + return 0, errParseJavaVersion + } + idxTail := strings.LastIndex(lines[0], "\"") + if idx == -1 { + return 0, errParseJavaVersion + } + + versionStr := lines[0][idx+1 : idxTail-1] + li := strings.Split(versionStr, ".") + if len(li) < 2 { + return 0, errParseJavaVersion + } + + v, err := strconv.Atoi(li[0]) + if err != nil { + return 0, err + } + + if v == 1 { + v, err = strconv.Atoi(li[1]) + if err != nil { + return 0, err + } + return v, nil + } else { + return v, nil + } +} + +var regexpPythonMagic = regexp.MustCompile("^#!(/.*/python(?:3(?:[\\.\\d]+)*)?)\n$") + +func pythonScriptMagic(fp string) (string, error) { + fp, err := exec.LookPath(fp) + if fp == "" || err != nil { + return "", fmt.Errorf("not found %s", fp) + } + + if fp == "ddtrace-run" { + return "", fmt.Errorf("cannot inject ddtrace-run") + } + if strings.HasSuffix(fp, "/ddtrace-run") { + return "", fmt.Errorf("cannot inject ddtrace-run") + } + + f, err := os.Open(fp) // nolint:gosec + if err != nil { + return "", err + } + buf := make([]byte, 512) + n, err := f.Read(buf) + if err != nil { + return "", err + } + if n <= 0 { + return "", fmt.Errorf("read python magic fail") + } + buf = buf[:n] + + n = bytes.Index(buf, []byte("\n")) + if n == -1 { + return "", fmt.Errorf("read python magic fail") + } + + buf = buf[:n+1] + val := regexpPythonMagic.FindStringSubmatch(string(buf)) + if len(val) != 2 { + return "", fmt.Errorf("read python magic fail") + } + + return val[1], nil +} + +func checkPython(pyBinPath string, argv []string) (string, error) { + //nolint:gosec + // require ddtrace, python3 + cmd := exec.Command(pyBinPath, + "-c", "import ddtrace; print(ddtrace.__version__)") + + if _, err := cmd.Output(); err != nil { + return "", fmt.Errorf("get ddtrace-run version error: %w", err) + } + + ddrun, err := exec.LookPath(ddtraceRun) + if err != nil { + return "", errPyLibNotFound + } + finf, err := os.Stat(ddrun) + if err != nil { + return "", errPyLibNotFound + } + + if finf.IsDir() || (finf.Mode().Perm()&0b1) != 0b1 { + return "", fmt.Errorf("ddtrace-run is not executable") + } + + for _, v := range argv { + if strings.ToLower(strings.TrimSpace(v)) == "-m" { + return "", fmt.Errorf("python arg -m is not supported") + } + } + return ddrun, nil +} diff --git a/internal/apminject/rewriter/rewriter_test.go b/internal/apminject/rewriter/rewriter_test.go new file mode 100644 index 0000000000..412272cfe6 --- /dev/null +++ b/internal/apminject/rewriter/rewriter_test.go @@ -0,0 +1,49 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the MIT License. +// This product includes software developed at Guance Cloud (https://www.guance.com/). +// Copyright 2021-present Guance, Inc. + +package main + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestDKAddr(t *testing.T) { + dkAddr() +} + +var ( + javaVer8 = `java version "1.8.12_xx" 2024-07-16 +OpenJDK Runtime Environment (build 17.0.12+7-Ubuntu-1ubuntu222.04) +OpenJDK 64-Bit Server VM (build 17.0.12+7-Ubuntu-1ubuntu222.04, mixed mode, sharing)` + + javaVer17 = `openjdk version "17.0.12" 2024-07-16 +OpenJDK Runtime Environment (build 17.0.12+7-Ubuntu-1ubuntu222.04) +OpenJDK 64-Bit Server VM (build 17.0.12+7-Ubuntu-1ubuntu222.04, mixed mode, sharing)` +) + +func TestRegexp(t *testing.T) { + assert.True(t, pyRegexp.MatchString("python3.7")) + assert.True(t, pyRegexp.MatchString("python3")) + assert.True(t, pyRegexp.MatchString("python")) + + assert.False(t, pyRegexp.MatchString("python2")) + + assert.True(t, javaRegexp.MatchString("java")) + assert.False(t, javaRegexp.MatchString("java-")) + + ver, err := getJavaVersion(javaVer8) + assert.NoError(t, err) + assert.Equal(t, 8, ver) + + ver, err = getJavaVersion(javaVer17) + assert.NoError(t, err) + assert.Equal(t, 17, ver) + + assert.True(t, pyScriptWhitelist("/home/xx/flask")) + assert.True(t, pyScriptWhitelist("/home/xx/gunicorn")) + assert.False(t, pyScriptWhitelist("/home/xx/abcdefg")) +} diff --git a/internal/apminject/utils/install.go b/internal/apminject/utils/install.go new file mode 100644 index 0000000000..bd79c28ba6 --- /dev/null +++ b/internal/apminject/utils/install.go @@ -0,0 +1,358 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the MIT License. +// This product includes software developed at Guance Cloud (https://www.guance.com/). +// Copyright 2021-present Guance, Inc. + +//go:build (linux && amd64) || (linux && arm64) +// +build linux,amd64 linux,arm64 + +package utils + +import ( + "crypto/tls" + "debug/elf" + "fmt" + "os" + "os/exec" + "path/filepath" + "regexp" + "strconv" + "strings" + + "github.com/GuanceCloud/cliutils/logger" + cp "gitlab.jiagouyun.com/cloudcare-tools/datakit/internal/colorprint" + dl "gitlab.jiagouyun.com/cloudcare-tools/datakit/internal/downloader" + "gitlab.jiagouyun.com/cloudcare-tools/datakit/internal/httpcli" +) + +const ( + preloadConfigFilePath = "/etc/ld.so.preload" + + injectInstallDir = "/usr/local/datakit" + injectDir = "apm_inject" + subDirLib = "lib" + subDirInject = "inject" + + launcherName = "apm_launcher" + + jarLibURL = "https://static.guance.com/dd-image/dd-java-agent.jar" + + glibc = "glibc" + muslc = "musl" +) + +var ( + py3Regexp = regexp.MustCompile(`^Python 3.(\d+)`) + reGLBC = regexp.MustCompile(`ldd \(.*\) ([0-9\.]+)`) + reMusl = regexp.MustCompile("musl libc \\(.*\\)\nVersion ([0-9\\.]+)") + soGLibcVerRegexp = regexp.MustCompile(`^GLIBC_([0-9\.]+)$`) +) + +func Download(log *logger.Logger, opt ...Opt) error { + var c config + for _, fn := range opt { + fn(&c) + } + + if !(c.enableHostInject || c.enableDockerInject) { + return nil + } + + if c.installDir == "" { + c.installDir = injectInstallDir + } + + if c.launcherURL == "" { + return fmt.Errorf("apm inject url is empty") + } + + if c.cli == nil { + c.cli = httpcli.Cli(&httpcli.Options{ + // ignore SSL error + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, //nolint + }) + } + fmt.Printf("\n") + + dl.CurDownloading = "apm-inject" + injTo := filepath.Join(c.installDir, injectDir, subDirInject) + cp.Infof("Downloading %s => %s\n", c.launcherURL, injTo) + if err := dl.Download(c.cli, c.launcherURL, injTo, + true, false); err != nil { + return err + } + + fmt.Printf("\n") + + dl.CurDownloading = "apm-lib-java" + injTo = filepath.Join(c.installDir, injectDir, subDirLib, + "java", "dd-java-agent.jar") + cp.Infof("Downloading %s => %s\n", jarLibURL, injTo) + if err := dl.Download(c.cli, jarLibURL, injTo, + true, true); err != nil { + log.Warn(err) + } + + fmt.Printf("\n") + + cp.Infof("Installing ddtrace python library\n") + py, err := exec.LookPath("python3") + if err != nil { + py, err = exec.LookPath("python") + if err != nil { + log.Warn("python not found") + } + } + if py != "" { + //nolint:gosec + ver, err := exec.Command(py, "-V").CombinedOutput() + if err != nil { + log.Warnf("%s -V error: %s", py, err.Error()) + goto skip_python_lib + } + v := py3Regexp.FindStringSubmatch(string(ver)) + var py3Ver int + if len(v) == 2 { + py3Ver, _ = strconv.Atoi(v[1]) + } else { + log.Warnf("parse python version error: %s", string(ver)) + goto skip_python_lib + } + if py3Ver >= 7 { + // set env: PIP_INDEX_URL=https://pypi.tuna.tsinghua.edu.cn/simple + //nolint:gosec + if s, err := exec.Command(py, "-m", + "pip", "install", "ddtrace").CombinedOutput(); err != nil { + log.Warn(string(s)) + log.Warnf("pip install ddtrace error: %s", err.Error()) + } else { + log.Info(string(s)) + } + } + } +skip_python_lib: + + return nil +} + +func Install(opt ...Opt) error { + var c config + for _, fn := range opt { + fn(&c) + } + + if c.installDir == "" { + c.installDir = injectInstallDir + } + + if !c.enableHostInject && !c.enableDockerInject { + unsetPreload(c.installDir) + return nil + } + + // TODO: check docker inject + + if c.enableHostInject { + libc, hostVersion, err := lddInfo() + if err != nil { + unsetPreload(c.installDir) + return err + } + launcherSoPath := laucnherSoPath(libc, c.installDir) + elfFile, err := elf.Open(launcherSoPath) + if err != nil { + unsetPreload(c.installDir) + return err + } + dynSyms, err := elfFile.DynamicSymbols() + if err != nil { + unsetPreload(c.installDir) + return err + } + required, err := requiredGLIBCVersion(dynSyms) + if err != nil && libc == glibc { + unsetPreload(c.installDir) + return err + } + if hostVersion.LessThan(required) { + unsetPreload(c.installDir) + return fmt.Errorf("host libc version %s is less than required %s", + hostVersion, required) + } + if err := setPreload(c.installDir, launcherSoPath); err != nil { + unsetPreload(c.installDir) + return err + } + } else { + unsetPreload(c.installDir) + } + + return nil +} + +func Uninstall(opt ...Opt) error { + var c config + for _, fn := range opt { + fn(&c) + } + if c.installDir == "" { + c.installDir = injectInstallDir + } + + unsetPreload(c.installDir) + return nil +} + +func readPreloadWithoutLanucher(installDir string) (libs []byte) { + f, err := os.ReadFile(preloadConfigFilePath) + if err != nil { + fmt.Fprintf(os.Stderr, "failed to read %s: %s", + preloadConfigFilePath, err.Error()) + return + } + + prefix := filepath.Join(installDir, injectDir, subDirInject, launcherName) + for _, ln := range strings.Split(string(f), "\n") { + if ln == "" { + continue + } + if !strings.HasPrefix(ln, prefix) { + libs = append(libs, []byte(ln)...) + libs = append(libs, '\n') + } + } + return libs +} + +func unsetPreload(installDir string) { + if _, err := os.Stat(preloadConfigFilePath); err != nil { + return + } + + lines := readPreloadWithoutLanucher(installDir) + //nolint:gosec + if err := os.WriteFile(preloadConfigFilePath, lines, 0o644); err != nil { + fmt.Fprintf(os.Stderr, "failed to clean %s: %s", + preloadConfigFilePath, err.Error()) + return + } +} + +func setPreload(installDir, soPath string) error { + lines := readPreloadWithoutLanucher(installDir) + lines = append(lines, []byte(soPath)...) + lines = append(lines, '\n') + + //nolint:gosec + if err := os.WriteFile(preloadConfigFilePath, lines, 0o644); err != nil { + return err + } + return nil +} + +func laucnherSoPath(kind, installDir string) string { + var soPath string + bp := filepath.Join(installDir, injectDir, subDirInject) + switch kind { + case glibc: + soPath = filepath.Join(bp, launcherName+".so") + case muslc: + soPath = filepath.Join(bp, launcherName+"_musl.so") + } + return soPath +} + +func lddInfo() (string, Version, error) { + fp, err := exec.LookPath("ldd") + if err != nil { + return "", Version{}, err + } + //nolint:gosec + cmd := exec.Command(fp, "--version") + o, err := cmd.CombinedOutput() + if err != nil { + return "", Version{}, err + } + + text := string(o) + if v1, v2, ok := libcInfo(text); !ok { + return "", Version{}, fmt.Errorf("unknown libc") + } else { + var version Version + if err := version.Parse(v2); err != nil { + return "", Version{}, fmt.Errorf("parse version failed: %w", err) + } + return v1, version, nil + } +} + +func libcInfo(text string) (string, string, bool) { + v := reGLBC.FindStringSubmatch(text) + if len(v) != 2 { + v = reMusl.FindStringSubmatch(text) + if len(v) != 2 { + return "", "", false + } + return muslc, v[1], true + } else { + return glibc, v[1], true + } +} + +type Version [3]int + +func (v Version) String() string { + return fmt.Sprintf("%d.%d.%d", v[0], v[1], v[2]) +} + +func (v Version) LessThan(other Version) bool { + for i := 0; i < len(v); i++ { + if v[i] < other[i] { + return true + } + if v[i] > other[i] { + return false + } + } + return false +} + +func (v *Version) Parse(str string) error { + vStr := strings.Split(str, ".") + if len(vStr) > 3 { + return fmt.Errorf("invalid version: %s", + str) + } + + tmpV := Version{} + for i := 0; i < len(vStr); i++ { + val, err := strconv.Atoi(vStr[i]) + if err != nil { + return fmt.Errorf("invalid version: %s", + str) + } + tmpV[i] = val + } + *v = tmpV + return nil +} + +func requiredGLIBCVersion(dynamicSymbols []elf.Symbol) (Version, error) { + var required Version + + for _, sym := range dynamicSymbols { + versionMatch := soGLibcVerRegexp.FindStringSubmatch(sym.Version) + if len(versionMatch) != 2 { + continue + } + versionStr := versionMatch[1] + + var v Version + if err := v.Parse(versionStr); err != nil { + return Version{}, err + } else if required.LessThan(v) { + required = v + } + } + return required, nil +} diff --git a/internal/apminject/utils/install_other.go b/internal/apminject/utils/install_other.go new file mode 100644 index 0000000000..59d582dbd8 --- /dev/null +++ b/internal/apminject/utils/install_other.go @@ -0,0 +1,24 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the MIT License. +// This product includes software developed at Guance Cloud (https://www.guance.com/). +// Copyright 2021-present Guance, Inc. + +//go:build !(linux && amd64) && !(linux && arm64) +// +build !linux !amd64 +// +build !linux !arm64 + +package utils + +import "github.com/GuanceCloud/cliutils/logger" + +func Download(log *logger.Logger, opt ...Opt) error { + return nil +} + +func Install(opt ...Opt) error { + return nil +} + +func Uninstall(opt ...Opt) error { + return nil +} diff --git a/internal/apminject/utils/utils.go b/internal/apminject/utils/utils.go new file mode 100644 index 0000000000..2635ec2deb --- /dev/null +++ b/internal/apminject/utils/utils.go @@ -0,0 +1,83 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the MIT License. +// This product includes software developed at Guance Cloud (https://www.guance.com/). +// Copyright 2021-present Guance, Inc. + +// Package utils contains utils +package utils + +import ( + "net/http" + "strings" +) + +const ( + KindHOST = "host" + KindDocker = "docker" + KindDisable = "disable" +) + +type config struct { + enableHostInject bool + enableDockerInject bool + installDir string + launcherURL string + + cli *http.Client + + offline bool + offlineLauncherPath string + offlineAPMLibPath string + + forceUpgradeAPMLib bool +} + +type Opt func(c *config) + +func WithInstallDir(dir string) Opt { + return func(c *config) { + c.installDir = dir + } +} + +func WithInstrumentationEnabled(s string) Opt { + return func(c *config) { + disable := false + for _, val := range strings.Split(s, ",") { + switch strings.TrimSpace(val) { + case KindDisable: + disable = true + case KindDocker: + c.enableDockerInject = true + case KindHOST: + c.enableHostInject = true + } + } + if disable || s == "" { + c.enableDockerInject = false + c.enableHostInject = false + } + } +} + +func WithOnline(cli *http.Client, launcherURL string) Opt { + return func(c *config) { + c.launcherURL = launcherURL + c.cli = cli + c.offline = false + } +} + +func WithOffline(launcher, apmLib string) Opt { + return func(c *config) { + c.offline = true + c.offlineLauncherPath = launcher + c.offlineAPMLibPath = apmLib + } +} + +func WithForceUpgradeLib(y bool) Opt { + return func(c *config) { + c.forceUpgradeAPMLib = y + } +} diff --git a/internal/apminject/utils/utils_test.go b/internal/apminject/utils/utils_test.go new file mode 100644 index 0000000000..fea15cfae0 --- /dev/null +++ b/internal/apminject/utils/utils_test.go @@ -0,0 +1,70 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the MIT License. +// This product includes software developed at Guance Cloud (https://www.guance.com/). +// Copyright 2021-present Guance, Inc. + +package utils + +import ( + "debug/elf" + "testing" + + "github.com/stretchr/testify/assert" +) + +var ( + musl125 = `musl libc (x86_64) +Version 1.2.5 +Dynamic Program Loader +Usage: /lib/ld-musl-x86_64.so.1 [options] [--] pathname` + + glibc235 = `ldd (Ubuntu GLIBC 2.35-0ubuntu3.8) 2.35 +Copyright (C) 2022 Free Software Foundation, Inc. +This is free software; see the source for copying conditions. There is NO +warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +Written by Roland McGrath and Ulrich Drepper.` + + glic212 = `ldd (GNU libc) 2.12 +Copyright (C) 2010 Free Software Foundation, Inc. +This is free software; see the source for copying conditions. There is NO +warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +Written by Roland McGrath and Ulrich Drepper.` + + glibc236 = `ldd (Debian GLIBC 2.36-9+deb12u8) 2.36 +Copyright (C) 2022 Free Software Foundation, Inc. +This is free software; see the source for copying conditions. There is NO +warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +Written by Roland McGrath and Ulrich Drepper.` +) + +func TestUtils(t *testing.T) { + v1, v2, ok := libcInfo(glibc235) + assert.Equal(t, v1, glibc) + assert.Equal(t, v2, "2.35") + assert.True(t, ok) + + v1, v2, ok = libcInfo(glic212) + assert.Equal(t, v1, glibc) + assert.Equal(t, v2, "2.12") + assert.True(t, ok) + + v1, v2, ok = libcInfo(glibc236) + assert.Equal(t, v1, glibc) + assert.Equal(t, v2, "2.36") + assert.True(t, ok) + + v1, v2, ok = libcInfo(musl125) + assert.Equal(t, v1, muslc) + assert.Equal(t, v2, "1.2.5") + assert.True(t, ok) +} + +func TestLddInfo(t *testing.T) { + syms := []elf.Symbol{ + {Library: "libc.so.6", Version: "GLIBC_2.35"}, + {Library: "ld-linux-x86-64.so.2", Version: "GLIBC_2.35"}, + } + v, err := requiredGLIBCVersion(syms) + assert.NoError(t, err) + assert.Equal(t, Version{2, 35, 0}, v) +} diff --git a/internal/config/apminject.go b/internal/config/apminject.go new file mode 100644 index 0000000000..39ae693457 --- /dev/null +++ b/internal/config/apminject.go @@ -0,0 +1,10 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the MIT License. +// This product includes software developed at Guance Cloud (https://www.guance.com/). +// Copyright 2021-present Guance, Inc. + +package config + +type APMInject struct { + InstrumentationEnabled string `toml:"instrumentation_enabled"` +} diff --git a/internal/config/load.go b/internal/config/load.go index c347712976..5ef538e101 100644 --- a/internal/config/load.go +++ b/internal/config/load.go @@ -17,6 +17,7 @@ import ( plmanager "github.com/GuanceCloud/cliutils/pipeline/manager" "github.com/GuanceCloud/cliutils/point" + "gitlab.jiagouyun.com/cloudcare-tools/datakit/internal/apminject/utils" "gitlab.jiagouyun.com/cloudcare-tools/datakit/internal/datakit" "gitlab.jiagouyun.com/cloudcare-tools/datakit/internal/dkstring" "gitlab.jiagouyun.com/cloudcare-tools/datakit/internal/path" @@ -109,6 +110,16 @@ func LoadCfg(c *Config, mcp string) error { return err } + if c.APMInject != nil { + if err := utils.Install( + utils.WithInstallDir(datakit.InstallDir), + utils.WithInstrumentationEnabled( + c.APMInject.InstrumentationEnabled), + ); err != nil { + l.Warnf("failed to install/uninstall apm inject: %s", err.Error()) + } + } + l.Infof("init %d default plugins...", len(c.DefaultEnabledInputs)) initDefaultEnabledPlugins(c) diff --git a/internal/config/mainconf.go b/internal/config/mainconf.go index 94ceab69db..c8a310d6bb 100644 --- a/internal/config/mainconf.go +++ b/internal/config/mainconf.go @@ -77,6 +77,8 @@ type Config struct { HTTPAPI *APIConfig `toml:"http_api"` + APMInject *APMInject `toml:"apm_inject"` + Recorder *recorder.Recorder `toml:"recorder"` IO *io.IOConf `toml:"io"` IOCacheCountDeprecated int `toml:"io_cache_count,omitzero"` @@ -188,6 +190,8 @@ func DefaultConfig() *Config { WhiteList: []string{}, }, + APMInject: &APMInject{}, + DKUpgrader: &DKUpgraderCfg{ Host: "0.0.0.0", Port: 9542, diff --git a/internal/downloader/dl.go b/internal/downloader/dl.go index a453f4ec1c..4e85dbfc2e 100644 --- a/internal/downloader/dl.go +++ b/internal/downloader/dl.go @@ -90,8 +90,10 @@ func Extract(r io.Reader, to string) error { return fmt.Errorf("MkdirAll: %w", err) } + _ = os.Remove(filepath.Clean(target)) // TODO: lock file before extracting, to avoid `text file busy` error - f, err := os.OpenFile(filepath.Clean(target), os.O_CREATE|os.O_RDWR, os.FileMode(hdr.Mode)) + f, err := os.OpenFile(filepath.Clean(target), + os.O_CREATE|os.O_RDWR|os.O_TRUNC, os.FileMode(hdr.Mode)) if err != nil { return fmt.Errorf("OpenFile: %w", err) } @@ -143,7 +145,17 @@ func Download(cli *http.Client, from, to string, progress, downloadOnly bool) er } func doDownload(r io.Reader, to string) error { - f, err := os.OpenFile(filepath.Clean(to), os.O_CREATE|os.O_RDWR, os.ModePerm) + to = filepath.Clean(to) + + if _, err := os.Stat(filepath.Dir(to)); err != nil { + if err := os.MkdirAll(filepath.Dir(to), os.ModePerm); err != nil { + return fmt.Errorf("MkdirAll: %w", err) + } + } + + _ = os.Remove(filepath.Clean(to)) + f, err := os.OpenFile(filepath.Clean(to), + os.O_CREATE|os.O_RDWR|os.O_TRUNC, os.ModePerm) if err != nil { return err } diff --git a/internal/export/doc/en/datakit-install.md b/internal/export/doc/en/datakit-install.md index 7a26d8df84..e640b54b60 100644 --- a/internal/export/doc/en/datakit-install.md +++ b/internal/export/doc/en/datakit-install.md @@ -94,18 +94,18 @@ You can specify the environment variable `DK_ELINKER` to install DataKit ELinker DataKit ELinker only contains collectors as below: -| Collector Name | Description | -| -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | -| [`cpu`](../integrations/cpu.md) | Collect the CPU usage of the host | -| [`disk`](../integrations/disk.md) | Collect disk occupancy | -| [`diskio`](../integrations/diskio.md) | Collect the disk IO status of the host | -| [`ebpftrace`](../integrations/ebpftrace.md) | Receive eBPF trace span and link these spans to generate trace id | -| [`mem`](../integrations/mem.md) | Collect the memory usage of the host | -| [`swap`](../integrations/swap.md) | Collect Swap memory usage | -| [`system`](../integrations/system.md) | Collect the load of host operating system | -| [`net`](../integrations/net.md) | Collect host network traffic | -| [`hostobject`](../integrations/hostobject.md) | Collect basic information of host computer (such as operating system information, hardware information, etc.) | -| [`DataKit(dk)`](../integrations/dk.md) | Collect Datakit running metrics | +| Collector Name | Description | +| --------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | +| [`cpu`](../integrations/cpu.md) | Collect the CPU usage of the host | +| [`disk`](../integrations/disk.md) | Collect disk occupancy | +| [`diskio`](../integrations/diskio.md) | Collect the disk IO status of the host | +| [`ebpftrace`](../integrations/ebpftrace.md) | Receive eBPF trace span and link these spans to generate trace id | +| [`mem`](../integrations/mem.md) | Collect the memory usage of the host | +| [`swap`](../integrations/swap.md) | Collect Swap memory usage | +| [`system`](../integrations/system.md) | Collect the load of host operating system | +| [`net`](../integrations/net.md) | Collect host network traffic | +| [`hostobject`](../integrations/hostobject.md) | Collect basic information of host computer (such as operating system information, hardware information, etc.) | +| [`DataKit(dk)`](../integrations/dk.md) | Collect Datakit running metrics | ### Install Specific Version {#version-install} @@ -317,21 +317,22 @@ Only Linux and Windows ([:octicons-tag-24: Version-1.15.0](changelog.md#cl-1.15. ### Other Installation Options {#env-others} -| Environment Variable Name | Sample | Description | -|-------------------------------|-----------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `DK_INSTALL_ONLY` | `on` | Install only, not run | -| `DK_HOSTNAME` | `some-host-name` | Support custom configuration hostname during installation | -| `DK_UPGRADE` | `1` | Upgrade to the latest version (Note: Once this option is turned on, all other options except `DK_UPGRADE_MANAGER` are invalid) | -| `DK_UPGRADE_MANAGER` | `on` | Whether we upgrade the **Remote Upgrade Service** when upgrading Datakit, it's used in conjunction with `DK_UPGRADE`, supported start from [1.5.9](changelog.md#cl-1.5.9) | -| `DK_INSTALLER_BASE_URL` | `https://your-url` | You can choose the installation script for different environments, default to `https://static.guance.com/datakit` | -| `DK_PROXY_TYPE` | - | Proxy type. The options are: `datakit` or `nginx`, both lowercase | -| `DK_NGINX_IP` | - | Proxy server IP address (only need to fill in IP but not port). With the highest priority, this is mutually exclusive with the above "HTTP_PROXY" and "HTTPS_PROXY" and will override both. | -| `DK_INSTALL_LOG` | - | Set the setup log path, default to *install.log* in the current directory, if set to `stdout`, output to the command line terminal. | -| `HTTPS_PROXY` | `IP:Port` | Installed through the Datakit agent | -| `DK_INSTALL_RUM_SYMBOL_TOOLS` | `on` | Install source map tools for RUM, support from Datakit [1.9.2](changelog.md#cl-1.9.2). | -| `DK_VERBOSE` | `on` | Enable more verbose info during install(only for Linux/Mac)[:octicons-tag-24: Version-1.19.0](changelog.md#cl-1.19.0) | -| `DK_CRYPTO_AES_KEY` | `0123456789abcdfg` | Use the encrypted password decryption key to protect plaintext passwords in the collector. [:octicons-tag-24: Version-1.31.0](changelog.md#cl-1.31.0) | -| `DK_CRYPTO_AES_KEY_FILE` | `/usr/local/datakit/enc4dk` | Another way to configure the secret key takes priority over the previous one. Put the key into the file and configure the configuration file path through environment variables. | +| Environment Variable Name | Sample | Description | +| -------------------------------- | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `DK_INSTALL_ONLY` | `on` | Install only, not run | +| `DK_HOSTNAME` | `some-host-name` | Support custom configuration hostname during installation | +| `DK_UPGRADE` | `1` | Upgrade to the latest version (Note: Once this option is turned on, all other options except `DK_UPGRADE_MANAGER` are invalid) | +| `DK_UPGRADE_MANAGER` | `on` | Whether we upgrade the **Remote Upgrade Service** when upgrading Datakit, it's used in conjunction with `DK_UPGRADE`, supported start from [1.5.9](changelog.md#cl-1.5.9) | +| `DK_INSTALLER_BASE_URL` | `https://your-url` | You can choose the installation script for different environments, default to `https://static.guance.com/datakit` | +| `DK_PROXY_TYPE` | - | Proxy type. The options are: `datakit` or `nginx`, both lowercase | +| `DK_NGINX_IP` | - | Proxy server IP address (only need to fill in IP but not port). With the highest priority, this is mutually exclusive with the above "HTTP_PROXY" and "HTTPS_PROXY" and will override both. | +| `DK_INSTALL_LOG` | - | Set the setup log path, default to *install.log* in the current directory, if set to `stdout`, output to the command line terminal. | +| `HTTPS_PROXY` | `IP:Port` | Installed through the Datakit agent | +| `DK_INSTALL_RUM_SYMBOL_TOOLS` | `on` | Install source map tools for RUM, support from Datakit [1.9.2](changelog.md#cl-1.9.2). | +| `DK_VERBOSE` | `on` | Enable more verbose info during install(only for Linux/Mac)[:octicons-tag-24: Version-1.19.0](changelog.md#cl-1.19.0) | +| `DK_CRYPTO_AES_KEY` | `0123456789abcdfg` | Use the encrypted password decryption key to protect plaintext passwords in the collector. [:octicons-tag-24: Version-1.31.0](changelog.md#cl-1.31.0) | +| `DK_CRYPTO_AES_KEY_FILE` | `/usr/local/datakit/enc4dk` | Another way to configure the secret key takes priority over the previous one. Put the key into the file and configure the configuration file path through environment variables. | +| `DK_APM_INSTRUMENTATION_ENABLED` | `host`, `disable` | Enable APM automatic injection for newly started Java and Python applications on the host. | ## FAQ {#faq} diff --git a/internal/export/doc/zh/datakit-install.md b/internal/export/doc/zh/datakit-install.md index e2e5f6d214..0927283b67 100644 --- a/internal/export/doc/zh/datakit-install.md +++ b/internal/export/doc/zh/datakit-install.md @@ -100,7 +100,7 @@ DataKit ELinker 只包含以下采集器: | 采集器名称 | 说明 | -| --------------------------------------------------------- | ----------------------------------------------------------- | +| ---------------------------------------------------------------- | ----------------------------------------------------------- | | [CPU(`cpu`)](../integrations/cpu.md) | 采集主机的 CPU 使用情况 | | [Disk(`disk`)](../integrations/disk.md) | 采集磁盘占用情况 | | [磁盘 IO(`diskio`)](../integrations/diskio.md) | 采集主机的磁盘 IO 情况 | @@ -340,21 +340,22 @@ NAME1="value1" NAME2="value2" ### 其它安装选项 {#env-others} -| 环境变量名 | 取值示例 | 说明 | -| ------------------------------- | ----------------------------- | --------------------------------------------------------------------------------------------------------- | -| `DK_INSTALL_ONLY` | `on` | 仅安装,不运行 | -| `DK_HOSTNAME` | `some-host-name` | 支持安装阶段自定义配置主机名 | -| `DK_UPGRADE` | `1` | 升级到最新版本(注:一旦开启该选项,除 `DK_UPGRADE_MANAGER` 外其它选项均无效) | -| `DK_UPGRADE_MANAGER` | `on` | 升级 Datakit 同时是否升级 **远程升级服务**,需要和 `DK_UPGRADE` 配合使用, 从 [1.5.9](changelog.md#cl-1.5.9) 版本开始支持 | -| `DK_INSTALLER_BASE_URL` | `https://your-url` | 可选择不同环境的安装脚本,默认为 `https://static.guance.com/datakit` | -| `DK_PROXY_TYPE` | - | 代理类型。选项有:`datakit` 或 `nginx`,均为小写 | -| `DK_NGINX_IP` | - | 代理服务器 IP 地址(只需要填 IP 不需要填端口)。这个与上面的 "HTTP_PROXY" 和 "HTTPS_PROXY" 互斥,而且优先级最高,会覆盖以上两者 | -| `DK_INSTALL_LOG` | - | 设置安装程序日志路径,默认为当前目录下的 *install.log*,如果设置为 `stdout` 则输出到命令行终端 | -| `HTTPS_PROXY` | `IP:Port` | 通过 Datakit 代理安装 | -| `DK_INSTALL_RUM_SYMBOL_TOOLS` | `on` | 是否安装 RUM source map 工具集,从 Datakit [1.9.2](changelog.md#cl-1.9.2) 开始支持 | -| `DK_VERBOSE` | `on` | 打开安装过程中的 verbose 选项(仅 Linux/Mac 支持),将输出更多调试信息[:octicons-tag-24: Version-1.19.0](changelog.md#cl-1.19.0) | -| `DK_CRYPTO_AES_KEY` | `0123456789abcdfg` | 使用加密后的密码解密秘钥,用于采集器中明文密码的保护 [:octicons-tag-24: Version-1.31.0](changelog.md#cl-1.31.0) | -| `DK_CRYPTO_AES_KEY_FILE` | `/usr/local/datakit/enc4dk` | 秘钥的另一种配置方式,优先于上一种。将秘钥放到该文件中,并将配置文件路径通过环境变量方式配置即可。 | +| 环境变量名 | 取值示例 | 说明 | +| -------------------------------- | --------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | +| `DK_INSTALL_ONLY` | `on` | 仅安装,不运行 | +| `DK_HOSTNAME` | `some-host-name` | 支持安装阶段自定义配置主机名 | +| `DK_UPGRADE` | `1` | 升级到最新版本(注:一旦开启该选项,除 `DK_UPGRADE_MANAGER` 外其它选项均无效) | +| `DK_UPGRADE_MANAGER` | `on` | 升级 Datakit 同时是否升级 **远程升级服务**,需要和 `DK_UPGRADE` 配合使用, 从 [1.5.9](changelog.md#cl-1.5.9) 版本开始支持 | +| `DK_INSTALLER_BASE_URL` | `https://your-url` | 可选择不同环境的安装脚本,默认为 `https://static.guance.com/datakit` | +| `DK_PROXY_TYPE` | - | 代理类型。选项有:`datakit` 或 `nginx`,均为小写 | +| `DK_NGINX_IP` | - | 代理服务器 IP 地址(只需要填 IP 不需要填端口)。这个与上面的 "HTTP_PROXY" 和 "HTTPS_PROXY" 互斥,而且优先级最高,会覆盖以上两者 | +| `DK_INSTALL_LOG` | - | 设置安装程序日志路径,默认为当前目录下的 *install.log*,如果设置为 `stdout` 则输出到命令行终端 | +| `HTTPS_PROXY` | `IP:Port` | 通过 Datakit 代理安装 | +| `DK_INSTALL_RUM_SYMBOL_TOOLS` | `on` | 是否安装 RUM source map 工具集,从 Datakit [1.9.2](changelog.md#cl-1.9.2) 开始支持 | +| `DK_VERBOSE` | `on` | 打开安装过程中的 verbose 选项(仅 Linux/Mac 支持),将输出更多调试信息[:octicons-tag-24: Version-1.19.0](changelog.md#cl-1.19.0) | +| `DK_CRYPTO_AES_KEY` | `0123456789abcdfg` | 使用加密后的密码解密秘钥,用于采集器中明文密码的保护 [:octicons-tag-24: Version-1.31.0](changelog.md#cl-1.31.0) | +| `DK_CRYPTO_AES_KEY_FILE` | `/usr/local/datakit/enc4dk` | 秘钥的另一种配置方式,优先于上一种。将秘钥放到该文件中,并将配置文件路径通过环境变量方式配置即可。 | +| `DK_APM_INSTRUMENTATION_ENABLED` | `host`, `disable` | 对主机上的新启动的 Java 和 Python 应用开启 APM 自动注入功能 | ## FAQ {#faq}