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  }