├── .gitignore ├── GLOCKFILE ├── LICENSE ├── README.md ├── main.go └── src ├── papertrail.go └── rdstail.go /.gitignore: -------------------------------------------------------------------------------- 1 | rdstail 2 | -------------------------------------------------------------------------------- /GLOCKFILE: -------------------------------------------------------------------------------- 1 | github.com/aws/aws-sdk-go 2a76bd5c7daceae9b072d5070d555bde8c6f17d7 2 | github.com/chrismrivera/backoff 0d906324f9aae5fc9630ad1b35f8a7314700926e 3 | github.com/codegangsta/cli 70e3fa51ebed95df8c0fbe1519c1c1f9bc16bb13 4 | github.com/vaughan0/go-ini a98ad7ee00ec53921f08832bc06ecf7fd600e6a1 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 litl, LLC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | RDSTail 2 | ======= 3 | 4 | RDSTail is a tool for tailing or streaming RDS log files. Supports piping to papertrail. 5 | 6 | Installation 7 | ============ 8 | 9 | For now, you must compile from source. Install [Go](https://golang.org). 10 | 11 | » go get github.com/litl/rdstail 12 | 13 | 14 | Usage 15 | ===== 16 | 17 | ``` 18 | » ./rdstail -h 19 | 20 | NAME: 21 | rdstail - Reads AWS RDS logs 22 | 23 | AWS credentials are taken from an ~/.aws/credentials file or the env vars AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY. 24 | 25 | USAGE: 26 | ./rdstail [global options] command [command options] [arguments...] 27 | 28 | VERSION: 29 | 0.1.0 30 | 31 | COMMANDS: 32 | papertrail stream logs into papertrail 33 | watch stream logs to stdout 34 | tail tail the last N lines 35 | help, h Shows a list of commands or help for one command 36 | 37 | GLOBAL OPTIONS: 38 | --instance, -i name of the db instance in rds [required] 39 | --region "us-east-1" AWS region [$AWS_REGION] 40 | --max-retries "10" maximium number of retries for rds requests 41 | --help, -h show help 42 | --version, -v print the version 43 | 44 | ------------------------------------------------------------ 45 | » ./rdstail papertrail -h 46 | 47 | NAME: 48 | ./rdstail papertrail - stream logs into papertrail 49 | 50 | USAGE: 51 | ./rdstail papertrail [command options] [arguments...] 52 | 53 | OPTIONS: 54 | --papertrail, -p papertrail host e.g. logs.papertrailapp.com:8888 [required] 55 | --app, -a "rdstail" app name to send to papertrail 56 | --hostname "os.Hostname()" hostname of the client, sent to papertrail 57 | --rate, -r "3s" rds log polling rate 58 | 59 | ------------------------------------------------------------ 60 | » ./rdstail watch -h 61 | 62 | NAME: 63 | ./rdstail watch - stream logs to stdout 64 | 65 | USAGE: 66 | ./rdstail watch [command options] [arguments...] 67 | 68 | OPTIONS: 69 | --rate, -r "3s" rds log polling rate 70 | 71 | ------------------------------------------------------------ 72 | » ./rdstail tail -h 73 | 74 | NAME: 75 | ./rdstail tail - tail the last N lines 76 | 77 | USAGE: 78 | ./rdstail tail [command options] [arguments...] 79 | 80 | OPTIONS: 81 | --lines, -n "20" output the last n lines. use 0 for a full dump of the most recent file 82 | 83 | ``` 84 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "log" 7 | "os" 8 | "os/signal" 9 | "syscall" 10 | "time" 11 | 12 | "github.com/aws/aws-sdk-go/aws" 13 | "github.com/aws/aws-sdk-go/aws/session" 14 | "github.com/aws/aws-sdk-go/service/rds" 15 | "github.com/urfave/cli" 16 | "github.com/litl/rdstail/src" 17 | ) 18 | 19 | func fie(e error) { 20 | if e != nil { 21 | fmt.Println(e) 22 | os.Exit(1) 23 | } 24 | } 25 | 26 | func signalListen(stop chan<- struct{}) { 27 | c := make(chan os.Signal) 28 | signal.Notify(c, syscall.SIGTERM, syscall.SIGINT) 29 | 30 | <-c 31 | close(stop) 32 | <-c 33 | log.Panic("Aborting on second signal") 34 | } 35 | 36 | func setupRDS(c *cli.Context) *rds.RDS { 37 | region := c.GlobalString("region") 38 | maxRetries := c.GlobalInt("max-retries") 39 | cfg := aws.NewConfig().WithRegion(region).WithMaxRetries(maxRetries) 40 | return rds.New(session.New(), cfg) 41 | } 42 | 43 | func parseRate(c *cli.Context) time.Duration { 44 | rate, err := time.ParseDuration(c.String("rate")) 45 | fie(err) 46 | return rate 47 | } 48 | 49 | func parseDB(c *cli.Context) string { 50 | db := c.GlobalString("instance") 51 | if db == "" { 52 | fie(errors.New("-instance required")) 53 | } 54 | return db 55 | } 56 | 57 | func watch(c *cli.Context) { 58 | r := setupRDS(c) 59 | db := parseDB(c) 60 | rate := parseRate(c) 61 | 62 | stop := make(chan struct{}) 63 | go signalListen(stop) 64 | 65 | err := rdstail.Watch(r, db, rate, func(lines string) error { 66 | fmt.Print(lines) 67 | return nil 68 | }, stop) 69 | 70 | fie(err) 71 | } 72 | 73 | func papertrail(c *cli.Context) { 74 | r := setupRDS(c) 75 | db := parseDB(c) 76 | rate := parseRate(c) 77 | papertrailHost := c.String("papertrail") 78 | if papertrailHost == "" { 79 | fie(errors.New("-papertrail required")) 80 | } 81 | appName := c.String("app") 82 | hostname := c.String("hostname") 83 | if hostname == "os.Hostname()" { 84 | var err error 85 | hostname, err = os.Hostname() 86 | fie(err) 87 | } 88 | 89 | stop := make(chan struct{}) 90 | go signalListen(stop) 91 | 92 | err := rdstail.FeedPapertrail(r, db, rate, papertrailHost, appName, hostname, stop) 93 | 94 | fie(err) 95 | } 96 | 97 | func tail(c *cli.Context) { 98 | r := setupRDS(c) 99 | db := parseDB(c) 100 | numLines := int64(c.Int("lines")) 101 | err := rdstail.Tail(r, db, numLines) 102 | fie(err) 103 | } 104 | 105 | func main() { 106 | app := cli.NewApp() 107 | 108 | app.Name = "rdstail" 109 | app.Usage = `Reads AWS RDS logs 110 | 111 | AWS credentials are taken from an ~/.aws/credentials file or the env vars AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY.` 112 | app.Version = "0.1.0" 113 | app.Flags = []cli.Flag{ 114 | cli.StringFlag{ 115 | Name: "instance, i", 116 | Usage: "name of the db instance in rds [required]", 117 | }, 118 | cli.StringFlag{ 119 | Name: "region", 120 | Value: "us-east-1", 121 | Usage: "AWS region", 122 | EnvVar: "AWS_REGION", 123 | }, 124 | cli.IntFlag{ 125 | Name: "max-retries", 126 | Value: 10, 127 | Usage: "maximium number of retries for rds requests", 128 | }, 129 | } 130 | 131 | app.Commands = []cli.Command{ 132 | { 133 | Name: "papertrail", 134 | Usage: "stream logs into papertrail", 135 | Action: papertrail, 136 | Flags: []cli.Flag{ 137 | cli.StringFlag{ 138 | Name: "papertrail, p", 139 | Value: "", 140 | Usage: "papertrail host e.g. logs.papertrailapp.com:8888 [required]", 141 | }, 142 | cli.StringFlag{ 143 | Name: "app, a", 144 | Value: "rdstail", 145 | Usage: "app name to send to papertrail", 146 | }, 147 | cli.StringFlag{ 148 | Name: "hostname", 149 | Value: "os.Hostname()", 150 | Usage: "hostname of the client, sent to papertrail", 151 | }, 152 | cli.StringFlag{ 153 | Name: "rate, r", 154 | Value: "3s", 155 | Usage: "rds log polling rate", 156 | }, 157 | }, 158 | }, 159 | 160 | { 161 | Name: "watch", 162 | Usage: "stream logs to stdout", 163 | Action: watch, 164 | Flags: []cli.Flag{ 165 | cli.StringFlag{ 166 | Name: "rate, r", 167 | Value: "3s", 168 | Usage: "rds log polling rate", 169 | }, 170 | }, 171 | }, 172 | 173 | { 174 | Name: "tail", 175 | Usage: "tail the last N lines", 176 | Action: tail, 177 | Flags: []cli.Flag{ 178 | cli.IntFlag{ 179 | Name: "lines, n", 180 | Value: 20, 181 | Usage: "output the last n lines. use 0 for a full dump of the most recent file", 182 | }, 183 | }, 184 | }, 185 | } 186 | 187 | app.Run(os.Args) 188 | } 189 | -------------------------------------------------------------------------------- /src/rdstail.go: -------------------------------------------------------------------------------- 1 | package rdstail 2 | 3 | import ( 4 | "bytes" 5 | "crypto/tls" 6 | "crypto/x509" 7 | "errors" 8 | "fmt" 9 | "time" 10 | 11 | "github.com/aws/aws-sdk-go/aws" 12 | "github.com/aws/aws-sdk-go/service/rds" 13 | "github.com/chrismrivera/backoff" 14 | ) 15 | 16 | const ( 17 | papertrailBackoffMaxWait = time.Minute 18 | papertrailBackoffDeadline = time.Minute * 5 19 | // aws-sdk-go already offers retry functionality 20 | ) 21 | 22 | func getMostRecentLogFile(r *rds.RDS, db string) (file *rds.DescribeDBLogFilesDetails, err error) { 23 | yesterday := time.Now().Add(-24 * time.Hour).Unix() 24 | file, err = getMostRecentLogFileSince(r, db, yesterday) 25 | if err != nil { 26 | return 27 | } 28 | 29 | if file == nil { 30 | lastWeek := time.Now().Add(-7 * 24 * time.Hour).Unix() 31 | file, err = getMostRecentLogFileSince(r, db, lastWeek) 32 | if err != nil { 33 | return 34 | } 35 | } 36 | 37 | if file == nil { 38 | file, err = getMostRecentLogFileSince(r, db, 0) 39 | if err != nil { 40 | return 41 | } 42 | } 43 | 44 | return 45 | } 46 | 47 | func getMostRecentLogFileSince(r *rds.RDS, db string, since int64) (file *rds.DescribeDBLogFilesDetails, err error) { 48 | resp, err := describeLogFiles(r, db, since) 49 | if err != nil { 50 | return nil, err 51 | } 52 | for _, d := range resp { 53 | hasData := d.LastWritten != nil && d.LogFileName != nil 54 | isNewer := file == nil || file.LastWritten == nil || *d.LastWritten > *file.LastWritten 55 | if hasData && isNewer { 56 | file = d 57 | } 58 | } 59 | return 60 | } 61 | 62 | func describeLogFiles(r *rds.RDS, db string, since int64) (details []*rds.DescribeDBLogFilesDetails, err error) { 63 | req := &rds.DescribeDBLogFilesInput{ 64 | DBInstanceIdentifier: aws.String(db), 65 | } 66 | if since != 0 { 67 | req.FileLastWritten = aws.Int64(since) 68 | } 69 | 70 | err = r.DescribeDBLogFilesPages(req, func(p *rds.DescribeDBLogFilesOutput, lastPage bool) bool { 71 | details = append(details, p.DescribeDBLogFiles...) 72 | return true 73 | }) 74 | 75 | return 76 | } 77 | 78 | func tailLogFile(r *rds.RDS, db, name string, numLines int64, marker string) (string, string, error) { 79 | req := &rds.DownloadDBLogFilePortionInput{ 80 | DBInstanceIdentifier: aws.String(db), 81 | LogFileName: aws.String(name), 82 | } 83 | if numLines != 0 { 84 | req.NumberOfLines = aws.Int64(numLines) 85 | } 86 | if marker != "" { 87 | req.Marker = aws.String(marker) 88 | } 89 | 90 | var buf bytes.Buffer 91 | var markerPtr *string 92 | err := r.DownloadDBLogFilePortionPages(req, func(p *rds.DownloadDBLogFilePortionOutput, lastPage bool) bool { 93 | if p.LogFileData != nil { 94 | buf.WriteString(*p.LogFileData) 95 | } 96 | if lastPage { 97 | markerPtr = p.Marker 98 | } 99 | return true 100 | }) 101 | 102 | marker = "" 103 | if markerPtr != nil { 104 | marker = *markerPtr 105 | } 106 | 107 | return buf.String(), marker, err 108 | } 109 | 110 | /// cmds 111 | 112 | func Tail(r *rds.RDS, db string, numLines int64) error { 113 | logFile, err := getMostRecentLogFile(r, db) 114 | if err != nil { 115 | return nil 116 | } 117 | if logFile == nil { 118 | return errors.New("no log file found") 119 | } 120 | 121 | tail, _, err := tailLogFile(r, db, *logFile.LogFileName, numLines, "") 122 | if err != nil { 123 | return err 124 | } 125 | fmt.Println(tail) 126 | return nil 127 | } 128 | 129 | func Watch(r *rds.RDS, db string, rate time.Duration, callback func(string) error, stop <-chan struct{}) error { 130 | // Periodically check for new log files (unless there is a way to detect the file is done being written to) 131 | // Poll that log file, retaining the marker 132 | logFile, err := getMostRecentLogFile(r, db) 133 | if err != nil { 134 | return err 135 | } 136 | if logFile == nil { 137 | return errors.New("no log files") 138 | } 139 | 140 | // Get a marker for the end of the log file by requesting the most recent line 141 | lines, marker, err := tailLogFile(r, db, *logFile.LogFileName, 1, "") 142 | if err != nil { 143 | return err 144 | } 145 | 146 | t := time.NewTicker(rate) 147 | empty := 0 148 | const checkLogfileRate = 4 149 | for { 150 | select { 151 | case <-t.C: 152 | // If the logfile tail was empty n times, check for a newer log file 153 | if empty >= checkLogfileRate { 154 | empty = 0 155 | newLogFile, err := getMostRecentLogFileSince(r, db, *logFile.LastWritten) 156 | if err != nil { 157 | return err 158 | } 159 | if newLogFile != nil { 160 | logFile = newLogFile 161 | marker = "" 162 | } 163 | } 164 | 165 | lines, marker, err = tailLogFile(r, db, *logFile.LogFileName, 0, marker) 166 | if err != nil { 167 | return err 168 | } 169 | 170 | if lines == "" { 171 | empty++ 172 | } else { 173 | empty = 0 174 | if err := callback(lines); err != nil { 175 | return err 176 | } 177 | } 178 | case <-stop: 179 | return nil 180 | } 181 | } 182 | 183 | return nil 184 | } 185 | 186 | func FeedPapertrail(r *rds.RDS, db string, rate time.Duration, papertrailHost, app, hostname string, stop <-chan struct{}) error { 187 | nameSegment := fmt.Sprintf(" %s %s: ", hostname, app) 188 | 189 | // Establish TLS connection with papertrail 190 | roots := x509.NewCertPool() 191 | ok := roots.AppendCertsFromPEM([]byte(papertrailPEM)) 192 | if !ok { 193 | return errors.New("failed to parse papertrail root certificate") 194 | } 195 | 196 | conn, err := tls.Dial("tcp", papertrailHost, &tls.Config{ 197 | RootCAs: roots, 198 | }) 199 | if err != nil { 200 | return err 201 | } 202 | defer conn.Close() 203 | 204 | // watch with callback writing to the connection 205 | buf := bytes.Buffer{} 206 | return Watch(r, db, rate, func(lines string) error { 207 | timestamp := time.Now().UTC().Format("2006-01-02T15:04:05") 208 | buf.Reset() 209 | buf.WriteString(timestamp) 210 | buf.WriteString(nameSegment) 211 | buf.WriteString(lines) 212 | return backoff.Try(papertrailBackoffMaxWait, papertrailBackoffDeadline, func() error { 213 | _, err := conn.Write(buf.Bytes()) 214 | return err 215 | }) 216 | }, stop) 217 | } 218 | --------------------------------------------------------------------------------