├── .gitignore ├── Makefile ├── README.md ├── bin └── .gitkeep ├── command ├── command.go └── config.go ├── console └── console.go ├── doc └── .gitkeep ├── example.toml ├── go.mod ├── go.sum ├── main.go ├── ssh └── ssh.go └── supervisor-remote-tail.conf /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .vscode 3 | bin/remote-tail* 4 | yunsom.toml 5 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | Version := $(shell date "+%Y%m%d%H%M") 2 | GitCommit := $(shell git rev-parse HEAD) 3 | LDFLAGS := "-s -w -X main.Version=$(Version) -X main.GitCommit=$(GitCommit)" 4 | 5 | run: 6 | go run *.go -conf=example.toml 7 | 8 | mac: 9 | go build -ldflags $(LDFLAGS) -a -installsuffix cgo -o bin/remote-tail-mac *.go 10 | 11 | linux: 12 | CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags $(LDFLAGS) -a -installsuffix cgo -o bin/remote-tail-linux *.go 13 | 14 | windows: 15 | CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags $(LDFLAGS) -a -installsuffix cgo -o bin/remote-tail-win.exe *.go 16 | 17 | dist: 18 | CGO_ENABLED=0 GOOS=linux go build -ldflags $(LDFLAGS) -a -installsuffix cgo -o bin/remote-tail-linux *.go 19 | CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags $(LDFLAGS) -a -installsuffix cgo -o bin/remote-tail-linux-arm64 *.go 20 | CGO_ENABLED=0 GOOS=darwin go build -ldflags $(LDFLAGS) -a -installsuffix cgo -o bin/remote-tail-darwin *.go 21 | CGO_ENABLED=0 GOOS=windows go build -ldflags $(LDFLAGS) -a -installsuffix cgo -o bin/remote-tail.exe *.go 22 | 23 | deploy-local: 24 | cp ./bin/remote-tail-mac /usr/local/bin/remote-tail 25 | 26 | clean: 27 | rm -fr ./bin/remote-tail-linux ./bin/remote-tail-mac ./bin/remote-tail-win.exe 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RemoteTail 2 | 3 | RemoteTail是一款支持同步显示多台远程服务器的日志文件内容更新的工具,使用它可以让你同时监控多台服务器中某个(某些)日志文件的变更,将多台服务器的`tail -f xxx.log`命令的输出合并展示。相比于其他流行的日志手机工具,RemoteTail去掉了在远程服务器安装agent的必要,减小了与远程服务器的耦合,但需要注意的是,由于日志收集使用的是远程执行`tail`命令,因此如果进程退出重启后会出现日志重复或者丢失部分日志的风险。 4 | 5 | RemoteTail只适应于简单的日志收集聚合,如果你不介意重启服务时日志丢失或者重复的问题,那么推荐你尝试一下。 6 | 7 | ![logo](https://ssl.aicode.cc/remote-tail.jpg?20161011) 8 | 9 | ## 使用场景 10 | 11 | 假设公司有两台web服务器A和B,由于初期没有专业运维进行配置集中式的日志服务系统,两台服务器上分别部署了两套相同的代码提供web服务,使用nginx作为负载均衡,请求根据设定的策略转发的这两台web服务器上。 12 | 13 | AB两台服务器中的项目均将日志写到文件系统的`/home/data/logs/laravel.log`文件。这种情况下如果我们需要查看web日志是否正常,一般情况下就需要分别登陆两台服务器,然后分别执行`tail -f /home/data/logs/laravel.log`查看日志文件的最新内容,这在排查问题的时候是非常不方便的。RemoteTail就是为了解决这种问题的,开发人员可以使用它同步显示两台(多台)服务器的日志信息。 14 | 15 | ## 安装 16 | 17 | 在[release页面](https://github.com/mylxsw/remote-tail/releases)下载对应的`remote-tail-平台`可执行文件,将该文件加入到系统的`PATH`环境变量指定的目录中即可。 18 | 19 | 比如,Centos下可以放到`/usr/local/bin`目录。 20 | 21 | mv remote-tail-linux /usr/local/bin/remote-tail 22 | 23 | ## 使用方法 24 | 25 | 使用前需要宿主机建立与远程主机之间的[ssh公钥免密码登陆](https://aicode.cc/linux-mian-mi-ma-deng-lu.html)。 26 | 27 | remote-tail -hosts 'watcher@192.168.1.226,watcher@192.168.1.225' \ 28 | -file '/usr/local/openresty/nginx/logs/access.log' 29 | 30 | ![demo](https://ssl.aicode.cc/remote-tail-demo.jpg?20161011) 31 | 32 | > 如果服务器sshd监听的非默认端口22,可以使用`watcher@192.168.1.226:2222`这种方式指定其它端口。 33 | 34 | ### 简单的日志收集 35 | 36 | 日志聚合后作为单独文件存储,可以使用下面的方法 37 | 38 | nohup remote-tail -hosts 'watcher@192.168.1.226,watcher@192.168.1.225' -file '/usr/local/openresthy/nginx/logs/access.log' -slient=true > ./res.log & 39 | 40 | > `-slient=true`参数用于指定RemoteTail不输出欢迎信息和控制台彩色字符,只输出纯净整洁的日志。 41 | 42 | ### 指定配置文件 43 | 44 | 通过使用`-conf`参数可以为命令指定读取的配置文件,配置文件为TOML格式,请参考`example.toml`文件。 45 | 46 | 配置文件`example.toml`: 47 | 48 | # 全局配置,所有的servers中tail_file配置的默认值 49 | tail_file="/data/logs/laravel.log" 50 | 51 | # tail 命令的选项,一般Linux服务器不需要设置此项,采用默认值即可 52 | # 如果是AIX等服务器,可能tail命令不支持下面这两个选项,可以修改该配置项为 "-f" 53 | #tail_flags="--retry --follow=name" 54 | 55 | # 服务器配置,可以配置多个 56 | # 如果不提供password, 则默认使用系统配置的 ssh-agent 设置, 57 | # 你也可以通过指定 private_key_path 配置项来指定使用特定的私钥来登录 (private_key_path=/home/mylxsw/.ssh/id_rsa) 58 | # 私钥如果有密码的话,需要指定 private_key_passphrase 配置项来指定私钥密码 59 | # server_name, hostname, user 配置为必选,其它可选 60 | [servers] 61 | 62 | [servers.1] 63 | server_name="测试服务器1" 64 | hostname="test1.server.aicode.cc" 65 | user="root" 66 | tail_file="/var/log/messages" 67 | # 指定ssh端口,不指定的情况下使用默认值22 68 | port=2222 69 | 70 | [servers.2] 71 | server_name="测试服务器2" 72 | hostname="test2.server.aicode.cc" 73 | user="root" 74 | tail_file="/var/log/messages" 75 | tail_flags="-f" 76 | 77 | [servers.3] 78 | server_name="测试服务器3" 79 | hostname="test2.server.aicode.cc" 80 | user="demo" 81 | password="123456" 82 | 83 | 执行命令: 84 | 85 | remote-tail -conf=example.toml 86 | 87 | ## 如何贡献 88 | 89 | 欢迎贡献新的功能以及bug修复,**Fork**项目后修改代码,测试通过后提交**pull request**即可。 90 | 91 | ## 问题反馈 92 | 93 | 你可以在github的issue中提出你的bug或者其它需求。 94 | 95 | ## Stargazers over time 96 | 97 | [![Stargazers over time](https://starchart.cc/mylxsw/remote-tail.svg)](https://starchart.cc/mylxsw/remote-tail) 98 | -------------------------------------------------------------------------------- /bin/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mylxsw/remote-tail/db440117d426095516e9c53daa04daa0d749edac/bin/.gitkeep -------------------------------------------------------------------------------- /command/command.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "io" 7 | "strconv" 8 | "strings" 9 | 10 | "github.com/mylxsw/remote-tail/console" 11 | "github.com/mylxsw/remote-tail/ssh" 12 | ) 13 | 14 | type Command struct { 15 | Host string 16 | User string 17 | Script string 18 | Stdout io.Reader 19 | Stderr io.Reader 20 | Server Server 21 | } 22 | 23 | // Message The message used by channel to transport log line by line 24 | type Message struct { 25 | Host string 26 | Content string 27 | } 28 | 29 | // NewCommand Create a new command 30 | func NewCommand(server Server) (cmd *Command) { 31 | cmd = &Command{ 32 | Host: server.Hostname, 33 | User: server.User, 34 | Script: fmt.Sprintf("tail %s %s", server.TailFlags, server.TailFile), 35 | Server: server, 36 | } 37 | 38 | if !strings.Contains(cmd.Host, ":") { 39 | cmd.Host = cmd.Host + ":" + strconv.Itoa(server.Port) 40 | } 41 | 42 | return 43 | } 44 | 45 | // Execute the remote command 46 | func (cmd *Command) Execute(output chan Message) { 47 | 48 | client := &ssh.Client{ 49 | Host: cmd.Host, 50 | User: cmd.User, 51 | Password: cmd.Server.Password, 52 | PrivateKeyPath: cmd.Server.PrivateKeyPath, 53 | PrivateKeyPassphrase: cmd.Server.PrivateKeyPassphrase, 54 | } 55 | 56 | if err := client.Connect(); err != nil { 57 | panic(fmt.Sprintf("[%s] unable to connect: %s", cmd.Host, err)) 58 | } 59 | defer client.Close() 60 | 61 | session, err := client.NewSession() 62 | if err != nil { 63 | panic(fmt.Sprintf("[%s] unable to create session: %s", cmd.Host, err)) 64 | } 65 | defer session.Close() 66 | 67 | if err := session.RequestPty("xterm", 80, 40, *ssh.CreateTerminalModes()); err != nil { 68 | panic(fmt.Sprintf("[%s] unable to create pty: %v", cmd.Host, err)) 69 | } 70 | 71 | cmd.Stdout, err = session.StdoutPipe() 72 | if err != nil { 73 | panic(fmt.Sprintf("[%s] redirect stdout failed: %s", cmd.Host, err)) 74 | } 75 | 76 | cmd.Stderr, err = session.StderrPipe() 77 | if err != nil { 78 | panic(fmt.Sprintf("[%s] redirect stderr failed: %s", cmd.Host, err)) 79 | } 80 | 81 | go bindOutput(cmd.Host, output, &cmd.Stdout, "", 0) 82 | go bindOutput(cmd.Host, output, &cmd.Stderr, "Error:", console.TextRed) 83 | 84 | if err = session.Start(cmd.Script); err != nil { 85 | panic(fmt.Sprintf("[%s] failed to execute command: %s", cmd.Host, err)) 86 | } 87 | 88 | if err = session.Wait(); err != nil { 89 | panic(fmt.Sprintf("[%s] failed to wait command: %s", cmd.Host, err)) 90 | } 91 | } 92 | 93 | // bing the pipe output for formatted output to channel 94 | func bindOutput(host string, output chan Message, input *io.Reader, prefix string, color int) { 95 | reader := bufio.NewReader(*input) 96 | for { 97 | line, err := reader.ReadString('\n') 98 | if err != nil || io.EOF == err { 99 | if err != io.EOF { 100 | panic(fmt.Sprintf("[%s] faield to execute command: %s", host, err)) 101 | } 102 | break 103 | } 104 | 105 | line = prefix + line 106 | if color != 0 { 107 | line = console.ColorfulText(color, line) 108 | } 109 | 110 | output <- Message{ 111 | Host: host, 112 | Content: line, 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /command/config.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | type Server struct { 4 | ServerName string `toml:"server_name"` 5 | Hostname string `toml:"hostname"` 6 | Port int `toml:"port"` 7 | User string `toml:"user"` 8 | Password string `toml:"password"` 9 | PrivateKeyPath string `toml:"private_key_path"` 10 | PrivateKeyPassphrase string `toml:"private_key_passphrase"` 11 | TailFile string `toml:"tail_file"` 12 | TailFlags string `toml:"tail_flags"` 13 | } 14 | 15 | type Config struct { 16 | TailFile string `toml:"tail_file"` 17 | Servers map[string]Server `toml:"servers"` 18 | Slient bool `toml:"slient"` 19 | TailFlags string `toml:"tail_flags"` 20 | } 21 | -------------------------------------------------------------------------------- /console/console.go: -------------------------------------------------------------------------------- 1 | package console 2 | 3 | import "fmt" 4 | 5 | const ( 6 | TextBlack = iota + 30 7 | TextRed 8 | TextGreen 9 | TextYellow 10 | TextBlue 11 | TextMagenta 12 | TextCyan 13 | TextWhite 14 | ) 15 | 16 | func ColorfulText(color int, text string) string { 17 | return fmt.Sprintf("\x1b[0;%dm%s\x1b[0m", color, text) 18 | } 19 | -------------------------------------------------------------------------------- /doc/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mylxsw/remote-tail/db440117d426095516e9c53daa04daa0d749edac/doc/.gitkeep -------------------------------------------------------------------------------- /example.toml: -------------------------------------------------------------------------------- 1 | 2 | # 全局配置,所有的servers中tail_file配置的默认值 3 | tail_file="/data/logs/laravel.log" 4 | 5 | # 是否是静默模式 6 | # 静默模式启动时不会输出welcome消息 7 | slient=false 8 | 9 | # 服务器配置,可以配置多个 10 | # 如果不提供password,则使用当前用户的ssh公钥(private_key_path=/home/mylxsw/.ssh/id_rsa),建议采用该方式,使用密码方式不安全 11 | # server_name, hostname, user 配置为必选,其它可选 12 | [servers] 13 | 14 | [servers.1] 15 | server_name="测试服务器1" 16 | hostname="test1.server.aicode.cc" 17 | user="root" 18 | tail_file="/var/log/messages" 19 | port=22 20 | 21 | [servers.2] 22 | server_name="测试服务器2" 23 | hostname="test2.server.aicode.cc" 24 | user="root" 25 | tail_file="/var/log/messages-not-exist" 26 | 27 | [servers.3] 28 | server_name="测试服务器3" 29 | hostname="test2.server.aicode.cc" 30 | user="demo" 31 | password="123456" 32 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mylxsw/remote-tail 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/BurntSushi/toml v0.3.1 7 | golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c 8 | ) 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= 2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 3 | golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c h1:Vj5n4GlwjmQteupaxJ9+0FNOmBrHfq7vN4btdGoDZgI= 4 | golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 5 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= 6 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 7 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | "os" 8 | "strconv" 9 | "strings" 10 | "sync" 11 | 12 | "github.com/BurntSushi/toml" 13 | "github.com/mylxsw/remote-tail/command" 14 | "github.com/mylxsw/remote-tail/console" 15 | ) 16 | 17 | var mossSep = ".--. --- .-- . .-. . -.. -... -.-- -- -.-- .-.. -..- ... .-- \n" 18 | 19 | var welcomeMessage = getWelcomeMessage() + console.ColorfulText(console.TextMagenta, mossSep) 20 | 21 | var filePath = flag.String("file", "", "-file=\"/var/log/*.log\"") 22 | var hostStr = flag.String("hosts", "", "-hosts=root@192.168.1.101,root@192.168.1.102") 23 | var configFile = flag.String("conf", "", "-conf=example.toml") 24 | var tailFlags = flag.String("tail-flags", "--retry --follow=name", "flags for tail command, you can use -f instead if your server does't support `--retry --follow=name` flags") 25 | var slient = flag.Bool("slient", false, "-slient=false") 26 | 27 | var Version = "" 28 | var GitCommit = "" 29 | 30 | func usageAndExit(message string) { 31 | 32 | if message != "" { 33 | fmt.Fprintln(os.Stderr, message) 34 | } 35 | 36 | flag.Usage() 37 | fmt.Fprint(os.Stderr, "\n") 38 | 39 | os.Exit(1) 40 | } 41 | 42 | func printWelcomeMessage(config command.Config) { 43 | fmt.Println(welcomeMessage) 44 | 45 | for _, server := range config.Servers { 46 | // If there is no tail_file for a service configuration, the global configuration is used 47 | if server.TailFile == "" { 48 | server.TailFile = config.TailFile 49 | } 50 | 51 | serverInfo := fmt.Sprintf("%s@%s:%s", server.User, server.Hostname, server.TailFile) 52 | fmt.Println(console.ColorfulText(console.TextMagenta, serverInfo)) 53 | } 54 | fmt.Printf("\n%s\n", console.ColorfulText(console.TextCyan, mossSep)) 55 | } 56 | 57 | func parseConfig(filePath string, hostStr string, configFile string, slient bool, tailFlags string) (config command.Config) { 58 | if configFile != "" { 59 | if _, err := toml.DecodeFile(configFile, &config); err != nil { 60 | log.Fatal(err) 61 | } 62 | 63 | } else { 64 | 65 | hosts := strings.Split(hostStr, ",") 66 | 67 | config = command.Config{} 68 | config.TailFile = filePath 69 | config.Servers = make(map[string]command.Server, len(hosts)) 70 | config.Slient = slient 71 | config.TailFlags = tailFlags 72 | 73 | for index, hostname := range hosts { 74 | hostInfo := strings.Split(strings.Replace(hostname, ":", "@", -1), "@") 75 | var port int 76 | if len(hostInfo) > 2 { 77 | port, _ = strconv.Atoi(hostInfo[2]) 78 | } 79 | config.Servers["server_"+string(index)] = command.Server{ 80 | ServerName: "server_" + string(index), 81 | Hostname: hostInfo[1], 82 | User: hostInfo[0], 83 | Port: port, 84 | } 85 | } 86 | } 87 | 88 | if config.TailFlags == "" { 89 | config.TailFlags = "--retry --follow=name" 90 | } 91 | 92 | return 93 | } 94 | 95 | func main() { 96 | 97 | flag.Usage = func() { 98 | fmt.Fprint(os.Stderr, welcomeMessage) 99 | fmt.Fprint(os.Stderr, "Options:\n\n") 100 | flag.PrintDefaults() 101 | } 102 | 103 | flag.Parse() 104 | 105 | if (*filePath == "" || *hostStr == "") && *configFile == "" { 106 | usageAndExit("") 107 | } 108 | 109 | config := parseConfig(*filePath, *hostStr, *configFile, *slient, *tailFlags) 110 | if !config.Slient { 111 | printWelcomeMessage(config) 112 | } 113 | 114 | outputs := make(chan command.Message, 255) 115 | var wg sync.WaitGroup 116 | 117 | for _, server := range config.Servers { 118 | wg.Add(1) 119 | go func(server command.Server) { 120 | defer func() { 121 | if err := recover(); err != nil { 122 | fmt.Printf(console.ColorfulText(console.TextRed, "Error: %s\n"), err) 123 | } 124 | }() 125 | defer wg.Done() 126 | 127 | // If there is no tail_file for a service configuration, the global configuration is used 128 | if server.TailFile == "" { 129 | server.TailFile = config.TailFile 130 | } 131 | 132 | if server.TailFlags == "" { 133 | server.TailFlags = config.TailFlags 134 | } 135 | 136 | // If the service configuration does not have a port, the default value of 22 is used 137 | if server.Port == 0 { 138 | server.Port = 22 139 | } 140 | 141 | cmd := command.NewCommand(server) 142 | cmd.Execute(outputs) 143 | }(server) 144 | } 145 | 146 | if len(config.Servers) > 0 { 147 | go func() { 148 | for output := range outputs { 149 | content := strings.Trim(output.Content, "\r\n") 150 | // 去掉文件名称输出 151 | if content == "" || (strings.HasPrefix(content, "==>") && strings.HasSuffix(content, "<==")) { 152 | continue 153 | } 154 | 155 | if config.Slient { 156 | fmt.Printf("%s -> %s\n", output.Host, content) 157 | } else { 158 | fmt.Printf( 159 | "%s %s %s\n", 160 | console.ColorfulText(console.TextGreen, output.Host), 161 | console.ColorfulText(console.TextYellow, "->"), 162 | content, 163 | ) 164 | } 165 | } 166 | }() 167 | } else { 168 | fmt.Println(console.ColorfulText(console.TextRed, "No target host is available")) 169 | } 170 | 171 | wg.Wait() 172 | } 173 | 174 | func getWelcomeMessage() string { 175 | return ` 176 | ____ _ _____ _ _ 177 | | _ \ ___ _ __ ___ ___ | |_ __|_ _|_ _(_) | 178 | | |_) / _ \ '_ ' _ \ / _ \| __/ _ \| |/ _' | | | 179 | | _ < __/ | | | | | (_) | || __/| | (_| | | | 180 | |_| \_\___|_| |_| |_|\___/ \__\___||_|\__,_|_|_| 181 | 182 | Author: mylxsw 183 | Homepage: github.com/mylxsw/remote-tail 184 | Version: ` + Version + "(" + GitCommit + ")" + ` 185 | ` 186 | } 187 | -------------------------------------------------------------------------------- /ssh/ssh.go: -------------------------------------------------------------------------------- 1 | package ssh 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "log" 7 | "net" 8 | "os" 9 | "path/filepath" 10 | 11 | "golang.org/x/crypto/ssh" 12 | "golang.org/x/crypto/ssh/agent" 13 | ) 14 | 15 | type Client struct { 16 | Host string 17 | User string 18 | Password string 19 | PrivateKeyPath string 20 | PrivateKeyPassphrase string 21 | *ssh.Client 22 | } 23 | 24 | func (sshClient *Client) Connect() error { 25 | 26 | conf := ssh.ClientConfig{ 27 | User: sshClient.User, 28 | HostKeyCallback: ssh.InsecureIgnoreHostKey(), 29 | } 30 | if sshClient.Password != "" { 31 | conf.Auth = append(conf.Auth, ssh.Password(sshClient.Password)) 32 | } else if sshClient.PrivateKeyPath != "" { 33 | privateKey, err := getPrivateKey(sshClient.PrivateKeyPath, sshClient.PrivateKeyPassphrase) 34 | if err != nil { 35 | return err 36 | } 37 | 38 | conf.Auth = append(conf.Auth, privateKey) 39 | } else { 40 | // if occur error "Failed to open SSH_AUTH_SOCK: dial unix: missing address", 41 | // execute command: eval `ssh-agent`,and enter passphrase 42 | conn, err := net.Dial("unix", os.Getenv("SSH_AUTH_SOCK")) 43 | if err != nil { 44 | log.Fatalf("Failed to open SSH_AUTH_SOCK: %v", err) 45 | } 46 | 47 | agentClient := agent.NewClient(conn) 48 | // Use a callback rather than PublicKeys so we only consult the 49 | // agent once the remote server wants it. 50 | conf.Auth = append(conf.Auth, ssh.PublicKeysCallback(agentClient.Signers)) 51 | } 52 | client, err := ssh.Dial("tcp", sshClient.Host, &conf) 53 | 54 | if err != nil { 55 | return fmt.Errorf("unable to connect: %v", err) 56 | } 57 | 58 | sshClient.Client = client 59 | 60 | return nil 61 | } 62 | 63 | // Close the connection 64 | func (sshClient *Client) Close() { 65 | sshClient.Client.Close() 66 | } 67 | 68 | // Get the private key for current user 69 | func getPrivateKey(privateKeyPath string, privateKeyPassphrase string) (ssh.AuthMethod, error) { 70 | if !fileExist(privateKeyPath) { 71 | defaultPrivateKeyPath := filepath.Join(os.Getenv("HOME"), ".ssh/id_rsa") 72 | log.Printf("Warning: private key path [%s] does not exist, using default %s instead", privateKeyPath, defaultPrivateKeyPath) 73 | 74 | privateKeyPath = defaultPrivateKeyPath 75 | } 76 | 77 | key, err := ioutil.ReadFile(privateKeyPath) 78 | if err != nil { 79 | return nil, fmt.Errorf("unable to parse private key: %v", err) 80 | } 81 | 82 | var signer ssh.Signer 83 | if privateKeyPassphrase != "" { 84 | signer, err = ssh.ParsePrivateKeyWithPassphrase(key, []byte(privateKeyPassphrase)) 85 | } else { 86 | signer, err = ssh.ParsePrivateKey(key) 87 | } 88 | if err != nil { 89 | return nil, fmt.Errorf("parse private key failed: %v", err) 90 | } 91 | 92 | return ssh.PublicKeys(signer), nil 93 | } 94 | 95 | func CreateTerminalModes() *ssh.TerminalModes { 96 | return &ssh.TerminalModes{ 97 | ssh.ECHO: 0, 98 | ssh.TTY_OP_ISPEED: 14400, 99 | ssh.TTY_OP_OSPEED: 14400, 100 | } 101 | } 102 | 103 | func fileExist(path string) bool { 104 | _, err := os.Stat(path) 105 | if err != nil && os.IsNotExist(err) { 106 | return false 107 | } 108 | 109 | return true 110 | } 111 | -------------------------------------------------------------------------------- /supervisor-remote-tail.conf: -------------------------------------------------------------------------------- 1 | [program:remote-tail-example] 2 | process_name=%(program_name)s_%(process_num)02d 3 | command=remote-tail -conf=/usr/local/etc/remote-tail/example.toml 4 | autostart=true 5 | autorestart=true 6 | user=root 7 | numprocs=1 8 | redirect_stderr=true 9 | stdout_logfile=/data/logs/example.log --------------------------------------------------------------------------------