github.com/technosophos/deis@v1.7.1-0.20150915173815-f9005256004b/logspout/logspout.go (about) 1 package main 2 3 import ( 4 "fmt" 5 "log" 6 "net" 7 "net/http" 8 "net/url" 9 "os" 10 "regexp" 11 "runtime" 12 "strconv" 13 "strings" 14 "time" 15 16 "github.com/coreos/go-etcd/etcd" 17 dtime "github.com/deis/deis/pkg/time" 18 "github.com/fsouza/go-dockerclient" 19 "github.com/go-martini/martini" 20 "golang.org/x/net/websocket" 21 ) 22 23 var debugMode bool 24 25 func debug(v ...interface{}) { 26 if debugMode { 27 log.Println(v...) 28 } 29 } 30 31 func assert(err error, context string) { 32 if err != nil { 33 log.Fatalf("%s: %v", context, err) 34 } 35 } 36 37 func getopt(name, dfault string) string { 38 value := os.Getenv(name) 39 if value == "" { 40 value = dfault 41 } 42 return value 43 } 44 45 type Colorizer map[string]int 46 47 // returns up to 14 color escape codes (then repeats) for each unique key 48 func (c Colorizer) Get(key string) string { 49 i, exists := c[key] 50 if !exists { 51 c[key] = len(c) 52 i = c[key] 53 } 54 bright := "1;" 55 if i%14 > 6 { 56 bright = "" 57 } 58 return "\x1b[" + bright + "3" + strconv.Itoa(7-(i%7)) + "m" 59 } 60 61 func syslogStreamer(target Target, types []string, logstream chan *Log) { 62 typestr := "," + strings.Join(types, ",") + "," 63 for logline := range logstream { 64 if typestr != ",," && !strings.Contains(typestr, logline.Type) { 65 continue 66 } 67 tag, pid, data := getLogParts(logline) 68 var conn net.Conn 69 if strings.EqualFold(target.Protocol, "tcp") { 70 addr, err := net.ResolveTCPAddr("tcp", target.Addr) 71 assert(err, "syslog") 72 tcpconn, err := net.DialTCP("tcp", nil, addr) 73 assert(err, "syslog") 74 assert(tcpconn.SetWriteBuffer(1048576), "syslog") 75 conn = tcpconn 76 } else if strings.EqualFold(target.Protocol, "udp") { 77 addr, err := net.ResolveUDPAddr("udp", target.Addr) 78 assert(err, "syslog") 79 udpconn, err := net.DialUDP("udp", nil, addr) 80 assert(err, "syslog") 81 assert(udpconn.SetWriteBuffer(1048576), "syslog") 82 conn = udpconn 83 } else { 84 assert(fmt.Errorf("%s is not a supported protocol, use either udp or tcp", target.Protocol), "syslog") 85 } 86 // HACK: Go's syslog package hardcodes the log format, so let's send our own message 87 _, err := fmt.Fprintf(conn, 88 "%s %s[%s]: %s", 89 time.Now().Format(getopt("DATETIME_FORMAT", dtime.DeisDatetimeFormat)), 90 tag, 91 pid, 92 data) 93 assert(err, "syslog") 94 } 95 } 96 97 // getLogParts returns a custom tag and PID for containers that 98 // match Deis' specific application name format. Otherwise, 99 // it returns the original name and 1 as the PID. Additionally, 100 // it returns log data. The function is also smart enough to 101 // detect when a leading tag in the log data represents an attempt 102 // by the controller to log an application event. 103 func getLogParts(logline *Log) (string, string, string) { 104 // example regex that should match: go_v2.web.1 105 match := getMatch(`(^[a-z0-9-]+)_(v[0-9]+)\.([a-z-_]+\.[0-9]+)$`, logline.Name) 106 if match != nil { 107 return match[1], match[3], logline.Data 108 } 109 match = getMatch(`^k8s_([a-z0-9-]+)-[a-z]+\.[\da-f]+_[a-z0-9-]+-([a-z]+-[\da-z]*)_`, logline.Name) 110 if match != nil { 111 return match[1], match[2], logline.Data 112 } 113 if logline.Name == "deis-controller" { 114 data_match := getMatch(`^[A-Z]+ \[([a-z0-9-]+)\]: (.*)`, logline.Data) 115 if data_match != nil { 116 return data_match[1], "deis-controller", data_match[2] 117 } 118 } 119 return logline.Name, "1", logline.Data 120 } 121 122 func getMatch(regex string, name string) []string { 123 r := regexp.MustCompile(regex) 124 match := r.FindStringSubmatch(name) 125 return match 126 } 127 128 func websocketStreamer(w http.ResponseWriter, req *http.Request, logstream chan *Log, closer chan bool) { 129 websocket.Handler(func(conn *websocket.Conn) { 130 for logline := range logstream { 131 if req.URL.Query().Get("type") != "" && logline.Type != req.URL.Query().Get("type") { 132 continue 133 } 134 _, err := conn.Write(append(marshal(logline), '\n')) 135 if err != nil { 136 closer <- true 137 return 138 } 139 } 140 }).ServeHTTP(w, req) 141 } 142 143 func httpStreamer(w http.ResponseWriter, req *http.Request, logstream chan *Log, multi bool) { 144 var colors Colorizer 145 var usecolor, usejson bool 146 nameWidth := 16 147 if req.URL.Query().Get("colors") != "off" { 148 colors = make(Colorizer) 149 usecolor = true 150 } 151 if req.Header.Get("Accept") == "application/json" { 152 w.Header().Add("Content-Type", "application/json") 153 usejson = true 154 } else { 155 w.Header().Add("Content-Type", "text/plain") 156 } 157 for logline := range logstream { 158 if req.URL.Query().Get("types") != "" && logline.Type != req.URL.Query().Get("types") { 159 continue 160 } 161 if usejson { 162 w.Write(append(marshal(logline), '\n')) 163 } else { 164 if multi { 165 if len(logline.Name) > nameWidth { 166 nameWidth = len(logline.Name) 167 } 168 if usecolor { 169 w.Write([]byte(fmt.Sprintf( 170 "%s%"+strconv.Itoa(nameWidth)+"s|%s\x1b[0m\n", 171 colors.Get(logline.Name), logline.Name, logline.Data, 172 ))) 173 } else { 174 w.Write([]byte(fmt.Sprintf( 175 "%"+strconv.Itoa(nameWidth)+"s|%s\n", logline.Name, logline.Data, 176 ))) 177 } 178 } else { 179 w.Write(append([]byte(logline.Data), '\n')) 180 } 181 } 182 w.(http.Flusher).Flush() 183 } 184 } 185 186 func getEtcdValueOrDefault(c *etcd.Client, key string, defaultValue string) string { 187 resp, err := c.Get(key, false, false) 188 if err != nil { 189 if strings.Contains(fmt.Sprintf("%v", err), "Key not found") { 190 return defaultValue 191 } 192 assert(err, "url") 193 } 194 return resp.Node.Value 195 } 196 197 func main() { 198 runtime.GOMAXPROCS(1) 199 debugMode = getopt("DEBUG", "") != "" 200 port := getopt("PORT", "8000") 201 endpoint := getopt("DOCKER_HOST", "unix:///var/run/docker.sock") 202 routespath := getopt("ROUTESPATH", "/var/lib/logspout") 203 204 client, err := docker.NewClient(endpoint) 205 assert(err, "docker") 206 attacher := NewAttachManager(client) 207 router := NewRouteManager(attacher) 208 209 // HACK: if we are connecting to etcd, get the logger's connection 210 // details from there 211 if etcdHost := os.Getenv("ETCD_HOST"); etcdHost != "" { 212 connectionString := []string{"http://" + etcdHost + ":4001"} 213 debug("etcd:", connectionString[0]) 214 etcd := etcd.NewClient(connectionString) 215 etcd.SetDialTimeout(3 * time.Second) 216 hostResp, err := etcd.Get("/deis/logs/host", false, false) 217 assert(err, "url") 218 portResp, err := etcd.Get("/deis/logs/port", false, false) 219 assert(err, "url") 220 protocol := getEtcdValueOrDefault(etcd, "/deis/logs/protocol", "udp") 221 host := fmt.Sprintf("%s:%s", hostResp.Node.Value, portResp.Node.Value) 222 log.Printf("routing all to %s://%s", protocol, host) 223 router.Add(&Route{Target: Target{Type: "syslog", Addr: host, Protocol: protocol}}) 224 } 225 226 if len(os.Args) > 1 { 227 u, err := url.Parse(os.Args[1]) 228 assert(err, "url") 229 log.Println("routing all to " + os.Args[1]) 230 router.Add(&Route{Target: Target{Type: u.Scheme, Addr: u.Host}}) 231 } 232 233 if _, err := os.Stat(routespath); err == nil { 234 log.Println("loading and persisting routes in " + routespath) 235 assert(router.Load(RouteFileStore(routespath)), "persistor") 236 } 237 238 m := martini.Classic() 239 240 m.Get("/logs(?:/(?P<predicate>[a-zA-Z]+):(?P<value>.+))?", func(w http.ResponseWriter, req *http.Request, params martini.Params) { 241 source := new(Source) 242 switch { 243 case params["predicate"] == "id" && params["value"] != "": 244 source.ID = params["value"][:12] 245 case params["predicate"] == "name" && params["value"] != "": 246 source.Name = params["value"] 247 case params["predicate"] == "filter" && params["value"] != "": 248 source.Filter = params["value"] 249 } 250 251 if source.ID != "" && attacher.Get(source.ID) == nil { 252 http.NotFound(w, req) 253 return 254 } 255 256 logstream := make(chan *Log) 257 defer close(logstream) 258 259 var closer <-chan bool 260 if req.Header.Get("Upgrade") == "websocket" { 261 closerBi := make(chan bool) 262 go websocketStreamer(w, req, logstream, closerBi) 263 closer = closerBi 264 } else { 265 go httpStreamer(w, req, logstream, source.All() || source.Filter != "") 266 closer = w.(http.CloseNotifier).CloseNotify() 267 } 268 269 attacher.Listen(source, logstream, closer) 270 }) 271 272 m.Get("/routes", func(w http.ResponseWriter, req *http.Request) { 273 w.Header().Add("Content-Type", "application/json") 274 routes, _ := router.GetAll() 275 w.Write(append(marshal(routes), '\n')) 276 }) 277 278 m.Post("/routes", func(w http.ResponseWriter, req *http.Request) (int, string) { 279 route := new(Route) 280 if err := unmarshal(req.Body, route); err != nil { 281 return http.StatusBadRequest, "Bad request: " + err.Error() 282 } 283 284 // TODO: validate? 285 router.Add(route) 286 287 w.Header().Add("Content-Type", "application/json") 288 return http.StatusCreated, string(append(marshal(route), '\n')) 289 }) 290 291 m.Get("/routes/:id", func(w http.ResponseWriter, req *http.Request, params martini.Params) { 292 route, _ := router.Get(params["id"]) 293 if route == nil { 294 http.NotFound(w, req) 295 return 296 } 297 w.Write(append(marshal(route), '\n')) 298 }) 299 300 m.Delete("/routes/:id", func(w http.ResponseWriter, req *http.Request, params martini.Params) { 301 if ok := router.Remove(params["id"]); !ok { 302 http.NotFound(w, req) 303 } 304 }) 305 306 log.Println("logspout serving http on :" + port) 307 log.Fatal(http.ListenAndServe(":"+port, m)) 308 }