├── .github └── workflows │ ├── ci.yml │ └── lint.yml ├── .gitignore ├── Changes ├── LICENSE ├── README.md ├── format.go ├── go.mod ├── interface.go ├── internal ├── httputil │ └── httputil.go └── logctx │ └── logctx.go ├── internal_test.go ├── logformat.go ├── logformat_test.go ├── pool.go └── v2 ├── format.go ├── go.mod ├── interface.go ├── internal ├── httputil │ └── httputil.go └── logctx │ └── logctx.go ├── internal_test.go ├── logformat.go ├── logformat_test.go └── pool.go /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | build: 6 | runs-on: ubuntu-latest 7 | strategy: 8 | matrix: 9 | go: [ '1.15', '1.14' ] 10 | name: Go ${{ matrix.go }} test 11 | steps: 12 | - name: Checkout repository 13 | uses: actions/checkout@v2 14 | - name: Install Go stable version 15 | if: matrix.go != 'tip' 16 | uses: actions/setup-go@v2 17 | with: 18 | go-version: ${{ matrix.go }} 19 | - name: Install Go tip 20 | if: matrix.go == 'tip' 21 | run: | 22 | git clone --depth=1 https://go.googlesource.com/go $HOME/gotip 23 | cd $HOME/gotip/src 24 | ./make.bash 25 | echo "::set-env name=GOROOT::$HOME/gotip" 26 | echo "::add-path::$HOME/gotip/bin" 27 | echo "::add-path::$(go env GOPATH)/bin" 28 | - name: Test 29 | run: go test -v -race ./... 30 | - name: Upload code coverage to codecov 31 | if: matrix.go == '1.15' 32 | uses: codecov/codecov-action@v1 33 | with: 34 | file: ./coverage.out 35 | 36 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: lint 2 | on: [push, pull_request] 3 | jobs: 4 | golangci: 5 | name: lint 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v2 9 | - uses: golangci/golangci-lint-action@v2 10 | with: 11 | version: v1.34.1 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | -------------------------------------------------------------------------------- /Changes: -------------------------------------------------------------------------------- 1 | Changes 2 | ======= 3 | 4 | v2.0.5 - 29 Sep 2020 5 | * Implement %T with units, available for Apache 2.4.13+ (#25, jmrein) 6 | 7 | v2.0.4 - 11 Sep 2019 8 | * Fulfill the http.Flusher interface, which allows servers to emit chunks of response while using this library (#24) 9 | 10 | v2.0.3 - 10 Jul 2019 11 | [MISCELLANEOUS] 12 | * Belated addition of Changes file 13 | [FEATURES] 14 | * Environment variables can now be used via `%{ENVIRONMENT_VARIABLE}e` format (#22) 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 lestrrat 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | apache-logformat 2 | =================== 3 | 4 | [![Build Status](https://travis-ci.org/lestrrat-go/apache-logformat.png?branch=master)](https://travis-ci.org/lestrrat-go/apache-logformat) 5 | 6 | [![GoDoc](https://godoc.org/github.com/lestrrat-go/apache-logformat?status.svg)](https://godoc.org/github.com/lestrrat-go/apache-logformat) 7 | 8 | [![Coverage Status](https://coveralls.io/repos/lestrrat-go/apache-logformat/badge.png?branch=topic%2Fgoveralls)](https://coveralls.io/r/lestrrat-go/apache-logformat?branch=topic%2Fgoveralls) 9 | 10 | # SYNOPSYS 11 | 12 | ```go 13 | import ( 14 | "net/http" 15 | "os" 16 | 17 | "github.com/lestrrat-go/apache-logformat" 18 | ) 19 | 20 | func main() { 21 | var s http.ServeMux 22 | s.HandleFunc("/", handleIndex) 23 | s.HandleFunc("/foo", handleFoo) 24 | 25 | http.ListenAndServe(":8080", apachelog.CombinedLog.Wrap(&s, os.Stderr)) 26 | } 27 | ``` 28 | 29 | # DESCRIPTION 30 | 31 | This is a port of Perl5's [Apache::LogFormat::Compiler](https://metacpan.org/release/Apache-LogFormat-Compiler) to golang 32 | -------------------------------------------------------------------------------- /format.go: -------------------------------------------------------------------------------- 1 | package apachelog 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | "strconv" 8 | "strings" 9 | "time" 10 | "unicode/utf8" 11 | 12 | strftime "github.com/lestrrat-go/strftime" 13 | "github.com/pkg/errors" 14 | ) 15 | 16 | func (f FormatWriteFunc) WriteTo(dst io.Writer, ctx LogCtx) error { 17 | return f(dst, ctx) 18 | } 19 | 20 | var dashValue = []byte{'-'} 21 | var emptyValue = []byte(nil) 22 | 23 | func valueOf(s string, replacement []byte) []byte { 24 | if s == "" { 25 | return replacement 26 | } 27 | return []byte(s) 28 | } 29 | 30 | type fixedByteSequence []byte 31 | 32 | func (seq fixedByteSequence) WriteTo(dst io.Writer, _ LogCtx) error { 33 | if _, err := dst.Write([]byte(seq)); err != nil { 34 | return errors.Wrapf(err, "failed to write fixed byte sequence %s", seq) 35 | } 36 | return nil 37 | } 38 | 39 | type requestHeader string 40 | 41 | func (h requestHeader) WriteTo(dst io.Writer, ctx LogCtx) error { 42 | v := ctx.Request().Header.Get(string(h)) 43 | if _, err := dst.Write(valueOf(v, dashValue)); err != nil { 44 | return errors.Wrap(err, "failed to write request header value") 45 | } 46 | return nil 47 | } 48 | 49 | type responseHeader string 50 | 51 | func (h responseHeader) WriteTo(dst io.Writer, ctx LogCtx) error { 52 | v := ctx.ResponseHeader().Get(string(h)) 53 | if _, err := dst.Write(valueOf(v, dashValue)); err != nil { 54 | return errors.Wrap(err, "failed to write response header value") 55 | } 56 | return nil 57 | } 58 | 59 | func makeRequestTimeBegin(s string) (FormatWriter, error) { 60 | f, err := strftime.New(s) 61 | if err != nil { 62 | return nil, errors.Wrap(err, `failed to compile strftime pattern`) 63 | } 64 | 65 | return FormatWriteFunc(func(dst io.Writer, ctx LogCtx) error { 66 | return f.Format(dst, ctx.RequestTime()) 67 | }), nil 68 | } 69 | 70 | func makeRequestTimeEnd(s string) (FormatWriter, error) { 71 | f, err := strftime.New(s) 72 | if err != nil { 73 | return nil, errors.Wrap(err, `failed to compile strftime pattern`) 74 | } 75 | 76 | return FormatWriteFunc(func(dst io.Writer, ctx LogCtx) error { 77 | return f.Format(dst, ctx.ResponseTime()) 78 | }), nil 79 | } 80 | 81 | func elapsedFormatter(key string) (FormatWriter, error) { 82 | switch key { 83 | case "ms": 84 | return elapsedTimeMilliSeconds, nil 85 | case "us": 86 | return elapsedTimeMicroSeconds, nil 87 | case "s": 88 | return elapsedTimeSeconds, nil 89 | default: 90 | return nil, fmt.Errorf("unrecognised elapsed time unit: %s", key) 91 | } 92 | } 93 | 94 | func timeFormatter(key string) (FormatWriter, error) { 95 | var formatter FormatWriter 96 | switch key { 97 | case "sec": 98 | formatter = requestTimeSecondsSinceEpoch 99 | case "msec": 100 | formatter = requestTimeMillisecondsSinceEpoch 101 | case "usec": 102 | formatter = requestTimeMicrosecondsSinceEpoch 103 | case "msec_frac": 104 | formatter = requestTimeMillisecondsFracSinceEpoch 105 | case "usec_frac": 106 | formatter = requestTimeMicrosecondsFracSinceEpoch 107 | default: 108 | const beginPrefix = "begin:" 109 | const endPrefix = "end:" 110 | if strings.HasPrefix(key, beginPrefix) { 111 | return makeRequestTimeBegin(key[len(beginPrefix):]) 112 | } else if strings.HasPrefix(key, endPrefix) { 113 | return makeRequestTimeEnd(key[len(endPrefix):]) 114 | } 115 | // if specify the format of strftime(3) without begin: or end:, same as bigin: 116 | // FYI https://httpd.apache.org/docs/current/en/mod/mod_log_config.html 117 | return makeRequestTimeBegin(key) 118 | } 119 | return formatter, nil 120 | } 121 | 122 | var epoch = time.Unix(0, 0) 123 | 124 | func makeRequestTimeSinceEpoch(base time.Duration) FormatWriter { 125 | return FormatWriteFunc(func(dst io.Writer, ctx LogCtx) error { 126 | dur := ctx.RequestTime().Sub(epoch) 127 | s := strconv.FormatInt(dur.Nanoseconds()/int64(base), 10) 128 | if _, err := dst.Write(valueOf(s, dashValue)); err != nil { 129 | return errors.Wrap(err, `failed to write request time since epoch`) 130 | } 131 | return nil 132 | }) 133 | } 134 | 135 | func makeRequestTimeFracSinceEpoch(base time.Duration) FormatWriter { 136 | return FormatWriteFunc(func(dst io.Writer, ctx LogCtx) error { 137 | dur := ctx.RequestTime().Sub(epoch) 138 | 139 | s := fmt.Sprintf("%g", float64(dur.Nanoseconds()%int64(base*1000))/float64(base)) 140 | if _, err := dst.Write(valueOf(s, dashValue)); err != nil { 141 | return errors.Wrap(err, `failed to write request time since epoch`) 142 | } 143 | return nil 144 | }) 145 | } 146 | 147 | func makeElapsedTime(base time.Duration, fraction int) FormatWriter { 148 | return FormatWriteFunc(func(dst io.Writer, ctx LogCtx) error { 149 | var str string 150 | if elapsed := ctx.ElapsedTime(); elapsed > 0 { 151 | switch fraction { 152 | case timeNotFraction: 153 | str = strconv.Itoa(int(elapsed / base)) 154 | case timeMicroFraction: 155 | str = fmt.Sprintf("%03d", int((elapsed%time.Millisecond)/base)) 156 | case timeMilliFraction: 157 | str = fmt.Sprintf("%03d", int((elapsed%time.Second)/base)) 158 | default: 159 | return errors.New("failed to write elapsed time") 160 | } 161 | } 162 | if _, err := dst.Write(valueOf(str, dashValue)); err != nil { 163 | return errors.Wrap(err, "failed to write elapsed time") 164 | } 165 | return nil 166 | }) 167 | } 168 | 169 | const ( 170 | timeNotFraction = iota 171 | timeMicroFraction 172 | timeMilliFraction 173 | ) 174 | 175 | var ( 176 | elapsedTimeMicroSeconds = makeElapsedTime(time.Microsecond, timeNotFraction) 177 | elapsedTimeMilliSeconds = makeElapsedTime(time.Millisecond, timeNotFraction) 178 | elapsedTimeSeconds = makeElapsedTime(time.Second, timeNotFraction) 179 | requestTimeMicrosecondsFracSinceEpoch = makeRequestTimeFracSinceEpoch(time.Microsecond) 180 | requestTimeMillisecondsFracSinceEpoch = makeRequestTimeFracSinceEpoch(time.Millisecond) 181 | requestTimeSecondsSinceEpoch = makeRequestTimeSinceEpoch(time.Second) 182 | requestTimeMillisecondsSinceEpoch = makeRequestTimeSinceEpoch(time.Millisecond) 183 | requestTimeMicrosecondsSinceEpoch = makeRequestTimeSinceEpoch(time.Microsecond) 184 | ) 185 | 186 | var requestHttpMethod = FormatWriteFunc(func(dst io.Writer, ctx LogCtx) error { 187 | v := valueOf(ctx.Request().Method, emptyValue) 188 | if _, err := dst.Write(v); err != nil { 189 | return errors.Wrap(err, "failed to write request method") 190 | } 191 | return nil 192 | }) 193 | 194 | var requestHttpProto = FormatWriteFunc(func(dst io.Writer, ctx LogCtx) error { 195 | v := valueOf(ctx.Request().Proto, emptyValue) 196 | if _, err := dst.Write(v); err != nil { 197 | return errors.Wrap(err, "failed to write request HTTP request proto") 198 | } 199 | return nil 200 | }) 201 | 202 | var requestRemoteAddr = FormatWriteFunc(func(dst io.Writer, ctx LogCtx) error { 203 | addr := ctx.Request().RemoteAddr 204 | if i := strings.LastIndexByte(addr, ':'); i > -1 { 205 | addr = addr[:i] 206 | } 207 | v := valueOf(addr, dashValue) 208 | if _, err := dst.Write(v); err != nil { 209 | return errors.Wrap(err, "failed to write request remote address") 210 | } 211 | return nil 212 | }) 213 | 214 | var pid = FormatWriteFunc(func(dst io.Writer, ctx LogCtx) error { 215 | v := valueOf(strconv.Itoa(os.Getpid()), emptyValue) 216 | if _, err := dst.Write(v); err != nil { 217 | return errors.Wrap(err, "failed to write pid") 218 | } 219 | return nil 220 | }) 221 | 222 | var rawQuery = FormatWriteFunc(func(dst io.Writer, ctx LogCtx) error { 223 | q := ctx.Request().URL.RawQuery 224 | if q != "" { 225 | q = "?" + q 226 | } 227 | v := valueOf(q, emptyValue) 228 | if _, err := dst.Write(v); err != nil { 229 | return errors.Wrap(err, "failed to write raw request query") 230 | } 231 | return nil 232 | }) 233 | 234 | var requestLine = FormatWriteFunc(func(dst io.Writer, ctx LogCtx) error { 235 | buf := getLogBuffer() 236 | defer releaseLogBuffer(buf) 237 | 238 | r := ctx.Request() 239 | buf.WriteString(r.Method) 240 | buf.WriteByte(' ') 241 | buf.WriteString(r.URL.String()) 242 | buf.WriteByte(' ') 243 | buf.WriteString(r.Proto) 244 | if _, err := buf.WriteTo(dst); err != nil { 245 | return errors.Wrap(err, "failed to write request line") 246 | } 247 | return nil 248 | }) 249 | 250 | var httpStatus = FormatWriteFunc(func(dst io.Writer, ctx LogCtx) error { 251 | var s string 252 | if st := ctx.ResponseStatus(); st != 0 { // can't really happen, but why not 253 | s = strconv.Itoa(st) 254 | } 255 | if _, err := dst.Write(valueOf(s, emptyValue)); err != nil { 256 | return errors.Wrap(err, "failed to write response status") 257 | } 258 | return nil 259 | }) 260 | 261 | var requestTime = FormatWriteFunc(func(dst io.Writer, ctx LogCtx) error { 262 | v := valueOf(ctx.RequestTime().Format("[02/Jan/2006:15:04:05 -0700]"), []byte{'[', ']'}) 263 | if _, err := dst.Write(v); err != nil { 264 | return errors.Wrap(err, "failed to write request time") 265 | } 266 | return nil 267 | }) 268 | 269 | var urlPath = FormatWriteFunc(func(dst io.Writer, ctx LogCtx) error { 270 | v := valueOf(ctx.Request().URL.Path, emptyValue) 271 | if _, err := dst.Write(v); err != nil { 272 | return errors.Wrap(err, "failed to write request URL path") 273 | } 274 | return nil 275 | }) 276 | 277 | var username = FormatWriteFunc(func(dst io.Writer, ctx LogCtx) error { 278 | var v = dashValue 279 | if u := ctx.Request().URL.User; u != nil { 280 | v = valueOf(u.Username(), dashValue) 281 | } 282 | if _, err := dst.Write(v); err != nil { 283 | return errors.Wrap(err, "failed to write username") 284 | } 285 | return nil 286 | }) 287 | 288 | var requestHost = FormatWriteFunc(func(dst io.Writer, ctx LogCtx) error { 289 | var v []byte 290 | h := ctx.Request().Host 291 | if strings.IndexByte(h, ':') > 0 { 292 | v = valueOf(strings.Split(ctx.Request().Host, ":")[0], dashValue) 293 | } else { 294 | v = valueOf(h, dashValue) 295 | } 296 | if _, err := dst.Write(v); err != nil { 297 | return errors.Wrap(err, "failed to write request host") 298 | } 299 | return nil 300 | }) 301 | 302 | var responseContentLength = FormatWriteFunc(func(dst io.Writer, ctx LogCtx) error { 303 | var s string 304 | if cl := ctx.ResponseContentLength(); cl != 0 { 305 | s = strconv.FormatInt(cl, 10) 306 | } 307 | _, err := dst.Write(valueOf(s, dashValue)) 308 | return err 309 | }) 310 | 311 | func makeEnvVar(key string) FormatWriter { 312 | return FormatWriteFunc(func(dst io.Writer, ctx LogCtx) error { 313 | _, err := dst.Write(valueOf(os.Getenv(key), dashValue)) 314 | return err 315 | }) 316 | } 317 | 318 | func (f *Format) compile(s string) error { 319 | var cbs []FormatWriter 320 | 321 | start := 0 322 | max := len(s) 323 | 324 | for i := 0; i < max; { 325 | r, n := utf8.DecodeRuneInString(s[i:]) 326 | if r == utf8.RuneError { 327 | return errors.Wrap(ErrInvalidRuneSequence, "failed to compile format") 328 | } 329 | i += n 330 | 331 | // Not q sequence... go to next rune 332 | if r != '%' { 333 | continue 334 | } 335 | 336 | if start != i { 337 | // this *could* be the last element in string, in which case we just 338 | // say meh, just assume this was a literal percent. 339 | if i == max { 340 | cbs = append(cbs, fixedByteSequence(s[start:i])) 341 | start = i 342 | break 343 | } 344 | cbs = append(cbs, fixedByteSequence(s[start:i-1])) 345 | } 346 | 347 | // Find what we have next. 348 | 349 | r, n = utf8.DecodeRuneInString(s[i:]) 350 | if r == utf8.RuneError { 351 | return errors.Wrap(ErrInvalidRuneSequence, "failed to compile format") 352 | } 353 | i += n 354 | 355 | switch r { 356 | case '%': 357 | cbs = append(cbs, fixedByteSequence([]byte{'%'})) 358 | start = i + n - 1 359 | case 'b': 360 | cbs = append(cbs, responseContentLength) 361 | start = i + n - 1 362 | case 'D': // custom 363 | cbs = append(cbs, elapsedTimeMicroSeconds) 364 | start = i + n - 1 365 | case 'h': 366 | cbs = append(cbs, requestRemoteAddr) 367 | start = i + n - 1 368 | case 'H': 369 | cbs = append(cbs, requestHttpProto) 370 | start = i + n - 1 371 | case 'l': 372 | cbs = append(cbs, fixedByteSequence(dashValue)) 373 | start = i + n - 1 374 | case 'm': 375 | cbs = append(cbs, requestHttpMethod) 376 | start = i + n - 1 377 | case 'p': 378 | cbs = append(cbs, pid) 379 | start = i + n - 1 380 | case 'P': 381 | // Unimplemented 382 | return errors.Wrap(ErrUnimplemented, "failed to compile format") 383 | case 'q': 384 | cbs = append(cbs, rawQuery) 385 | start = i + n - 1 386 | case 'r': 387 | cbs = append(cbs, requestLine) 388 | start = i + n - 1 389 | case 's': 390 | cbs = append(cbs, httpStatus) 391 | start = i + n - 1 392 | case 't': 393 | cbs = append(cbs, requestTime) 394 | start = i + n - 1 395 | case 'T': // custom 396 | cbs = append(cbs, elapsedTimeSeconds) 397 | start = i + n - 1 398 | case 'u': 399 | cbs = append(cbs, username) 400 | start = i + n - 1 401 | case 'U': 402 | cbs = append(cbs, urlPath) 403 | start = i + n - 1 404 | case 'V', 'v': 405 | cbs = append(cbs, requestHost) 406 | start = i + n - 1 407 | case '>': 408 | if max >= i && s[i] == 's' { 409 | // "Last" status doesn't exist in our case, so it's the same as %s 410 | cbs = append(cbs, httpStatus) 411 | start = i + 1 412 | i = i + 1 413 | } else { 414 | // Otherwise we don't know what this is. just do a verbatim copy 415 | cbs = append(cbs, fixedByteSequence([]byte{'%', '>'})) 416 | start = i + n - 1 417 | } 418 | case '{': 419 | // Search the next } 420 | end := -1 421 | for j := i; j < max; j++ { 422 | if s[j] == '}' { 423 | end = j 424 | break 425 | } 426 | } 427 | 428 | if end != -1 && end < max-1 { // Found it! 429 | // check for suffix 430 | blockType := s[end+1] 431 | key := s[i:end] 432 | switch blockType { 433 | case 'e': // environment variables 434 | cbs = append(cbs, makeEnvVar(key)) 435 | case 'i': 436 | cbs = append(cbs, requestHeader(key)) 437 | case 'o': 438 | cbs = append(cbs, responseHeader(key)) 439 | case 't': 440 | // The time, in the form given by format, which should be in an 441 | // extended strftime(3) format (potentially localized). If the 442 | // format starts with begin: (default) the time is taken at the 443 | // beginning of the request processing. If it starts with end: 444 | // it is the time when the log entry gets written, close to the 445 | // end of the request processing. In addition to the formats 446 | // supported by strftime(3), the following format tokens are 447 | // supported: 448 | // 449 | // sec number of seconds since the Epoch 450 | // msec number of milliseconds since the Epoch 451 | // usec number of microseconds since the Epoch 452 | // msec_frac millisecond fraction 453 | // usec_frac microsecond fraction 454 | // 455 | // These tokens can not be combined with each other or strftime(3) 456 | // formatting in the same format string. You can use multiple 457 | // %{format}t tokens instead. 458 | formatter, err := timeFormatter(key) 459 | if err != nil { 460 | return err 461 | } 462 | cbs = append(cbs, formatter) 463 | case 'T': 464 | formatter, err := elapsedFormatter(key) 465 | if err != nil { 466 | return err 467 | } 468 | cbs = append(cbs, formatter) 469 | default: 470 | return errors.Wrap(ErrUnimplemented, "failed to compile format") 471 | } 472 | 473 | start = end + 2 474 | i = end + 1 475 | } else { 476 | cbs = append(cbs, fixedByteSequence([]byte{'%', '{'})) 477 | start = i + n - 1 478 | } 479 | } 480 | } 481 | 482 | if start < max { 483 | cbs = append(cbs, fixedByteSequence(s[start:max])) 484 | } 485 | 486 | f.writers = cbs 487 | return nil 488 | } 489 | 490 | func (f *Format) WriteTo(dst io.Writer, ctx LogCtx) error { 491 | for _, w := range f.writers { 492 | if err := w.WriteTo(dst, ctx); err != nil { 493 | return errors.Wrap(err, "failed to execute FormatWriter") 494 | } 495 | } 496 | return nil 497 | } 498 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/lestrrat-go/apache-logformat 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a 7 | github.com/lestrrat-go/strftime v0.0.0-20180821113735-8b31f9c59b0f 8 | github.com/pkg/errors v0.8.1 9 | github.com/stretchr/testify v1.3.0 10 | ) 11 | -------------------------------------------------------------------------------- /interface.go: -------------------------------------------------------------------------------- 1 | package apachelog 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "net/http" 7 | "time" 8 | ) 9 | 10 | type ApacheLog struct { 11 | format *Format 12 | } 13 | 14 | // Combined is a pre-defined ApacheLog struct to log "common" log format 15 | var CommonLog, _ = New(`%h %l %u %t "%r" %>s %b`) 16 | 17 | // Combined is a pre-defined ApacheLog struct to log "combined" log format 18 | var CombinedLog, _ = New(`%h %l %u %t "%r" %>s %b "%{Referer}i" "%{User-agent}i"`) 19 | 20 | var ( 21 | ErrInvalidRuneSequence = errors.New("invalid rune sequence found in format") 22 | ErrUnimplemented = errors.New("pattern unimplemented") 23 | ) 24 | 25 | // Format describes an Apache log format. Given a logging context, 26 | // it can create a log line. 27 | type Format struct { 28 | writers []FormatWriter 29 | } 30 | 31 | type LogCtx interface { 32 | ElapsedTime() time.Duration 33 | Request() *http.Request 34 | RequestTime() time.Time 35 | ResponseContentLength() int64 36 | ResponseHeader() http.Header 37 | ResponseStatus() int 38 | ResponseTime() time.Time 39 | } 40 | 41 | type FormatWriter interface { 42 | WriteTo(io.Writer, LogCtx) error 43 | } 44 | 45 | type FormatWriteFunc func(io.Writer, LogCtx) error 46 | -------------------------------------------------------------------------------- /internal/httputil/httputil.go: -------------------------------------------------------------------------------- 1 | package httputil 2 | 3 | import ( 4 | "net/http" 5 | "sync" 6 | ) 7 | 8 | var responseWriterPool sync.Pool 9 | 10 | func init() { 11 | responseWriterPool.New = allocResponseWriter 12 | } 13 | 14 | type ResponseWriter struct { 15 | responseContentLength int64 16 | responseStatus int 17 | responseWriter http.ResponseWriter 18 | } 19 | 20 | func GetResponseWriter(w http.ResponseWriter) *ResponseWriter { 21 | rw := responseWriterPool.Get().(*ResponseWriter) 22 | rw.responseWriter = w 23 | return rw 24 | } 25 | 26 | func ReleaseResponseWriter(rw *ResponseWriter) { 27 | rw.Reset() 28 | responseWriterPool.Put(rw) 29 | } 30 | 31 | func allocResponseWriter() interface{} { 32 | rw := &ResponseWriter{} 33 | rw.Reset() 34 | return rw 35 | } 36 | 37 | func (rw ResponseWriter) ContentLength() int64 { 38 | return rw.responseContentLength 39 | } 40 | 41 | func (rw ResponseWriter) StatusCode() int { 42 | return rw.responseStatus 43 | } 44 | 45 | func (rw *ResponseWriter) Reset() { 46 | rw.responseContentLength = 0 47 | rw.responseStatus = http.StatusOK 48 | rw.responseWriter = nil 49 | } 50 | 51 | func (rw *ResponseWriter) Write(buf []byte) (int, error) { 52 | n, err := rw.responseWriter.Write(buf) 53 | rw.responseContentLength += int64(n) 54 | return n, err 55 | } 56 | 57 | func (rw *ResponseWriter) Header() http.Header { 58 | return rw.responseWriter.Header() 59 | } 60 | 61 | func (rw *ResponseWriter) WriteHeader(status int) { 62 | rw.responseStatus = status 63 | rw.responseWriter.WriteHeader(status) 64 | } 65 | 66 | func (rw *ResponseWriter) Flush() { 67 | if f, ok := rw.responseWriter.(http.Flusher); ok { 68 | f.Flush() 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /internal/logctx/logctx.go: -------------------------------------------------------------------------------- 1 | package logctx 2 | 3 | import ( 4 | "net/http" 5 | "sync" 6 | "time" 7 | 8 | "github.com/lestrrat-go/apache-logformat/internal/httputil" 9 | ) 10 | 11 | type clock interface { 12 | Now() time.Time 13 | } 14 | 15 | type defaultClock struct{} 16 | 17 | func (_ defaultClock) Now() time.Time { 18 | return time.Now() 19 | } 20 | 21 | var Clock clock = defaultClock{} 22 | 23 | type Context struct { 24 | elapsedTime time.Duration 25 | request *http.Request 26 | requestTime time.Time 27 | responseContentLength int64 28 | responseHeader http.Header 29 | responseStatus int 30 | responseTime time.Time 31 | } 32 | 33 | var pool = sync.Pool{New: allocCtx} 34 | 35 | func allocCtx() interface{} { 36 | return &Context{} 37 | } 38 | 39 | func Get(r *http.Request) *Context { 40 | ctx := pool.Get().(*Context) 41 | ctx.request = r 42 | ctx.requestTime = Clock.Now() 43 | return ctx 44 | } 45 | 46 | func Release(ctx *Context) { 47 | ctx.Reset() 48 | pool.Put(ctx) 49 | } 50 | 51 | func (ctx *Context) ElapsedTime() time.Duration { 52 | return ctx.elapsedTime 53 | } 54 | 55 | func (ctx *Context) Request() *http.Request { 56 | return ctx.request 57 | } 58 | 59 | func (ctx *Context) RequestTime() time.Time { 60 | return ctx.requestTime 61 | } 62 | 63 | func (ctx *Context) ResponseContentLength() int64 { 64 | return ctx.responseContentLength 65 | } 66 | 67 | func (ctx *Context) ResponseHeader() http.Header { 68 | return ctx.responseHeader 69 | } 70 | 71 | func (ctx *Context) ResponseStatus() int { 72 | return ctx.responseStatus 73 | } 74 | 75 | func (ctx *Context) ResponseTime() time.Time { 76 | return ctx.responseTime 77 | } 78 | 79 | func (ctx *Context) Reset() { 80 | ctx.elapsedTime = time.Duration(0) 81 | ctx.request = nil 82 | ctx.requestTime = time.Time{} 83 | ctx.responseContentLength = 0 84 | ctx.responseHeader = http.Header{} 85 | ctx.responseStatus = http.StatusOK 86 | ctx.responseTime = time.Time{} 87 | } 88 | 89 | func (ctx *Context) Finalize(wrapped *httputil.ResponseWriter) { 90 | ctx.responseTime = Clock.Now() 91 | ctx.elapsedTime = ctx.responseTime.Sub(ctx.requestTime) 92 | ctx.responseContentLength = wrapped.ContentLength() 93 | ctx.responseHeader = wrapped.Header() 94 | ctx.responseStatus = wrapped.StatusCode() 95 | } 96 | -------------------------------------------------------------------------------- /internal_test.go: -------------------------------------------------------------------------------- 1 | package apachelog 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "io/ioutil" 8 | "net/http" 9 | "testing" 10 | "time" 11 | 12 | "github.com/lestrrat-go/apache-logformat/internal/httputil" 13 | "github.com/lestrrat-go/apache-logformat/internal/logctx" 14 | "github.com/stretchr/testify/assert" 15 | "net/http/httptest" 16 | ) 17 | 18 | func isDash(t *testing.T, s string) bool { 19 | return assert.Equal(t, "-", s, "expected dash") 20 | } 21 | 22 | func isEmpty(t *testing.T, s string) bool { 23 | return assert.Equal(t, "", s, "expected dash") 24 | } 25 | 26 | func TestInternalDashEmpty(t *testing.T) { 27 | f := func(t *testing.T, name string, dash bool, f FormatWriter) { 28 | var buf bytes.Buffer 29 | r, _ := http.NewRequest(http.MethodGet, "http://example.com", nil) 30 | r.Host = "" 31 | r.Method = "" 32 | r.Proto = "" 33 | ctx := logctx.Get(r) 34 | 35 | t.Run(fmt.Sprintf("%s (dash=%t)", name, dash), func(t *testing.T) { 36 | if !assert.NoError(t, f.WriteTo(&buf, ctx), "callback should succeed") { 37 | return 38 | } 39 | if dash { 40 | isDash(t, buf.String()) 41 | } else { 42 | isEmpty(t, buf.String()) 43 | } 44 | }) 45 | } 46 | 47 | type dashEmptyCase struct { 48 | Name string 49 | Dash bool 50 | Format FormatWriter 51 | } 52 | cases := []dashEmptyCase{ 53 | {Name: "Request Header", Dash: true, Format: requestHeader("foo")}, 54 | {Name: "Response Header", Dash: true, Format: responseHeader("foo")}, 55 | {Name: "Request Method", Dash: false, Format: requestHttpMethod}, 56 | {Name: "Request Proto", Dash: false, Format: requestHttpProto}, 57 | {Name: "Request RemoteAddr", Dash: true, Format: requestRemoteAddr}, 58 | {Name: "Request Raw Query", Dash: false, Format: rawQuery}, 59 | {Name: "Response Status", Dash: false, Format: httpStatus}, 60 | {Name: "Request Username", Dash: true, Format: username}, 61 | {Name: "Request Host", Dash: true, Format: requestHost}, 62 | {Name: "Response ContentLength", Dash: true, Format: responseContentLength}, 63 | } 64 | 65 | for _, c := range cases { 66 | f(t, c.Name, c.Dash, c.Format) 67 | } 68 | } 69 | 70 | func TestResponseWriterDefaultStatusCode(t *testing.T) { 71 | writer := httptest.NewRecorder() 72 | uut := httputil.GetResponseWriter(writer) 73 | if uut.StatusCode() != http.StatusOK { 74 | t.Fail() 75 | } 76 | } 77 | 78 | func TestFlusherInterface(t *testing.T) { 79 | var rw httputil.ResponseWriter 80 | var f http.Flusher = &rw 81 | _ = f 82 | } 83 | 84 | func TestFlusher(t *testing.T) { 85 | lines := []string{ 86 | "Hello, World!", 87 | "Hello again, World!", 88 | } 89 | 90 | // We need to synchronize the reads with the writes of each chunk 91 | sync := make(chan struct{}) 92 | 93 | handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 94 | for _, line := range lines { 95 | if _, err := w.Write([]byte(line)); err != nil { 96 | return 97 | } 98 | if f, ok := w.(http.Flusher); ok { 99 | f.Flush() 100 | } 101 | sync <- struct{}{} 102 | } 103 | close(sync) 104 | }) 105 | 106 | s := httptest.NewServer(CommonLog.Wrap(handler, ioutil.Discard)) 107 | defer s.Close() 108 | 109 | req, err := http.NewRequest("GET", s.URL, nil) 110 | if !assert.NoError(t, err, "request creation should succeed") { 111 | return 112 | } 113 | 114 | // If it isn't flushing properly and sending chunks, then the call to 115 | // Do() will hang waiting for the entire body, while the handler will be 116 | // stuck after having written the first line. So have an explicit 117 | // timeout for the read. 118 | timer := time.NewTimer(time.Second) 119 | done := make(chan struct{}) 120 | go func() { 121 | defer close(done) 122 | resp, err := http.DefaultClient.Do(req) 123 | if !assert.NoError(t, err, "GET should succeed") { 124 | return 125 | } 126 | defer resp.Body.Close() 127 | 128 | buf := make([]byte, 64) 129 | var i int 130 | for { 131 | _, ok := <-sync 132 | if !ok { 133 | break 134 | } 135 | n, err := resp.Body.Read(buf) 136 | if n == 0 { 137 | if !assert.Equal(t, len(lines)-1, i-1, "wrong number of chunks") { 138 | return 139 | } 140 | break 141 | } 142 | t.Logf("Response body %d: %d %s", i, n, buf[:n]) 143 | if !assert.Equal(t, []byte(lines[i]), buf[:n], "wrong response body") { 144 | return 145 | } 146 | if err == io.EOF { 147 | if !assert.Equal(t, len(lines)-1, i, "wrong number of chunks") { 148 | return 149 | } 150 | break 151 | } 152 | if !assert.NoError(t, err, "Body read should succeed") { 153 | return 154 | } 155 | i++ 156 | } 157 | }() 158 | 159 | select { 160 | case <-timer.C: 161 | close(sync) 162 | t.Fatal("timed out: not flushing properly?") 163 | case <-done: 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /logformat.go: -------------------------------------------------------------------------------- 1 | package apachelog 2 | 3 | import ( 4 | "io" 5 | "net/http" 6 | "os" 7 | 8 | "github.com/lestrrat-go/apache-logformat/internal/httputil" 9 | "github.com/lestrrat-go/apache-logformat/internal/logctx" 10 | "github.com/pkg/errors" 11 | ) 12 | 13 | // New creates a new ApacheLog instance from the given 14 | // format. It will return an error if the format fails to compile. 15 | func New(format string) (*ApacheLog, error) { 16 | var f Format 17 | if err := f.compile(format); err != nil { 18 | return nil, errors.Wrap(err, "failed to compile log format") 19 | } 20 | 21 | return &ApacheLog{format: &f}, nil 22 | } 23 | 24 | // WriteLog generates a log line using the format associated with the 25 | // ApacheLog instance, using the values from ctx. The result is written 26 | // to dst 27 | func (al *ApacheLog) WriteLog(dst io.Writer, ctx LogCtx) error { 28 | buf := getLogBuffer() 29 | defer releaseLogBuffer(buf) 30 | 31 | if err := al.format.WriteTo(buf, ctx); err != nil { 32 | return errors.Wrap(err, "failed to format log line") 33 | } 34 | 35 | b := buf.Bytes() 36 | if b[len(b)-1] != '\n' { 37 | buf.WriteByte('\n') 38 | } 39 | 40 | if _, err := buf.WriteTo(dst); err != nil { 41 | return errors.Wrap(err, "failed to write formated line to destination") 42 | } 43 | return nil 44 | } 45 | 46 | // Wrap creates a new http.Handler that logs a formatted log line 47 | // to dst. 48 | func (al *ApacheLog) Wrap(h http.Handler, dst io.Writer) http.Handler { 49 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 50 | ctx := logctx.Get(r) 51 | defer logctx.Release(ctx) 52 | 53 | wrapped := httputil.GetResponseWriter(w) 54 | defer httputil.ReleaseResponseWriter(wrapped) 55 | 56 | defer func() { 57 | ctx.Finalize(wrapped) 58 | if err := al.WriteLog(dst, ctx); err != nil { 59 | // Hmmm... no where to log except for stderr 60 | os.Stderr.Write([]byte(err.Error())) 61 | return 62 | } 63 | }() 64 | 65 | h.ServeHTTP(wrapped, r) 66 | }) 67 | } 68 | -------------------------------------------------------------------------------- /logformat_test.go: -------------------------------------------------------------------------------- 1 | package apachelog_test 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | "net/http/httptest" 9 | "os" 10 | "strconv" 11 | "strings" 12 | "testing" 13 | "time" 14 | 15 | "github.com/facebookgo/clock" 16 | apachelog "github.com/lestrrat-go/apache-logformat" 17 | "github.com/lestrrat-go/apache-logformat/internal/logctx" 18 | strftime "github.com/lestrrat-go/strftime" 19 | "github.com/stretchr/testify/assert" 20 | ) 21 | 22 | const message = "Hello, World!" 23 | 24 | var hello = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 25 | w.Header().Set("Content-Type", "text/plain") 26 | w.WriteHeader(http.StatusOK) 27 | fmt.Fprint(w, message) 28 | }) 29 | 30 | func TestBasic(t *testing.T) { 31 | r, err := http.NewRequest("GET", "http://golang.org", nil) 32 | if err != nil { 33 | t.Errorf("Failed to create request: %s", err) 34 | } 35 | r.RemoteAddr = "127.0.0.1" 36 | r.Header.Set("User-Agent", "Apache-LogFormat Port In Golang") 37 | r.Header.Set("Referer", "http://dummy.com") 38 | 39 | var out bytes.Buffer 40 | h := apachelog.CombinedLog.Wrap(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 41 | _, _ = w.Write([]byte("Hello, World!")) 42 | }), &out) 43 | 44 | w := httptest.NewRecorder() 45 | h.ServeHTTP(w, r) 46 | 47 | t.Logf("output = %s", strconv.Quote(out.String())) 48 | } 49 | 50 | func newServer(l *apachelog.ApacheLog, h http.Handler, out io.Writer) *httptest.Server { 51 | return httptest.NewServer(l.Wrap(h, out)) 52 | } 53 | 54 | func testLog(t *testing.T, pattern, expected string, h http.Handler, modifyURL func(string) string, modifyRequest func(*http.Request)) { 55 | l, err := apachelog.New(pattern) 56 | if !assert.NoError(t, err, "apachelog.New should succeed") { 57 | return 58 | } 59 | 60 | var buf bytes.Buffer 61 | s := newServer(l, h, &buf) 62 | defer s.Close() 63 | 64 | u := s.URL 65 | if modifyURL != nil { 66 | u = modifyURL(u) 67 | } 68 | 69 | r, err := http.NewRequest("GET", u, nil) 70 | if !assert.NoError(t, err, "request creation should succeed") { 71 | return 72 | } 73 | 74 | if modifyRequest != nil { 75 | modifyRequest(r) 76 | } 77 | 78 | _, err = http.DefaultClient.Do(r) 79 | if !assert.NoError(t, err, "GET should succeed") { 80 | return 81 | } 82 | 83 | if !assert.Equal(t, expected, buf.String()) { 84 | return 85 | } 86 | } 87 | 88 | func TestVerbatim(t *testing.T) { 89 | testLog(t, 90 | "This should be a verbatim percent sign -> %%", 91 | "This should be a verbatim percent sign -> %\n", 92 | hello, 93 | nil, 94 | nil, 95 | ) 96 | } 97 | 98 | func TestResponseHeader(t *testing.T) { 99 | testLog(t, 100 | "%{X-Req-Header}i %{X-Resp-Header}o", 101 | "Gimme a response! Here's your response\n", 102 | http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 103 | w.Header().Add("X-Resp-Header", "Here's your response") 104 | }), 105 | nil, 106 | func(r *http.Request) { 107 | r.Header.Set("X-Req-Header", "Gimme a response!") 108 | }, 109 | ) 110 | } 111 | 112 | func TestQuery(t *testing.T) { 113 | testLog(t, 114 | `%m %U %q %H`, 115 | "GET /foo ?bar=baz HTTP/1.1\n", 116 | hello, 117 | func(u string) string { 118 | return u + "/foo?bar=baz" 119 | }, 120 | nil, 121 | ) 122 | } 123 | 124 | func TestTime(t *testing.T) { 125 | o := logctx.Clock 126 | defer func() { logctx.Clock = o }() 127 | 128 | const longTimeAgo = 233431200 * time.Second 129 | const pattern = `%Y-%m-%d` 130 | 131 | f, _ := strftime.New(pattern) 132 | cl := clock.NewMock() 133 | cl.Add(longTimeAgo) 134 | logctx.Clock = cl 135 | 136 | // Mental note: %{[mu]?sec}t should (milli|micro)?seconds since the epoch. 137 | testLog(t, 138 | fmt.Sprintf( 139 | `%%T %%D %%{sec}t %%{msec}t %%{usec}t %%{begin:%s}t %%{end:%s}t %%{%s}t`, 140 | pattern, 141 | pattern, 142 | pattern, 143 | ), 144 | fmt.Sprintf( 145 | "1 1000000 %d %d %d %s %s %s\n", 146 | longTimeAgo/time.Second, 147 | longTimeAgo/time.Millisecond, 148 | longTimeAgo/time.Microsecond, 149 | f.FormatString(cl.Now()), 150 | f.FormatString(cl.Now().Add(time.Second)), 151 | f.FormatString(cl.Now()), 152 | ), 153 | http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 154 | cl.Add(time.Second) 155 | }), 156 | nil, 157 | nil, 158 | ) 159 | } 160 | 161 | func TestElapsed(t *testing.T) { 162 | o := logctx.Clock 163 | defer func() { logctx.Clock = o }() 164 | cl := clock.NewMock() 165 | logctx.Clock = cl 166 | testLog(t, `%T %D %{s}T %{us}T %{ms}T`, 167 | "3 3141592 3 3141592 3141\n", 168 | http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 169 | cl.Add(3141592 * time.Microsecond) 170 | }), 171 | nil, 172 | nil, 173 | ) 174 | 175 | _, err := apachelog.New(`%{h}T`) 176 | assert.EqualError(t, err, "failed to compile log format: unrecognised elapsed time unit: h") 177 | } 178 | 179 | func TestElapsedTimeFraction(t *testing.T) { 180 | o := logctx.Clock 181 | defer func() { logctx.Clock = o }() 182 | 183 | cl := clock.NewMock() 184 | cl.Add(time.Second + time.Millisecond*200 + time.Microsecond*90) 185 | logctx.Clock = cl 186 | testLog(t, 187 | `%{msec_frac}t %{usec_frac}t`, 188 | "200.09 90\n", 189 | http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}), 190 | nil, 191 | nil, 192 | ) 193 | } 194 | 195 | func TestStrayPercent(t *testing.T) { 196 | testLog(t, 197 | `stray percent at the end: %`, 198 | "stray percent at the end: %\n", 199 | hello, 200 | nil, 201 | nil, 202 | ) 203 | } 204 | 205 | func TestMissingClosingBrace(t *testing.T) { 206 | testLog(t, 207 | `Missing closing brace: %{Test <- this should be verbatim`, 208 | "Missing closing brace: %{Test <- this should be verbatim\n", 209 | hello, 210 | nil, 211 | nil, 212 | ) 213 | } 214 | 215 | func TestPercentS(t *testing.T) { 216 | // %s and %>s should be the same in our case 217 | testLog(t, 218 | `%s = %>s`, 219 | "404 = 404\n", 220 | http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 221 | w.WriteHeader(http.StatusNotFound) 222 | }), 223 | nil, 224 | nil, 225 | ) 226 | } 227 | 228 | func TestPid(t *testing.T) { 229 | testLog(t, 230 | `%p`, // pid 231 | strconv.Itoa(os.Getpid())+"\n", 232 | hello, 233 | nil, 234 | nil, 235 | ) 236 | } 237 | 238 | func TestUnknownAfterPecentGreaterThan(t *testing.T) { 239 | testLog(t, 240 | `%>X should be verbatim`, // %> followed by unknown char 241 | `%>X should be verbatim`+"\n", 242 | hello, 243 | nil, 244 | nil, 245 | ) 246 | } 247 | 248 | func TestFixedSequence(t *testing.T) { 249 | testLog(t, 250 | `hello, world!`, 251 | "hello, world!\n", 252 | hello, 253 | nil, 254 | nil, 255 | ) 256 | } 257 | 258 | func TestFull(t *testing.T) { 259 | l, err := apachelog.New(`hello, %% %b %D %h %H %l %m %p %q %r %s %t %T %{ms}T %u %U %v %V %>s %{X-LogFormat-Test}i %{X-LogFormat-Test}o world!`) 260 | if !assert.NoError(t, err, "apachelog.New should succeed") { 261 | return 262 | } 263 | 264 | o := logctx.Clock 265 | defer func() { logctx.Clock = o }() 266 | 267 | cl := clock.NewMock() 268 | logctx.Clock = cl 269 | h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 270 | cl.Add(5 * time.Second) 271 | w.Header().Set("X-LogFormat-Test", "Hello, Response!") 272 | w.WriteHeader(http.StatusBadRequest) 273 | }) 274 | var buf bytes.Buffer 275 | s := newServer(l, h, &buf) 276 | defer s.Close() 277 | 278 | r, err := http.NewRequest("GET", s.URL+"/hello_world?hello=world", nil) 279 | if !assert.NoError(t, err, "request creation should succeed") { 280 | return 281 | } 282 | 283 | r.Header.Add("X-LogFormat-Test", "Hello, Request!") 284 | 285 | _, err = http.DefaultClient.Do(r) 286 | if !assert.NoError(t, err, "GET should succeed") { 287 | return 288 | } 289 | 290 | if !assert.Regexp(t, `^hello, % - 5000000 127\.0\.0\.1 HTTP/1\.1 - GET \d+ \?hello=world GET /hello_world\?hello=world HTTP/1\.1 400 \[\d{2}/[a-zA-Z]+/\d{4}:\d{2}:\d{2}:\d{2} [+-]\d{4}\] 5 5000 - /hello_world 127\.0\.0\.1 127\.0\.0\.1 400 Hello, Request! Hello, Response! world!\n$`, buf.String(), "Log line must match") { 291 | return 292 | } 293 | t.Logf("%s", buf.String()) 294 | } 295 | 296 | func TestPercentB(t *testing.T) { 297 | testLog(t, 298 | `%b`, 299 | fmt.Sprintf("%d\n", len(message)), 300 | hello, 301 | nil, 302 | nil, 303 | ) 304 | } 305 | 306 | func TestIPv6RemoteAddr(t *testing.T) { 307 | format := `%h` 308 | expected := "[::1]\n" 309 | 310 | al, err := apachelog.New(format) 311 | if !assert.NoError(t, err, "apachelog.New should succeed") { 312 | return 313 | } 314 | 315 | ctx := &Context{ 316 | request: &http.Request{ 317 | RemoteAddr: "[::1]:51111", 318 | }, 319 | } 320 | 321 | var buf bytes.Buffer 322 | _ = al.WriteLog(&buf, ctx) 323 | 324 | if !assert.Equal(t, expected, buf.String()) { 325 | return 326 | } 327 | } 328 | 329 | func TestEnvironmentVariable(t *testing.T) { 330 | // Well.... let's see. I don't want to change the user's env var, 331 | // so let's just scan for something already present in the environment variable list 332 | 333 | for _, v := range os.Environ() { 334 | vs := strings.SplitN(v, "=", 2) 335 | 336 | t.Logf("Testing environment variable %s", vs[0]) 337 | al, err := apachelog.New(fmt.Sprintf(`%%{%s}e`, vs[0])) 338 | if !assert.NoError(t, err, "apachelog.New should succeed") { 339 | return 340 | } 341 | 342 | var ctx Context 343 | var buf bytes.Buffer 344 | _ = al.WriteLog(&buf, &ctx) 345 | 346 | var expected = "-" 347 | if vs[1] != "" { 348 | expected = vs[1] 349 | } 350 | // Be careful, the log line has a trailing new line 351 | expected = expected + "\n" 352 | 353 | if !assert.Equal(t, expected, buf.String()) { 354 | return 355 | } 356 | } 357 | } 358 | 359 | type Context struct { 360 | elapsedTime time.Duration 361 | request *http.Request 362 | requestTime time.Time 363 | responseContentLength int64 364 | responseHeader http.Header 365 | responseStatus int 366 | responseTime time.Time 367 | } 368 | 369 | func (ctx *Context) ElapsedTime() time.Duration { 370 | return ctx.elapsedTime 371 | } 372 | 373 | func (ctx *Context) Request() *http.Request { 374 | return ctx.request 375 | } 376 | 377 | func (ctx *Context) RequestTime() time.Time { 378 | return ctx.requestTime 379 | } 380 | 381 | func (ctx *Context) ResponseContentLength() int64 { 382 | return ctx.responseContentLength 383 | } 384 | 385 | func (ctx *Context) ResponseHeader() http.Header { 386 | return ctx.responseHeader 387 | } 388 | 389 | func (ctx *Context) ResponseStatus() int { 390 | return ctx.responseStatus 391 | } 392 | 393 | func (ctx *Context) ResponseTime() time.Time { 394 | return ctx.responseTime 395 | } 396 | -------------------------------------------------------------------------------- /pool.go: -------------------------------------------------------------------------------- 1 | package apachelog 2 | 3 | import ( 4 | "bytes" 5 | "sync" 6 | ) 7 | 8 | var logBufferPool sync.Pool 9 | 10 | func init() { 11 | logBufferPool.New = allocLogBuffer 12 | } 13 | 14 | func allocLogBuffer() interface{} { 15 | return &bytes.Buffer{} 16 | } 17 | 18 | func getLogBuffer() *bytes.Buffer { 19 | return logBufferPool.Get().(*bytes.Buffer) 20 | } 21 | 22 | func releaseLogBuffer(v *bytes.Buffer) { 23 | v.Reset() 24 | logBufferPool.Put(v) 25 | } 26 | -------------------------------------------------------------------------------- /v2/format.go: -------------------------------------------------------------------------------- 1 | package apachelog 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | "strconv" 8 | "strings" 9 | "time" 10 | "unicode/utf8" 11 | 12 | strftime "github.com/lestrrat-go/strftime" 13 | "github.com/pkg/errors" 14 | ) 15 | 16 | func (f FormatWriteFunc) WriteTo(dst io.Writer, ctx LogCtx) error { 17 | return f(dst, ctx) 18 | } 19 | 20 | var dashValue = []byte{'-'} 21 | var emptyValue = []byte(nil) 22 | 23 | func valueOf(s string, replacement []byte) []byte { 24 | if s == "" { 25 | return replacement 26 | } 27 | return []byte(s) 28 | } 29 | 30 | type fixedByteSequence []byte 31 | 32 | func (seq fixedByteSequence) WriteTo(dst io.Writer, _ LogCtx) error { 33 | if _, err := dst.Write([]byte(seq)); err != nil { 34 | return errors.Wrapf(err, "failed to write fixed byte sequence %s", seq) 35 | } 36 | return nil 37 | } 38 | 39 | type requestHeader string 40 | 41 | func (h requestHeader) WriteTo(dst io.Writer, ctx LogCtx) error { 42 | v := ctx.Request().Header.Get(string(h)) 43 | if _, err := dst.Write(valueOf(v, dashValue)); err != nil { 44 | return errors.Wrap(err, "failed to write request header value") 45 | } 46 | return nil 47 | } 48 | 49 | type responseHeader string 50 | 51 | func (h responseHeader) WriteTo(dst io.Writer, ctx LogCtx) error { 52 | v := ctx.ResponseHeader().Get(string(h)) 53 | if _, err := dst.Write(valueOf(v, dashValue)); err != nil { 54 | return errors.Wrap(err, "failed to write response header value") 55 | } 56 | return nil 57 | } 58 | 59 | func makeRequestTimeBegin(s string) (FormatWriter, error) { 60 | f, err := strftime.New(s) 61 | if err != nil { 62 | return nil, errors.Wrap(err, `failed to compile strftime pattern`) 63 | } 64 | 65 | return FormatWriteFunc(func(dst io.Writer, ctx LogCtx) error { 66 | return f.Format(dst, ctx.RequestTime()) 67 | }), nil 68 | } 69 | 70 | func makeRequestTimeEnd(s string) (FormatWriter, error) { 71 | f, err := strftime.New(s) 72 | if err != nil { 73 | return nil, errors.Wrap(err, `failed to compile strftime pattern`) 74 | } 75 | 76 | return FormatWriteFunc(func(dst io.Writer, ctx LogCtx) error { 77 | return f.Format(dst, ctx.ResponseTime()) 78 | }), nil 79 | } 80 | 81 | func elapsedFormatter(key string) (FormatWriter, error) { 82 | switch key { 83 | case "ms": 84 | return elapsedTimeMilliSeconds, nil 85 | case "us": 86 | return elapsedTimeMicroSeconds, nil 87 | case "s": 88 | return elapsedTimeSeconds, nil 89 | default: 90 | return nil, fmt.Errorf("unrecognised elapsed time unit: %s", key) 91 | } 92 | } 93 | 94 | func timeFormatter(key string) (FormatWriter, error) { 95 | var formatter FormatWriter 96 | switch key { 97 | case "sec": 98 | formatter = requestTimeSecondsSinceEpoch 99 | case "msec": 100 | formatter = requestTimeMillisecondsSinceEpoch 101 | case "usec": 102 | formatter = requestTimeMicrosecondsSinceEpoch 103 | case "msec_frac": 104 | formatter = requestTimeMillisecondsFracSinceEpoch 105 | case "usec_frac": 106 | formatter = requestTimeMicrosecondsFracSinceEpoch 107 | default: 108 | const beginPrefix = "begin:" 109 | const endPrefix = "end:" 110 | if strings.HasPrefix(key, beginPrefix) { 111 | return makeRequestTimeBegin(key[len(beginPrefix):]) 112 | } else if strings.HasPrefix(key, endPrefix) { 113 | return makeRequestTimeEnd(key[len(endPrefix):]) 114 | } 115 | // if specify the format of strftime(3) without begin: or end:, same as bigin: 116 | // FYI https://httpd.apache.org/docs/current/en/mod/mod_log_config.html 117 | return makeRequestTimeBegin(key) 118 | } 119 | return formatter, nil 120 | } 121 | 122 | var epoch = time.Unix(0, 0) 123 | 124 | func makeRequestTimeSinceEpoch(base time.Duration) FormatWriter { 125 | return FormatWriteFunc(func(dst io.Writer, ctx LogCtx) error { 126 | dur := ctx.RequestTime().Sub(epoch) 127 | s := strconv.FormatInt(dur.Nanoseconds()/int64(base), 10) 128 | if _, err := dst.Write(valueOf(s, dashValue)); err != nil { 129 | return errors.Wrap(err, `failed to write request time since epoch`) 130 | } 131 | return nil 132 | }) 133 | } 134 | 135 | func makeRequestTimeFracSinceEpoch(base time.Duration) FormatWriter { 136 | return FormatWriteFunc(func(dst io.Writer, ctx LogCtx) error { 137 | dur := ctx.RequestTime().Sub(epoch) 138 | 139 | s := fmt.Sprintf("%g", float64(dur.Nanoseconds()%int64(base*1000))/float64(base)) 140 | if _, err := dst.Write(valueOf(s, dashValue)); err != nil { 141 | return errors.Wrap(err, `failed to write request time since epoch`) 142 | } 143 | return nil 144 | }) 145 | } 146 | 147 | func makeElapsedTime(base time.Duration, fraction int) FormatWriter { 148 | return FormatWriteFunc(func(dst io.Writer, ctx LogCtx) error { 149 | var str string 150 | if elapsed := ctx.ElapsedTime(); elapsed > 0 { 151 | switch fraction { 152 | case timeNotFraction: 153 | str = strconv.Itoa(int(elapsed / base)) 154 | case timeMicroFraction: 155 | str = fmt.Sprintf("%03d", int((elapsed%time.Millisecond)/base)) 156 | case timeMilliFraction: 157 | str = fmt.Sprintf("%03d", int((elapsed%time.Second)/base)) 158 | default: 159 | return errors.New("failed to write elapsed time") 160 | } 161 | } 162 | if _, err := dst.Write(valueOf(str, dashValue)); err != nil { 163 | return errors.Wrap(err, "failed to write elapsed time") 164 | } 165 | return nil 166 | }) 167 | } 168 | 169 | const ( 170 | timeNotFraction = iota 171 | timeMicroFraction 172 | timeMilliFraction 173 | ) 174 | 175 | var ( 176 | elapsedTimeMicroSeconds = makeElapsedTime(time.Microsecond, timeNotFraction) 177 | elapsedTimeMilliSeconds = makeElapsedTime(time.Millisecond, timeNotFraction) 178 | elapsedTimeSeconds = makeElapsedTime(time.Second, timeNotFraction) 179 | requestTimeMicrosecondsFracSinceEpoch = makeRequestTimeFracSinceEpoch(time.Microsecond) 180 | requestTimeMillisecondsFracSinceEpoch = makeRequestTimeFracSinceEpoch(time.Millisecond) 181 | requestTimeSecondsSinceEpoch = makeRequestTimeSinceEpoch(time.Second) 182 | requestTimeMillisecondsSinceEpoch = makeRequestTimeSinceEpoch(time.Millisecond) 183 | requestTimeMicrosecondsSinceEpoch = makeRequestTimeSinceEpoch(time.Microsecond) 184 | ) 185 | 186 | var requestHttpMethod = FormatWriteFunc(func(dst io.Writer, ctx LogCtx) error { 187 | v := valueOf(ctx.Request().Method, emptyValue) 188 | if _, err := dst.Write(v); err != nil { 189 | return errors.Wrap(err, "failed to write request method") 190 | } 191 | return nil 192 | }) 193 | 194 | var requestHttpProto = FormatWriteFunc(func(dst io.Writer, ctx LogCtx) error { 195 | v := valueOf(ctx.Request().Proto, emptyValue) 196 | if _, err := dst.Write(v); err != nil { 197 | return errors.Wrap(err, "failed to write request HTTP request proto") 198 | } 199 | return nil 200 | }) 201 | 202 | var requestRemoteAddr = FormatWriteFunc(func(dst io.Writer, ctx LogCtx) error { 203 | addr := ctx.Request().RemoteAddr 204 | if i := strings.LastIndexByte(addr, ':'); i > -1 { 205 | addr = addr[:i] 206 | } 207 | v := valueOf(addr, dashValue) 208 | if _, err := dst.Write(v); err != nil { 209 | return errors.Wrap(err, "failed to write request remote address") 210 | } 211 | return nil 212 | }) 213 | 214 | var pid = FormatWriteFunc(func(dst io.Writer, ctx LogCtx) error { 215 | v := valueOf(strconv.Itoa(os.Getpid()), emptyValue) 216 | if _, err := dst.Write(v); err != nil { 217 | return errors.Wrap(err, "failed to write pid") 218 | } 219 | return nil 220 | }) 221 | 222 | var rawQuery = FormatWriteFunc(func(dst io.Writer, ctx LogCtx) error { 223 | q := ctx.Request().URL.RawQuery 224 | if q != "" { 225 | q = "?" + q 226 | } 227 | v := valueOf(q, emptyValue) 228 | if _, err := dst.Write(v); err != nil { 229 | return errors.Wrap(err, "failed to write raw request query") 230 | } 231 | return nil 232 | }) 233 | 234 | var requestLine = FormatWriteFunc(func(dst io.Writer, ctx LogCtx) error { 235 | buf := getLogBuffer() 236 | defer releaseLogBuffer(buf) 237 | 238 | r := ctx.Request() 239 | buf.WriteString(r.Method) 240 | buf.WriteByte(' ') 241 | buf.WriteString(r.URL.String()) 242 | buf.WriteByte(' ') 243 | buf.WriteString(r.Proto) 244 | if _, err := buf.WriteTo(dst); err != nil { 245 | return errors.Wrap(err, "failed to write request line") 246 | } 247 | return nil 248 | }) 249 | 250 | var httpStatus = FormatWriteFunc(func(dst io.Writer, ctx LogCtx) error { 251 | var s string 252 | if st := ctx.ResponseStatus(); st != 0 { // can't really happen, but why not 253 | s = strconv.Itoa(st) 254 | } 255 | if _, err := dst.Write(valueOf(s, emptyValue)); err != nil { 256 | return errors.Wrap(err, "failed to write response status") 257 | } 258 | return nil 259 | }) 260 | 261 | var requestTime = FormatWriteFunc(func(dst io.Writer, ctx LogCtx) error { 262 | v := valueOf(ctx.RequestTime().Format("[02/Jan/2006:15:04:05 -0700]"), []byte{'[', ']'}) 263 | if _, err := dst.Write(v); err != nil { 264 | return errors.Wrap(err, "failed to write request time") 265 | } 266 | return nil 267 | }) 268 | 269 | var urlPath = FormatWriteFunc(func(dst io.Writer, ctx LogCtx) error { 270 | v := valueOf(ctx.Request().URL.Path, emptyValue) 271 | if _, err := dst.Write(v); err != nil { 272 | return errors.Wrap(err, "failed to write request URL path") 273 | } 274 | return nil 275 | }) 276 | 277 | var username = FormatWriteFunc(func(dst io.Writer, ctx LogCtx) error { 278 | var v = dashValue 279 | if u := ctx.Request().URL.User; u != nil { 280 | v = valueOf(u.Username(), dashValue) 281 | } 282 | if _, err := dst.Write(v); err != nil { 283 | return errors.Wrap(err, "failed to write username") 284 | } 285 | return nil 286 | }) 287 | 288 | var requestHost = FormatWriteFunc(func(dst io.Writer, ctx LogCtx) error { 289 | var v []byte 290 | h := ctx.Request().Host 291 | if strings.IndexByte(h, ':') > 0 { 292 | v = valueOf(strings.Split(ctx.Request().Host, ":")[0], dashValue) 293 | } else { 294 | v = valueOf(h, dashValue) 295 | } 296 | if _, err := dst.Write(v); err != nil { 297 | return errors.Wrap(err, "failed to write request host") 298 | } 299 | return nil 300 | }) 301 | 302 | var responseContentLength = FormatWriteFunc(func(dst io.Writer, ctx LogCtx) error { 303 | var s string 304 | if cl := ctx.ResponseContentLength(); cl != 0 { 305 | s = strconv.FormatInt(cl, 10) 306 | } 307 | _, err := dst.Write(valueOf(s, dashValue)) 308 | return err 309 | }) 310 | 311 | func makeEnvVar(key string) FormatWriter { 312 | return FormatWriteFunc(func(dst io.Writer, ctx LogCtx) error { 313 | _, err := dst.Write(valueOf(os.Getenv(key), dashValue)) 314 | return err 315 | }) 316 | } 317 | 318 | func (f *Format) compile(s string) error { 319 | var cbs []FormatWriter 320 | 321 | start := 0 322 | max := len(s) 323 | 324 | for i := 0; i < max; { 325 | r, n := utf8.DecodeRuneInString(s[i:]) 326 | if r == utf8.RuneError { 327 | return errors.Wrap(ErrInvalidRuneSequence, "failed to compile format") 328 | } 329 | i += n 330 | 331 | // Not q sequence... go to next rune 332 | if r != '%' { 333 | continue 334 | } 335 | 336 | if start != i { 337 | // this *could* be the last element in string, in which case we just 338 | // say meh, just assume this was a literal percent. 339 | if i == max { 340 | cbs = append(cbs, fixedByteSequence(s[start:i])) 341 | start = i 342 | break 343 | } 344 | cbs = append(cbs, fixedByteSequence(s[start:i-1])) 345 | } 346 | 347 | // Find what we have next. 348 | 349 | r, n = utf8.DecodeRuneInString(s[i:]) 350 | if r == utf8.RuneError { 351 | return errors.Wrap(ErrInvalidRuneSequence, "failed to compile format") 352 | } 353 | i += n 354 | 355 | switch r { 356 | case '%': 357 | cbs = append(cbs, fixedByteSequence([]byte{'%'})) 358 | start = i + n - 1 359 | case 'b': 360 | cbs = append(cbs, responseContentLength) 361 | start = i + n - 1 362 | case 'D': // custom 363 | cbs = append(cbs, elapsedTimeMicroSeconds) 364 | start = i + n - 1 365 | case 'h': 366 | cbs = append(cbs, requestRemoteAddr) 367 | start = i + n - 1 368 | case 'H': 369 | cbs = append(cbs, requestHttpProto) 370 | start = i + n - 1 371 | case 'l': 372 | cbs = append(cbs, fixedByteSequence(dashValue)) 373 | start = i + n - 1 374 | case 'm': 375 | cbs = append(cbs, requestHttpMethod) 376 | start = i + n - 1 377 | case 'p': 378 | cbs = append(cbs, pid) 379 | start = i + n - 1 380 | case 'P': 381 | // Unimplemented 382 | return errors.Wrap(ErrUnimplemented, "failed to compile format") 383 | case 'q': 384 | cbs = append(cbs, rawQuery) 385 | start = i + n - 1 386 | case 'r': 387 | cbs = append(cbs, requestLine) 388 | start = i + n - 1 389 | case 's': 390 | cbs = append(cbs, httpStatus) 391 | start = i + n - 1 392 | case 't': 393 | cbs = append(cbs, requestTime) 394 | start = i + n - 1 395 | case 'T': // custom 396 | cbs = append(cbs, elapsedTimeSeconds) 397 | start = i + n - 1 398 | case 'u': 399 | cbs = append(cbs, username) 400 | start = i + n - 1 401 | case 'U': 402 | cbs = append(cbs, urlPath) 403 | start = i + n - 1 404 | case 'V', 'v': 405 | cbs = append(cbs, requestHost) 406 | start = i + n - 1 407 | case '>': 408 | if max >= i && s[i] == 's' { 409 | // "Last" status doesn't exist in our case, so it's the same as %s 410 | cbs = append(cbs, httpStatus) 411 | start = i + 1 412 | i = i + 1 413 | } else { 414 | // Otherwise we don't know what this is. just do a verbatim copy 415 | cbs = append(cbs, fixedByteSequence([]byte{'%', '>'})) 416 | start = i + n - 1 417 | } 418 | case '{': 419 | // Search the next } 420 | end := -1 421 | for j := i; j < max; j++ { 422 | if s[j] == '}' { 423 | end = j 424 | break 425 | } 426 | } 427 | 428 | if end != -1 && end < max-1 { // Found it! 429 | // check for suffix 430 | blockType := s[end+1] 431 | key := s[i:end] 432 | switch blockType { 433 | case 'e': // environment variables 434 | cbs = append(cbs, makeEnvVar(key)) 435 | case 'i': 436 | cbs = append(cbs, requestHeader(key)) 437 | case 'o': 438 | cbs = append(cbs, responseHeader(key)) 439 | case 't': 440 | // The time, in the form given by format, which should be in an 441 | // extended strftime(3) format (potentially localized). If the 442 | // format starts with begin: (default) the time is taken at the 443 | // beginning of the request processing. If it starts with end: 444 | // it is the time when the log entry gets written, close to the 445 | // end of the request processing. In addition to the formats 446 | // supported by strftime(3), the following format tokens are 447 | // supported: 448 | // 449 | // sec number of seconds since the Epoch 450 | // msec number of milliseconds since the Epoch 451 | // usec number of microseconds since the Epoch 452 | // msec_frac millisecond fraction 453 | // usec_frac microsecond fraction 454 | // 455 | // These tokens can not be combined with each other or strftime(3) 456 | // formatting in the same format string. You can use multiple 457 | // %{format}t tokens instead. 458 | formatter, err := timeFormatter(key) 459 | if err != nil { 460 | return err 461 | } 462 | cbs = append(cbs, formatter) 463 | case 'T': 464 | formatter, err := elapsedFormatter(key) 465 | if err != nil { 466 | return err 467 | } 468 | cbs = append(cbs, formatter) 469 | default: 470 | return errors.Wrap(ErrUnimplemented, "failed to compile format") 471 | } 472 | 473 | start = end + 2 474 | i = end + 1 475 | } else { 476 | cbs = append(cbs, fixedByteSequence([]byte{'%', '{'})) 477 | start = i + n - 1 478 | } 479 | } 480 | } 481 | 482 | if start < max { 483 | cbs = append(cbs, fixedByteSequence(s[start:max])) 484 | } 485 | 486 | f.writers = cbs 487 | return nil 488 | } 489 | 490 | func (f *Format) WriteTo(dst io.Writer, ctx LogCtx) error { 491 | for _, w := range f.writers { 492 | if err := w.WriteTo(dst, ctx); err != nil { 493 | return errors.Wrap(err, "failed to execute FormatWriter") 494 | } 495 | } 496 | return nil 497 | } 498 | -------------------------------------------------------------------------------- /v2/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/lestrrat-go/apache-logformat/v2 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a 7 | github.com/lestrrat-go/strftime v1.0.4 8 | github.com/pkg/errors v0.8.1 9 | github.com/stretchr/testify v1.3.0 10 | ) 11 | -------------------------------------------------------------------------------- /v2/interface.go: -------------------------------------------------------------------------------- 1 | package apachelog 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "net/http" 7 | "time" 8 | ) 9 | 10 | type ApacheLog struct { 11 | format *Format 12 | } 13 | 14 | // Combined is a pre-defined ApacheLog struct to log "common" log format 15 | var CommonLog, _ = New(`%h %l %u %t "%r" %>s %b`) 16 | 17 | // Combined is a pre-defined ApacheLog struct to log "combined" log format 18 | var CombinedLog, _ = New(`%h %l %u %t "%r" %>s %b "%{Referer}i" "%{User-agent}i"`) 19 | 20 | var ( 21 | ErrInvalidRuneSequence = errors.New("invalid rune sequence found in format") 22 | ErrUnimplemented = errors.New("pattern unimplemented") 23 | ) 24 | 25 | // Format describes an Apache log format. Given a logging context, 26 | // it can create a log line. 27 | type Format struct { 28 | writers []FormatWriter 29 | } 30 | 31 | type LogCtx interface { 32 | ElapsedTime() time.Duration 33 | Request() *http.Request 34 | RequestTime() time.Time 35 | ResponseContentLength() int64 36 | ResponseHeader() http.Header 37 | ResponseStatus() int 38 | ResponseTime() time.Time 39 | } 40 | 41 | type FormatWriter interface { 42 | WriteTo(io.Writer, LogCtx) error 43 | } 44 | 45 | type FormatWriteFunc func(io.Writer, LogCtx) error 46 | -------------------------------------------------------------------------------- /v2/internal/httputil/httputil.go: -------------------------------------------------------------------------------- 1 | package httputil 2 | 3 | import ( 4 | "net/http" 5 | "sync" 6 | ) 7 | 8 | var responseWriterPool sync.Pool 9 | 10 | func init() { 11 | responseWriterPool.New = allocResponseWriter 12 | } 13 | 14 | type ResponseWriter struct { 15 | responseContentLength int64 16 | responseStatus int 17 | responseWriter http.ResponseWriter 18 | } 19 | 20 | func GetResponseWriter(w http.ResponseWriter) *ResponseWriter { 21 | rw := responseWriterPool.Get().(*ResponseWriter) 22 | rw.responseWriter = w 23 | return rw 24 | } 25 | 26 | func ReleaseResponseWriter(rw *ResponseWriter) { 27 | rw.Reset() 28 | responseWriterPool.Put(rw) 29 | } 30 | 31 | func allocResponseWriter() interface{} { 32 | rw := &ResponseWriter{} 33 | rw.Reset() 34 | return rw 35 | } 36 | 37 | func (rw ResponseWriter) ContentLength() int64 { 38 | return rw.responseContentLength 39 | } 40 | 41 | func (rw ResponseWriter) StatusCode() int { 42 | return rw.responseStatus 43 | } 44 | 45 | func (rw *ResponseWriter) Reset() { 46 | rw.responseContentLength = 0 47 | rw.responseStatus = http.StatusOK 48 | rw.responseWriter = nil 49 | } 50 | 51 | func (rw *ResponseWriter) Write(buf []byte) (int, error) { 52 | n, err := rw.responseWriter.Write(buf) 53 | rw.responseContentLength += int64(n) 54 | return n, err 55 | } 56 | 57 | func (rw *ResponseWriter) Header() http.Header { 58 | return rw.responseWriter.Header() 59 | } 60 | 61 | func (rw *ResponseWriter) WriteHeader(status int) { 62 | rw.responseStatus = status 63 | rw.responseWriter.WriteHeader(status) 64 | } 65 | 66 | func (rw *ResponseWriter) Flush() { 67 | if f, ok := rw.responseWriter.(http.Flusher); ok { 68 | f.Flush() 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /v2/internal/logctx/logctx.go: -------------------------------------------------------------------------------- 1 | package logctx 2 | 3 | import ( 4 | "net/http" 5 | "sync" 6 | "time" 7 | 8 | "github.com/lestrrat-go/apache-logformat/v2/internal/httputil" 9 | ) 10 | 11 | type clock interface { 12 | Now() time.Time 13 | } 14 | 15 | type defaultClock struct{} 16 | 17 | func (_ defaultClock) Now() time.Time { 18 | return time.Now() 19 | } 20 | 21 | var Clock clock = defaultClock{} 22 | 23 | type Context struct { 24 | elapsedTime time.Duration 25 | request *http.Request 26 | requestTime time.Time 27 | responseContentLength int64 28 | responseHeader http.Header 29 | responseStatus int 30 | responseTime time.Time 31 | } 32 | 33 | var pool = sync.Pool{New: allocCtx} 34 | 35 | func allocCtx() interface{} { 36 | return &Context{} 37 | } 38 | 39 | func Get(r *http.Request) *Context { 40 | ctx := pool.Get().(*Context) 41 | ctx.request = r 42 | ctx.requestTime = Clock.Now() 43 | return ctx 44 | } 45 | 46 | func Release(ctx *Context) { 47 | ctx.Reset() 48 | pool.Put(ctx) 49 | } 50 | 51 | func (ctx *Context) ElapsedTime() time.Duration { 52 | return ctx.elapsedTime 53 | } 54 | 55 | func (ctx *Context) Request() *http.Request { 56 | return ctx.request 57 | } 58 | 59 | func (ctx *Context) RequestTime() time.Time { 60 | return ctx.requestTime 61 | } 62 | 63 | func (ctx *Context) ResponseContentLength() int64 { 64 | return ctx.responseContentLength 65 | } 66 | 67 | func (ctx *Context) ResponseHeader() http.Header { 68 | return ctx.responseHeader 69 | } 70 | 71 | func (ctx *Context) ResponseStatus() int { 72 | return ctx.responseStatus 73 | } 74 | 75 | func (ctx *Context) ResponseTime() time.Time { 76 | return ctx.responseTime 77 | } 78 | 79 | func (ctx *Context) Reset() { 80 | ctx.elapsedTime = time.Duration(0) 81 | ctx.request = nil 82 | ctx.requestTime = time.Time{} 83 | ctx.responseContentLength = 0 84 | ctx.responseHeader = http.Header{} 85 | ctx.responseStatus = http.StatusOK 86 | ctx.responseTime = time.Time{} 87 | } 88 | 89 | func (ctx *Context) Finalize(wrapped *httputil.ResponseWriter) { 90 | ctx.responseTime = Clock.Now() 91 | ctx.elapsedTime = ctx.responseTime.Sub(ctx.requestTime) 92 | ctx.responseContentLength = wrapped.ContentLength() 93 | ctx.responseHeader = wrapped.Header() 94 | ctx.responseStatus = wrapped.StatusCode() 95 | } 96 | -------------------------------------------------------------------------------- /v2/internal_test.go: -------------------------------------------------------------------------------- 1 | package apachelog 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "io/ioutil" 8 | "net/http" 9 | "testing" 10 | "time" 11 | 12 | "github.com/lestrrat-go/apache-logformat/v2/internal/httputil" 13 | "github.com/lestrrat-go/apache-logformat/v2/internal/logctx" 14 | "github.com/stretchr/testify/assert" 15 | "net/http/httptest" 16 | ) 17 | 18 | func isDash(t *testing.T, s string) bool { 19 | return assert.Equal(t, "-", s, "expected dash") 20 | } 21 | 22 | func isEmpty(t *testing.T, s string) bool { 23 | return assert.Equal(t, "", s, "expected dash") 24 | } 25 | 26 | func TestInternalDashEmpty(t *testing.T) { 27 | f := func(t *testing.T, name string, dash bool, f FormatWriter) { 28 | var buf bytes.Buffer 29 | r, _ := http.NewRequest(http.MethodGet, "http://example.com", nil) 30 | r.Host = "" 31 | r.Method = "" 32 | r.Proto = "" 33 | ctx := logctx.Get(r) 34 | 35 | t.Run(fmt.Sprintf("%s (dash=%t)", name, dash), func(t *testing.T) { 36 | if !assert.NoError(t, f.WriteTo(&buf, ctx), "callback should succeed") { 37 | return 38 | } 39 | if dash { 40 | isDash(t, buf.String()) 41 | } else { 42 | isEmpty(t, buf.String()) 43 | } 44 | }) 45 | } 46 | 47 | type dashEmptyCase struct { 48 | Name string 49 | Dash bool 50 | Format FormatWriter 51 | } 52 | cases := []dashEmptyCase{ 53 | {Name: "Request Header", Dash: true, Format: requestHeader("foo")}, 54 | {Name: "Response Header", Dash: true, Format: responseHeader("foo")}, 55 | {Name: "Request Method", Dash: false, Format: requestHttpMethod}, 56 | {Name: "Request Proto", Dash: false, Format: requestHttpProto}, 57 | {Name: "Request RemoteAddr", Dash: true, Format: requestRemoteAddr}, 58 | {Name: "Request Raw Query", Dash: false, Format: rawQuery}, 59 | {Name: "Response Status", Dash: false, Format: httpStatus}, 60 | {Name: "Request Username", Dash: true, Format: username}, 61 | {Name: "Request Host", Dash: true, Format: requestHost}, 62 | {Name: "Response ContentLength", Dash: true, Format: responseContentLength}, 63 | } 64 | 65 | for _, c := range cases { 66 | f(t, c.Name, c.Dash, c.Format) 67 | } 68 | } 69 | 70 | func TestResponseWriterDefaultStatusCode(t *testing.T) { 71 | writer := httptest.NewRecorder() 72 | uut := httputil.GetResponseWriter(writer) 73 | if uut.StatusCode() != http.StatusOK { 74 | t.Fail() 75 | } 76 | } 77 | 78 | func TestFlusherInterface(t *testing.T) { 79 | var rw httputil.ResponseWriter 80 | var f http.Flusher = &rw 81 | _ = f 82 | } 83 | 84 | func TestFlusher(t *testing.T) { 85 | lines := []string{ 86 | "Hello, World!", 87 | "Hello again, World!", 88 | } 89 | 90 | // We need to synchronize the reads with the writes of each chunk 91 | sync := make(chan struct{}) 92 | 93 | handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 94 | for _, line := range lines { 95 | if _, err := w.Write([]byte(line)); err != nil { 96 | return 97 | } 98 | if f, ok := w.(http.Flusher); ok { 99 | f.Flush() 100 | } 101 | sync <- struct{}{} 102 | } 103 | close(sync) 104 | }) 105 | 106 | s := httptest.NewServer(CommonLog.Wrap(handler, ioutil.Discard)) 107 | defer s.Close() 108 | 109 | req, err := http.NewRequest("GET", s.URL, nil) 110 | if !assert.NoError(t, err, "request creation should succeed") { 111 | return 112 | } 113 | 114 | // If it isn't flushing properly and sending chunks, then the call to 115 | // Do() will hang waiting for the entire body, while the handler will be 116 | // stuck after having written the first line. So have an explicit 117 | // timeout for the read. 118 | timer := time.NewTimer(time.Second) 119 | done := make(chan struct{}) 120 | go func() { 121 | defer close(done) 122 | resp, err := http.DefaultClient.Do(req) 123 | if !assert.NoError(t, err, "GET should succeed") { 124 | return 125 | } 126 | defer resp.Body.Close() 127 | 128 | buf := make([]byte, 64) 129 | var i int 130 | for { 131 | _, ok := <-sync 132 | if !ok { 133 | break 134 | } 135 | n, err := resp.Body.Read(buf) 136 | if n == 0 { 137 | if !assert.Equal(t, len(lines)-1, i-1, "wrong number of chunks") { 138 | return 139 | } 140 | break 141 | } 142 | t.Logf("Response body %d: %d %s", i, n, buf[:n]) 143 | if !assert.Equal(t, []byte(lines[i]), buf[:n], "wrong response body") { 144 | return 145 | } 146 | if err == io.EOF { 147 | if !assert.Equal(t, len(lines)-1, i, "wrong number of chunks") { 148 | return 149 | } 150 | break 151 | } 152 | if !assert.NoError(t, err, "Body read should succeed") { 153 | return 154 | } 155 | i++ 156 | } 157 | }() 158 | 159 | select { 160 | case <-timer.C: 161 | close(sync) 162 | t.Fatal("timed out: not flushing properly?") 163 | case <-done: 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /v2/logformat.go: -------------------------------------------------------------------------------- 1 | package apachelog 2 | 3 | import ( 4 | "io" 5 | "net/http" 6 | "os" 7 | 8 | "github.com/lestrrat-go/apache-logformat/v2/internal/httputil" 9 | "github.com/lestrrat-go/apache-logformat/v2/internal/logctx" 10 | "github.com/pkg/errors" 11 | ) 12 | 13 | // New creates a new ApacheLog instance from the given 14 | // format. It will return an error if the format fails to compile. 15 | func New(format string) (*ApacheLog, error) { 16 | var f Format 17 | if err := f.compile(format); err != nil { 18 | return nil, errors.Wrap(err, "failed to compile log format") 19 | } 20 | 21 | return &ApacheLog{format: &f}, nil 22 | } 23 | 24 | // WriteLog generates a log line using the format associated with the 25 | // ApacheLog instance, using the values from ctx. The result is written 26 | // to dst 27 | func (al *ApacheLog) WriteLog(dst io.Writer, ctx LogCtx) error { 28 | buf := getLogBuffer() 29 | defer releaseLogBuffer(buf) 30 | 31 | if err := al.format.WriteTo(buf, ctx); err != nil { 32 | return errors.Wrap(err, "failed to format log line") 33 | } 34 | 35 | b := buf.Bytes() 36 | if b[len(b)-1] != '\n' { 37 | buf.WriteByte('\n') 38 | } 39 | 40 | if _, err := buf.WriteTo(dst); err != nil { 41 | return errors.Wrap(err, "failed to write formated line to destination") 42 | } 43 | return nil 44 | } 45 | 46 | // Wrap creates a new http.Handler that logs a formatted log line 47 | // to dst. 48 | func (al *ApacheLog) Wrap(h http.Handler, dst io.Writer) http.Handler { 49 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 50 | ctx := logctx.Get(r) 51 | defer logctx.Release(ctx) 52 | 53 | wrapped := httputil.GetResponseWriter(w) 54 | defer httputil.ReleaseResponseWriter(wrapped) 55 | 56 | defer func() { 57 | ctx.Finalize(wrapped) 58 | if err := al.WriteLog(dst, ctx); err != nil { 59 | // Hmmm... no where to log except for stderr 60 | os.Stderr.Write([]byte(err.Error())) 61 | return 62 | } 63 | }() 64 | 65 | h.ServeHTTP(wrapped, r) 66 | }) 67 | } 68 | -------------------------------------------------------------------------------- /v2/logformat_test.go: -------------------------------------------------------------------------------- 1 | package apachelog_test 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | "net/http/httptest" 9 | "os" 10 | "strconv" 11 | "strings" 12 | "testing" 13 | "time" 14 | 15 | "github.com/facebookgo/clock" 16 | apachelog "github.com/lestrrat-go/apache-logformat/v2" 17 | "github.com/lestrrat-go/apache-logformat/v2/internal/logctx" 18 | strftime "github.com/lestrrat-go/strftime" 19 | "github.com/stretchr/testify/assert" 20 | ) 21 | 22 | const message = "Hello, World!" 23 | 24 | var hello = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 25 | w.Header().Set("Content-Type", "text/plain") 26 | w.WriteHeader(http.StatusOK) 27 | fmt.Fprint(w, message) 28 | }) 29 | 30 | func TestBasic(t *testing.T) { 31 | r, err := http.NewRequest("GET", "http://golang.org", nil) 32 | if err != nil { 33 | t.Errorf("Failed to create request: %s", err) 34 | } 35 | r.RemoteAddr = "127.0.0.1" 36 | r.Header.Set("User-Agent", "Apache-LogFormat Port In Golang") 37 | r.Header.Set("Referer", "http://dummy.com") 38 | 39 | var out bytes.Buffer 40 | h := apachelog.CombinedLog.Wrap(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 41 | _, _ = w.Write([]byte("Hello, World!")) 42 | }), &out) 43 | 44 | w := httptest.NewRecorder() 45 | h.ServeHTTP(w, r) 46 | 47 | t.Logf("output = %s", strconv.Quote(out.String())) 48 | } 49 | 50 | func newServer(l *apachelog.ApacheLog, h http.Handler, out io.Writer) *httptest.Server { 51 | return httptest.NewServer(l.Wrap(h, out)) 52 | } 53 | 54 | func testLog(t *testing.T, pattern, expected string, h http.Handler, modifyURL func(string) string, modifyRequest func(*http.Request)) { 55 | l, err := apachelog.New(pattern) 56 | if !assert.NoError(t, err, "apachelog.New should succeed") { 57 | return 58 | } 59 | 60 | var buf bytes.Buffer 61 | s := newServer(l, h, &buf) 62 | defer s.Close() 63 | 64 | u := s.URL 65 | if modifyURL != nil { 66 | u = modifyURL(u) 67 | } 68 | 69 | r, err := http.NewRequest("GET", u, nil) 70 | if !assert.NoError(t, err, "request creation should succeed") { 71 | return 72 | } 73 | 74 | if modifyRequest != nil { 75 | modifyRequest(r) 76 | } 77 | 78 | _, err = http.DefaultClient.Do(r) 79 | if !assert.NoError(t, err, "GET should succeed") { 80 | return 81 | } 82 | 83 | if !assert.Equal(t, expected, buf.String()) { 84 | return 85 | } 86 | } 87 | 88 | func TestVerbatim(t *testing.T) { 89 | testLog(t, 90 | "This should be a verbatim percent sign -> %%", 91 | "This should be a verbatim percent sign -> %\n", 92 | hello, 93 | nil, 94 | nil, 95 | ) 96 | } 97 | 98 | func TestResponseHeader(t *testing.T) { 99 | testLog(t, 100 | "%{X-Req-Header}i %{X-Resp-Header}o", 101 | "Gimme a response! Here's your response\n", 102 | http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 103 | w.Header().Add("X-Resp-Header", "Here's your response") 104 | }), 105 | nil, 106 | func(r *http.Request) { 107 | r.Header.Set("X-Req-Header", "Gimme a response!") 108 | }, 109 | ) 110 | } 111 | 112 | func TestQuery(t *testing.T) { 113 | testLog(t, 114 | `%m %U %q %H`, 115 | "GET /foo ?bar=baz HTTP/1.1\n", 116 | hello, 117 | func(u string) string { 118 | return u + "/foo?bar=baz" 119 | }, 120 | nil, 121 | ) 122 | } 123 | 124 | func TestTime(t *testing.T) { 125 | o := logctx.Clock 126 | defer func() { logctx.Clock = o }() 127 | 128 | const longTimeAgo = 233431200 * time.Second 129 | const pattern = `%Y-%m-%d` 130 | 131 | f, _ := strftime.New(pattern) 132 | cl := clock.NewMock() 133 | cl.Add(longTimeAgo) 134 | logctx.Clock = cl 135 | 136 | // Mental note: %{[mu]?sec}t should (milli|micro)?seconds since the epoch. 137 | testLog(t, 138 | fmt.Sprintf( 139 | `%%T %%D %%{sec}t %%{msec}t %%{usec}t %%{begin:%s}t %%{end:%s}t %%{%s}t`, 140 | pattern, 141 | pattern, 142 | pattern, 143 | ), 144 | fmt.Sprintf( 145 | "1 1000000 %d %d %d %s %s %s\n", 146 | longTimeAgo/time.Second, 147 | longTimeAgo/time.Millisecond, 148 | longTimeAgo/time.Microsecond, 149 | f.FormatString(cl.Now()), 150 | f.FormatString(cl.Now().Add(time.Second)), 151 | f.FormatString(cl.Now()), 152 | ), 153 | http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 154 | cl.Add(time.Second) 155 | }), 156 | nil, 157 | nil, 158 | ) 159 | } 160 | 161 | func TestElapsed(t *testing.T) { 162 | o := logctx.Clock 163 | defer func() { logctx.Clock = o }() 164 | cl := clock.NewMock() 165 | logctx.Clock = cl 166 | testLog(t, `%T %D %{s}T %{us}T %{ms}T`, 167 | "3 3141592 3 3141592 3141\n", 168 | http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 169 | cl.Add(3141592 * time.Microsecond) 170 | }), 171 | nil, 172 | nil, 173 | ) 174 | 175 | _, err := apachelog.New(`%{h}T`) 176 | assert.EqualError(t, err, "failed to compile log format: unrecognised elapsed time unit: h") 177 | } 178 | 179 | func TestElapsedTimeFraction(t *testing.T) { 180 | o := logctx.Clock 181 | defer func() { logctx.Clock = o }() 182 | 183 | cl := clock.NewMock() 184 | cl.Add(time.Second + time.Millisecond*200 + time.Microsecond*90) 185 | logctx.Clock = cl 186 | testLog(t, 187 | `%{msec_frac}t %{usec_frac}t`, 188 | "200.09 90\n", 189 | http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}), 190 | nil, 191 | nil, 192 | ) 193 | } 194 | 195 | func TestStrayPercent(t *testing.T) { 196 | testLog(t, 197 | `stray percent at the end: %`, 198 | "stray percent at the end: %\n", 199 | hello, 200 | nil, 201 | nil, 202 | ) 203 | } 204 | 205 | func TestMissingClosingBrace(t *testing.T) { 206 | testLog(t, 207 | `Missing closing brace: %{Test <- this should be verbatim`, 208 | "Missing closing brace: %{Test <- this should be verbatim\n", 209 | hello, 210 | nil, 211 | nil, 212 | ) 213 | } 214 | 215 | func TestPercentS(t *testing.T) { 216 | // %s and %>s should be the same in our case 217 | testLog(t, 218 | `%s = %>s`, 219 | "404 = 404\n", 220 | http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 221 | w.WriteHeader(http.StatusNotFound) 222 | }), 223 | nil, 224 | nil, 225 | ) 226 | } 227 | 228 | func TestPid(t *testing.T) { 229 | testLog(t, 230 | `%p`, // pid 231 | strconv.Itoa(os.Getpid())+"\n", 232 | hello, 233 | nil, 234 | nil, 235 | ) 236 | } 237 | 238 | func TestUnknownAfterPecentGreaterThan(t *testing.T) { 239 | testLog(t, 240 | `%>X should be verbatim`, // %> followed by unknown char 241 | `%>X should be verbatim`+"\n", 242 | hello, 243 | nil, 244 | nil, 245 | ) 246 | } 247 | 248 | func TestFixedSequence(t *testing.T) { 249 | testLog(t, 250 | `hello, world!`, 251 | "hello, world!\n", 252 | hello, 253 | nil, 254 | nil, 255 | ) 256 | } 257 | 258 | func TestFull(t *testing.T) { 259 | l, err := apachelog.New(`hello, %% %b %D %h %H %l %m %p %q %r %s %t %T %{ms}T %u %U %v %V %>s %{X-LogFormat-Test}i %{X-LogFormat-Test}o world!`) 260 | if !assert.NoError(t, err, "apachelog.New should succeed") { 261 | return 262 | } 263 | 264 | o := logctx.Clock 265 | defer func() { logctx.Clock = o }() 266 | 267 | cl := clock.NewMock() 268 | logctx.Clock = cl 269 | h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 270 | cl.Add(5 * time.Second) 271 | w.Header().Set("X-LogFormat-Test", "Hello, Response!") 272 | w.WriteHeader(http.StatusBadRequest) 273 | }) 274 | var buf bytes.Buffer 275 | s := newServer(l, h, &buf) 276 | defer s.Close() 277 | 278 | r, err := http.NewRequest("GET", s.URL+"/hello_world?hello=world", nil) 279 | if !assert.NoError(t, err, "request creation should succeed") { 280 | return 281 | } 282 | 283 | r.Header.Add("X-LogFormat-Test", "Hello, Request!") 284 | 285 | _, err = http.DefaultClient.Do(r) 286 | if !assert.NoError(t, err, "GET should succeed") { 287 | return 288 | } 289 | 290 | if !assert.Regexp(t, `^hello, % - 5000000 127\.0\.0\.1 HTTP/1\.1 - GET \d+ \?hello=world GET /hello_world\?hello=world HTTP/1\.1 400 \[\d{2}/[a-zA-Z]+/\d{4}:\d{2}:\d{2}:\d{2} [+-]\d{4}\] 5 5000 - /hello_world 127\.0\.0\.1 127\.0\.0\.1 400 Hello, Request! Hello, Response! world!\n$`, buf.String(), "Log line must match") { 291 | return 292 | } 293 | t.Logf("%s", buf.String()) 294 | } 295 | 296 | func TestPercentB(t *testing.T) { 297 | testLog(t, 298 | `%b`, 299 | fmt.Sprintf("%d\n", len(message)), 300 | hello, 301 | nil, 302 | nil, 303 | ) 304 | } 305 | 306 | func TestIPv6RemoteAddr(t *testing.T) { 307 | format := `%h` 308 | expected := "[::1]\n" 309 | 310 | al, err := apachelog.New(format) 311 | if !assert.NoError(t, err, "apachelog.New should succeed") { 312 | return 313 | } 314 | 315 | ctx := &Context{ 316 | request: &http.Request{ 317 | RemoteAddr: "[::1]:51111", 318 | }, 319 | } 320 | 321 | var buf bytes.Buffer 322 | _ = al.WriteLog(&buf, ctx) 323 | 324 | if !assert.Equal(t, expected, buf.String()) { 325 | return 326 | } 327 | } 328 | 329 | func TestEnvironmentVariable(t *testing.T) { 330 | // Well.... let's see. I don't want to change the user's env var, 331 | // so let's just scan for something already present in the environment variable list 332 | 333 | for _, v := range os.Environ() { 334 | vs := strings.SplitN(v, "=", 2) 335 | 336 | t.Logf("Testing environment variable %s", vs[0]) 337 | al, err := apachelog.New(fmt.Sprintf(`%%{%s}e`, vs[0])) 338 | if !assert.NoError(t, err, "apachelog.New should succeed") { 339 | return 340 | } 341 | 342 | var ctx Context 343 | var buf bytes.Buffer 344 | _ = al.WriteLog(&buf, &ctx) 345 | 346 | var expected = "-" 347 | if vs[1] != "" { 348 | expected = vs[1] 349 | } 350 | // Be careful, the log line has a trailing new line 351 | expected = expected + "\n" 352 | 353 | if !assert.Equal(t, expected, buf.String()) { 354 | return 355 | } 356 | } 357 | } 358 | 359 | type Context struct { 360 | elapsedTime time.Duration 361 | request *http.Request 362 | requestTime time.Time 363 | responseContentLength int64 364 | responseHeader http.Header 365 | responseStatus int 366 | responseTime time.Time 367 | } 368 | 369 | func (ctx *Context) ElapsedTime() time.Duration { 370 | return ctx.elapsedTime 371 | } 372 | 373 | func (ctx *Context) Request() *http.Request { 374 | return ctx.request 375 | } 376 | 377 | func (ctx *Context) RequestTime() time.Time { 378 | return ctx.requestTime 379 | } 380 | 381 | func (ctx *Context) ResponseContentLength() int64 { 382 | return ctx.responseContentLength 383 | } 384 | 385 | func (ctx *Context) ResponseHeader() http.Header { 386 | return ctx.responseHeader 387 | } 388 | 389 | func (ctx *Context) ResponseStatus() int { 390 | return ctx.responseStatus 391 | } 392 | 393 | func (ctx *Context) ResponseTime() time.Time { 394 | return ctx.responseTime 395 | } 396 | -------------------------------------------------------------------------------- /v2/pool.go: -------------------------------------------------------------------------------- 1 | package apachelog 2 | 3 | import ( 4 | "bytes" 5 | "sync" 6 | ) 7 | 8 | var logBufferPool sync.Pool 9 | 10 | func init() { 11 | logBufferPool.New = allocLogBuffer 12 | } 13 | 14 | func allocLogBuffer() interface{} { 15 | return &bytes.Buffer{} 16 | } 17 | 18 | func getLogBuffer() *bytes.Buffer { 19 | return logBufferPool.Get().(*bytes.Buffer) 20 | } 21 | 22 | func releaseLogBuffer(v *bytes.Buffer) { 23 | v.Reset() 24 | logBufferPool.Put(v) 25 | } 26 | --------------------------------------------------------------------------------