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 }