github.com/nikandfor/tlog@v0.21.5-0.20231108111739-3ef89426a96d/ext/tlflag/flag.go (about) 1 package tlflag 2 3 import ( 4 "io" 5 "net" 6 "net/url" 7 "os" 8 "path" 9 "path/filepath" 10 "strconv" 11 "strings" 12 "time" 13 14 "github.com/nikandfor/errors" 15 "github.com/nikandfor/loc" 16 17 "github.com/nikandfor/tlog" 18 "github.com/nikandfor/tlog/convert" 19 "github.com/nikandfor/tlog/rotated" 20 "github.com/nikandfor/tlog/tlio" 21 "github.com/nikandfor/tlog/tlwire" 22 "github.com/nikandfor/tlog/tlz" 23 ) 24 25 type ( 26 FileOpener = func(name string, flags int, mode os.FileMode) (interface{}, error) 27 28 writerWrapper func(io.Writer, io.Closer) (io.Writer, io.Closer, error) 29 readerWrapper func(io.Reader, io.Closer) (io.Reader, io.Closer, error) 30 ) 31 32 var ( 33 OpenFileWriter FileOpener = OSOpenFile 34 OpenFileReader FileOpener = OpenFileReReader(OSOpenFile) 35 ) 36 37 func OpenWriter(dst string) (wc io.WriteCloser, err error) { 38 var ws tlio.MultiWriter 39 40 defer func() { 41 if err == nil { 42 return 43 } 44 45 _ = ws.Close() 46 }() 47 48 for _, d := range strings.Split(dst, ",") { 49 if d == "" { 50 continue 51 } 52 53 u, err := ParseURL(d) 54 if err != nil { 55 return nil, errors.Wrap(err, "parse %v", d) 56 } 57 58 tlog.V("writer_url").Printw(d, "scheme", u.Scheme, "host", u.Host, "path", u.Path, "query", u.RawQuery, "from", loc.Caller(1)) 59 60 w, err := openw(u) 61 if err != nil { 62 return nil, errors.Wrap(err, "%v", d) 63 } 64 65 ws = append(ws, w) 66 } 67 68 if len(ws) == 1 { 69 var ok bool 70 if wc, ok = ws[0].(io.WriteCloser); ok { 71 return wc, nil 72 } 73 } 74 75 return ws, nil 76 } 77 78 func openw(u *url.URL) (io.Writer, error) { 79 // fmt.Fprintf(os.Stderr, "openw %q\n", fname) 80 81 w, c, err := openwc(u, u.Path) 82 if err != nil { 83 return nil, err 84 } 85 86 return writeCloser(w, c), nil 87 } 88 89 func openwc(u *url.URL, base string) (w io.Writer, c io.Closer, err error) { 90 // fmt.Fprintf(os.Stderr, "openwc %q %q\n", base, ext) 91 92 // w := os.Create("file.json.ez") 93 // w = tlz.NewEncoder(w) 94 // w = convert.NewJSON(w) 95 96 var wrap []writerWrapper 97 98 more: 99 ext := filepath.Ext(base) 100 base = strings.TrimSuffix(base, ext) 101 102 switch ext { 103 case ".tlog", ".tl": 104 case ".tlogdump", ".tldump": 105 wrap = append(wrap, func(w io.Writer, c io.Closer) (io.Writer, io.Closer, error) { 106 w = tlwire.NewDumper(w) 107 108 return w, c, nil 109 }) 110 case ".log", "": 111 wrap = append(wrap, func(w io.Writer, c io.Closer) (io.Writer, io.Closer, error) { 112 ff := tlog.LstdFlags 113 ff = updateConsoleFlags(ff, u.RawQuery) 114 115 w = tlog.NewConsoleWriter(w, ff) 116 117 return w, c, nil 118 }) 119 case ".tlz", ".eazy", ".ez": 120 wrap = append(wrap, func(w io.Writer, c io.Closer) (io.Writer, io.Closer, error) { 121 if f, ok := w.(*rotated.File); ok { 122 f.OpenFile = RotatedTLZFileOpener(f.OpenFile) 123 124 return f, c, nil 125 } 126 127 w = tlz.NewEncoder(w, tlz.MiB) 128 129 return w, c, nil 130 }) 131 132 if ext == ".eazy" || ext == ".ez" { 133 goto more 134 } 135 case ".json": 136 wrap = append(wrap, func(w io.Writer, c io.Closer) (io.Writer, io.Closer, error) { 137 w = convert.NewJSON(w) 138 139 return w, c, nil 140 }) 141 case ".logfmt": 142 wrap = append(wrap, func(w io.Writer, c io.Closer) (io.Writer, io.Closer, error) { 143 w = convert.NewLogfmt(w) 144 145 return w, c, nil 146 }) 147 case ".html": 148 wrap = append(wrap, func(w io.Writer, c io.Closer) (io.Writer, io.Closer, error) { 149 wc := writeCloser(w, c) 150 w = convert.NewWeb(wc) 151 c, _ = w.(io.Closer) 152 153 return w, c, nil 154 }) 155 case ".eazydump", ".ezdump": 156 wrap = append(wrap, func(w io.Writer, c io.Closer) (io.Writer, io.Closer, error) { 157 w = tlz.NewDumper(w) 158 w = tlz.NewEncoder(w, tlz.MiB) 159 160 return w, c, nil 161 }) 162 default: 163 return nil, nil, errors.New("unsupported format: %v", ext) 164 } 165 166 w, c, err = openwriter(u, base) 167 if err != nil { 168 return nil, nil, err 169 } 170 171 for _, wrap := range wrap { 172 w, c, err = wrap(w, c) 173 if err != nil { 174 return 175 } 176 } 177 178 return w, c, nil 179 } 180 181 func openwriter(u *url.URL, base string) (w io.Writer, c io.Closer, err error) { 182 switch base { 183 case "-", "stdout": 184 return os.Stdout, nil, nil 185 case "", "stderr": 186 return os.Stderr, nil, nil 187 case "discard": 188 return io.Discard, nil, nil 189 } 190 191 var f interface{} 192 193 if u.Scheme != "" { 194 f, err = openwurl(u) 195 } else { 196 f, err = openwfile(u) 197 } 198 if err != nil { 199 return nil, nil, err 200 } 201 202 w = f.(io.Writer) 203 c, _ = f.(io.Closer) 204 205 return w, c, nil 206 } 207 208 func openwfile(u *url.URL) (interface{}, error) { 209 fname := u.Path 210 211 of := os.O_APPEND | os.O_WRONLY | os.O_CREATE 212 of = updateFileFlags(of, u.RawQuery) 213 214 mode := os.FileMode(0o644) 215 216 q := u.Query() 217 218 if rotated.IsPattern(filepath.Base(fname)) || q.Get("rotated") != "" { 219 f := rotated.Create(fname) 220 f.Flags = of 221 f.Mode = mode 222 f.OpenFile = openFileWriter 223 224 if v := q.Get("max_file_size"); v != "" { 225 x, err := ParseBytes(v) 226 if err == nil { 227 f.MaxFileSize = x 228 } 229 } 230 231 if v := q.Get("max_file_age"); v != "" { 232 x, err := time.ParseDuration(v) 233 if err == nil { 234 f.MaxFileAge = x 235 } 236 } 237 238 if v := q.Get("max_total_size"); v != "" { 239 x, err := ParseBytes(v) 240 if err == nil { 241 f.MaxTotalSize = x 242 } 243 } 244 245 if v := q.Get("max_total_age"); v != "" { 246 x, err := time.ParseDuration(v) 247 if err == nil { 248 f.MaxTotalAge = x 249 } 250 } 251 252 return f, nil 253 } 254 255 return OpenFileWriter(fname, of, mode) 256 } 257 258 func openwurl(u *url.URL) (f interface{}, err error) { 259 if u.Scheme == "file" { 260 return openwfile(u) 261 } 262 263 switch u.Scheme { 264 case "unix", "unixgram": 265 default: 266 return nil, errors.New("unsupported scheme: %v", u.Scheme) 267 } 268 269 return tlio.NewReWriter(func(w io.Writer, err error) (io.Writer, error) { 270 if c, ok := w.(io.Closer); ok { 271 _ = c.Close() 272 } 273 274 return net.Dial(u.Scheme, u.Path) 275 }), nil 276 } 277 278 func OpenReader(src string) (rc io.ReadCloser, err error) { 279 u, err := ParseURL(src) 280 if err != nil { 281 return nil, errors.Wrap(err, "parse %v", src) 282 } 283 284 r, err := openr(u) 285 if err != nil { 286 return nil, err 287 } 288 289 var ok bool 290 if rc, ok = r.(io.ReadCloser); ok { 291 return rc, nil 292 } 293 294 return tlio.NopCloser{ 295 Reader: r, 296 }, nil 297 } 298 299 func openr(u *url.URL) (io.Reader, error) { 300 r, c, err := openrc(u, u.Path) 301 if err != nil { 302 return nil, err 303 } 304 305 if c == nil { 306 if _, ok := r.(io.Closer); ok { 307 return tlio.NopCloser{Reader: r}, nil 308 } 309 310 return r, nil 311 } 312 313 if r.(interface{}) == c.(interface{}) { 314 return r, nil 315 } 316 317 return tlio.ReadCloser{ 318 Reader: r, 319 Closer: c, 320 }, nil 321 } 322 323 func openrc(u *url.URL, base string) (r io.Reader, c io.Closer, err error) { 324 var wrap []readerWrapper 325 326 more: 327 ext := filepath.Ext(base) 328 base = strings.TrimSuffix(base, ext) 329 330 switch ext { 331 case ".tlog", ".tl", "": 332 case ".tlz", ".eazy", ".ez": 333 wrap = append(wrap, func(r io.Reader, c io.Closer) (io.Reader, io.Closer, error) { 334 r = tlz.NewDecoder(r) 335 336 return r, c, nil 337 }) 338 339 if ext == ".eazy" || ext == ".ez" { 340 goto more 341 } 342 default: 343 return nil, nil, errors.New("unsupported format: %v", ext) 344 } 345 346 r, c, err = openreader(u, base) 347 if err != nil { 348 return nil, nil, err 349 } 350 351 for _, wrap := range wrap { 352 r, c, err = wrap(r, c) 353 if err != nil { 354 return 355 } 356 } 357 358 return r, c, nil 359 } 360 361 func openreader(u *url.URL, base string) (r io.Reader, c io.Closer, err error) { 362 switch base { 363 case "-", "", "stdin": 364 return os.Stdin, nil, nil 365 } 366 367 var f interface{} 368 369 if u.Scheme != "" { 370 f, err = openrurl(u) 371 } else { 372 f, err = openrfile(u) 373 } 374 if err != nil { 375 return nil, nil, err 376 } 377 378 if rc, ok := f.(tlio.ReadCloser); ok { 379 return rc.Reader, rc.Closer, nil 380 } 381 382 r = f.(io.Reader) 383 c, _ = f.(io.Closer) 384 385 return r, c, nil 386 } 387 388 func openrfile(u *url.URL) (interface{}, error) { 389 return OpenFileReader(u.Path, os.O_RDONLY, 0) 390 } 391 392 func openrurl(u *url.URL) (interface{}, error) { 393 if u.Scheme == "file" { 394 return openrfile(u) 395 } 396 397 switch u.Scheme { //nolint:gocritic 398 default: 399 return nil, errors.New("unsupported scheme: %v", u.Scheme) 400 } 401 } 402 403 func updateFileFlags(of int, q string) int { 404 for _, c := range q { 405 if c == '0' { 406 of |= os.O_TRUNC 407 } 408 } 409 410 return of 411 } 412 413 func updateConsoleFlags(ff int, q string) int { 414 for _, c := range q { 415 switch c { 416 case 'd': 417 ff |= tlog.LdetFlags 418 case 'm': 419 ff |= tlog.Lmilliseconds 420 case 'M': 421 ff |= tlog.Lmicroseconds 422 case 'n': 423 ff |= tlog.Lfuncname 424 case 'f': 425 ff &^= tlog.Llongfile 426 ff |= tlog.Lshortfile 427 case 'F': 428 ff &^= tlog.Lshortfile 429 ff |= tlog.Llongfile 430 case 'U': 431 ff |= tlog.LUTC 432 } 433 } 434 435 return ff 436 } 437 438 func OSOpenFile(name string, flags int, mode os.FileMode) (interface{}, error) { 439 return os.OpenFile(name, flags, mode) 440 } 441 442 func OpenFileReReader(open FileOpener) FileOpener { 443 return func(name string, flags int, mode os.FileMode) (interface{}, error) { 444 f, err := open(name, flags, mode) 445 if err != nil { 446 return nil, err 447 } 448 449 rs := f.(tlio.ReadSeeker) 450 451 r, err := tlio.NewReReader(rs) 452 if err != nil { 453 return nil, errors.Wrap(err, "open ReReader") 454 } 455 456 r.Hook = func(old, cur int64) { 457 tlog.Printw("file truncated", "name", name, "old_len", old) 458 } 459 460 c, ok := f.(io.Closer) 461 if !ok { 462 return r, nil 463 } 464 465 return tlio.ReadCloser{ 466 Reader: r, 467 Closer: c, 468 }, nil 469 } 470 } 471 472 func OpenFileDumpReader(open FileOpener) FileOpener { 473 tr := tlog.Start("read dumper") 474 475 return func(name string, flags int, mode os.FileMode) (interface{}, error) { 476 f, err := open(name, flags, mode) 477 if err != nil { 478 return nil, err 479 } 480 481 r := f.(io.Reader) 482 483 r = &tlio.DumpReader{ 484 Reader: r, 485 Span: tr.Spawn("open dump reader", "file", name), 486 } 487 488 return r, nil 489 } 490 } 491 492 func RotatedTLZFileOpener(below rotated.FileOpener) rotated.FileOpener { 493 return func(name string, flags int, mode os.FileMode) (io.Writer, error) { 494 w, err := below(name, flags, mode) 495 if err != nil { 496 return nil, errors.Wrap(err, "") 497 } 498 499 w = tlz.NewEncoder(w, tlz.MiB) 500 501 return w, nil 502 } 503 } 504 505 func openFileWriter(name string, flags int, mode os.FileMode) (io.Writer, error) { 506 file, err := OpenFileWriter(name, flags, mode) 507 if err != nil { 508 return nil, err 509 } 510 511 return file.(io.Writer), nil 512 } 513 514 func ParseURL(d string) (u *url.URL, err error) { 515 u, err = url.Parse(d) 516 if err != nil { 517 return nil, err 518 } 519 520 if u.Opaque != "" { 521 return nil, errors.New("unexpected opaque url") 522 } 523 524 if (u.Scheme == "file" || u.Scheme == "unix" || u.Scheme == "unixgram") && u.Host != "" { 525 u.Path = path.Join(u.Host, u.Path) 526 u.Host = "" 527 } 528 529 return u, nil 530 } 531 532 func ParseBytes(s string) (int64, error) { 533 base := 10 534 neg := false 535 536 for strings.HasPrefix(s, "-") { 537 neg = !neg 538 s = s[1:] 539 } 540 541 if strings.HasPrefix(s, "0x") { 542 s = s[2:] 543 base = 16 544 } 545 546 l := 0 547 548 for l < len(s) && s[l] >= '0' && (s[l] <= '9' || base == 16 && (s[l] >= 'a' && s[l] <= 'f' || s[l] >= 'A' && s[l] <= 'F')) { 549 l++ 550 } 551 552 if l == 0 { 553 return 0, errors.New("bad size") 554 } 555 556 var m int64 557 558 switch strings.ToLower(s[l:]) { 559 case "b", "": 560 m = 1 561 case "kb", "k": 562 m = 1000 563 case "kib", "ki": 564 m = 1024 565 case "mb", "m": 566 m = 1e6 567 case "mib", "mi": 568 m = 1 << 20 569 case "gb", "g": 570 m = 1e9 571 case "gib", "gi": 572 m = 1 << 30 573 case "tb", "t": 574 m = 1e12 575 case "tib", "ti": 576 m = 1 << 40 577 default: 578 return 0, errors.New("unsupported suffix: %v", s[l:]) 579 } 580 581 x, err := strconv.ParseInt(s[:l], base, 64) 582 if err != nil { 583 return 0, err 584 } 585 586 if neg { 587 x = -x 588 } 589 590 return x * m, nil 591 } 592 593 func Describe(tr tlog.Span, x interface{}) { 594 describe(tr, x, 0) 595 } 596 597 func describe(tr tlog.Span, x interface{}, d int) { 598 switch x := x.(type) { 599 case tlio.MultiWriter: 600 tr.Printw("describe", "d", d, "typ", tlog.NextAsType, x) 601 602 for _, w := range x { 603 describe(tr, w, d+1) 604 } 605 case tlio.ReadCloser: 606 tr.Printw("describe", "d", d, "typ", tlog.NextAsType, x) 607 608 describe(tr, x.Reader, d+1) 609 describe(tr, x.Closer, d+1) 610 case *tlio.ReReader: 611 tr.Printw("describe", "d", d, "typ", tlog.NextAsType, x) 612 613 describe(tr, x.ReadSeeker, d+1) 614 case *tlz.Decoder: 615 tr.Printw("describe", "d", d, "typ", tlog.NextAsType, x) 616 617 describe(tr, x.Reader, d+1) 618 case *tlog.ConsoleWriter: 619 tr.Printw("describe", "d", d, "typ", tlog.NextAsType, x) 620 621 describe(tr, x.Writer, d+1) 622 case *os.File: 623 tr.Printw("describe", "d", d, "typ", tlog.NextAsType, x, "name", x.Name()) 624 625 case *net.UnixConn: 626 f, err := x.File() 627 628 tr.Printw("describe", "d", d, "typ", tlog.NextAsType, x, "get_file_err", err) 629 630 describe(tr, f, d+1) 631 default: 632 tr.Printw("describe", "d", d, "typ", tlog.NextAsType, x) 633 } 634 } 635 636 func writeCloser(w io.Writer, c io.Closer) io.Writer { 637 if c == nil { 638 if _, ok := w.(io.Closer); ok { 639 return tlio.NopCloser{Writer: w} 640 } 641 642 return w 643 } 644 645 if w.(interface{}) == c.(interface{}) { 646 return w 647 } 648 649 return tlio.WriteCloser{ 650 Writer: w, 651 Closer: c, 652 } 653 }