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  }