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