tlog.app/go/tlog@v0.23.1/web/server.go (about)

     1  package web
     2  
     3  import (
     4  	"bufio"
     5  	"context"
     6  	"embed"
     7  	"fmt"
     8  	"io"
     9  	"net"
    10  	"net/http"
    11  	"net/url"
    12  	"strconv"
    13  	"strings"
    14  	"sync"
    15  	"time"
    16  
    17  	"github.com/nikandfor/hacked/hnet"
    18  	"tlog.app/go/eazy"
    19  	"tlog.app/go/errors"
    20  
    21  	"tlog.app/go/tlog"
    22  	"tlog.app/go/tlog/convert"
    23  )
    24  
    25  type (
    26  	Agent interface {
    27  		Query(ctx context.Context, w io.Writer, ts int64, q string) error
    28  	}
    29  
    30  	Server struct {
    31  		Agent Agent
    32  		FS    http.FileSystem
    33  	}
    34  
    35  	response struct {
    36  		req *http.Request
    37  		w   io.Writer
    38  		h   http.Header
    39  
    40  		once    sync.Once
    41  		nl      bool
    42  		written bool
    43  	}
    44  
    45  	Proto func(context.Context, net.Conn) error
    46  )
    47  
    48  var (
    49  	//go:embed index.html
    50  	//go:embed manifest.json
    51  	//go:embed static
    52  	static embed.FS
    53  )
    54  
    55  func New(a Agent) (*Server, error) {
    56  	return &Server{
    57  		Agent: a,
    58  		FS:    http.FS(static),
    59  	}, nil
    60  }
    61  
    62  func (s *Server) Serve(ctx context.Context, l net.Listener, proto Proto) (err error) {
    63  	var wg sync.WaitGroup
    64  	defer wg.Wait()
    65  
    66  	ctx, cancel := context.WithCancel(ctx)
    67  	defer cancel()
    68  
    69  	for {
    70  		c, err := hnet.Accept(ctx, l)
    71  		if err != nil {
    72  			return err
    73  		}
    74  
    75  		wg.Add(1)
    76  
    77  		go func() {
    78  			defer wg.Done()
    79  
    80  			_ = proto(ctx, c)
    81  		}()
    82  	}
    83  }
    84  
    85  func (s *Server) HandleConn(ctx context.Context, c net.Conn) (err error) {
    86  	defer func() {
    87  		e := c.Close()
    88  		if err == nil {
    89  			err = errors.Wrap(e, "close conn")
    90  		}
    91  	}()
    92  
    93  	c = hnet.NewStoppableConn(ctx, c)
    94  
    95  	br := bufio.NewReader(c)
    96  
    97  	req, err := http.ReadRequest(br)
    98  	if errors.Is(err, io.EOF) {
    99  		return nil
   100  	}
   101  	if err != nil {
   102  		return errors.Wrap(err, "read request")
   103  	}
   104  
   105  	ctx, cancel := context.WithCancel(ctx)
   106  
   107  	go func() {
   108  		defer cancel()
   109  
   110  		_, _ = io.Copy(io.Discard, c)
   111  	}()
   112  
   113  	resp := &response{
   114  		req: req,
   115  		w:   c,
   116  	}
   117  
   118  	defer func() {
   119  		if resp.written {
   120  			return
   121  		}
   122  
   123  		if err == nil {
   124  			resp.WriteHeader(http.StatusNotFound)
   125  			return
   126  		}
   127  
   128  		resp.WriteHeader(http.StatusInternalServerError)
   129  		_, _ = fmt.Fprintf(resp, "%v\n", err)
   130  	}()
   131  
   132  	err = s.HandleRequest(ctx, resp, req)
   133  	if err != nil {
   134  		return err
   135  	}
   136  
   137  	return nil // TODO: handle Content-Length
   138  }
   139  
   140  func (s *Server) HandleRequest(ctx context.Context, rw http.ResponseWriter, req *http.Request) (err error) {
   141  	tr := tlog.SpanFromContext(ctx)
   142  	p := req.URL.Path
   143  
   144  	tr.Printw("request", "method", req.Method, "url", req.URL)
   145  
   146  	switch {
   147  	case strings.HasPrefix(p, "/v0/events"):
   148  		ts := queryInt64(req.URL, "ts", time.Now().UnixNano())
   149  
   150  		var qdata []byte
   151  
   152  		qdata, err = io.ReadAll(req.Body)
   153  		if err != nil {
   154  			return errors.Wrap(err, "read query")
   155  		}
   156  
   157  		var w io.Writer = rw
   158  
   159  		switch ext := pathExt(p); ext {
   160  		case ".tl", ".tlog":
   161  		case ".tlz":
   162  			w = eazy.NewWriter(w, eazy.MiB, 2*1024)
   163  		case ".json":
   164  			w = convert.NewJSON(w)
   165  		case ".logfmt":
   166  			w = convert.NewLogfmt(w)
   167  		case ".html":
   168  			ww := convert.NewWeb(w)
   169  			defer closeWrap(ww, "close Web", &err)
   170  
   171  			w = ww
   172  		default:
   173  			return errors.New("unsupported ext: %v", ext)
   174  		}
   175  
   176  		err = s.Agent.Query(ctx, w, ts, string(qdata))
   177  		if errors.Is(err, context.Canceled) {
   178  			err = nil
   179  		}
   180  
   181  		return errors.Wrap(err, "process query")
   182  	}
   183  
   184  	http.FileServer(s.FS).ServeHTTP(rw, req)
   185  
   186  	return nil
   187  }
   188  
   189  func (r *response) WriteHeader(code int) {
   190  	r.once.Do(func() {
   191  		fmt.Fprintf(r.w, "HTTP/%d.%d %03d %s\r\n", 1, 0, code, http.StatusText(code))
   192  
   193  		for k, v := range r.h {
   194  			fmt.Fprintf(r.w, "%s:", k)
   195  
   196  			for _, v := range v {
   197  				fmt.Fprintf(r.w, " %s", v)
   198  			}
   199  
   200  			fmt.Fprintf(r.w, "\r\n")
   201  		}
   202  
   203  		fmt.Fprintf(r.w, "\r\n")
   204  	})
   205  }
   206  
   207  func (r *response) Header() http.Header {
   208  	if r.h == nil {
   209  		r.h = make(http.Header)
   210  	}
   211  
   212  	return r.h
   213  }
   214  
   215  func (r *response) Write(p []byte) (n int, err error) {
   216  	r.WriteHeader(http.StatusOK)
   217  
   218  	n, err = r.w.Write(p)
   219  
   220  	if n < len(p) {
   221  		r.nl = p[n] == '\n'
   222  	}
   223  
   224  	return
   225  }
   226  
   227  func closeWrap(c io.Closer, msg string, errp *error) {
   228  	e := c.Close()
   229  	if *errp == nil {
   230  		*errp = errors.Wrap(e, msg)
   231  	}
   232  }
   233  
   234  func pathExt(name string) string {
   235  	last := len(name)
   236  
   237  	for i := len(name) - 1; i >= 0; i-- {
   238  		if name[i] == '/' {
   239  			return ""
   240  		}
   241  
   242  		if name[i] != '.' {
   243  			continue
   244  		}
   245  
   246  		switch name[i:last] {
   247  		case ".tl", ".tlog", ".tlz", ".json", ".logfmt", ".html":
   248  			return name[i:]
   249  		default:
   250  			return ""
   251  		}
   252  	}
   253  
   254  	return ""
   255  }
   256  
   257  func queryInt64(u *url.URL, key string, def int64) int64 {
   258  	val := u.Query().Get(key)
   259  
   260  	ts, err := strconv.ParseInt(val, 10, 64)
   261  	if err != nil {
   262  		return def
   263  	}
   264  
   265  	return ts
   266  }