Merge branch 'develop'

This commit is contained in:
Jimmy Zelinskie 2015-02-08 02:29:18 -05:00
commit 5ee355ed77
30 changed files with 474 additions and 72 deletions

15
Godeps/Godeps.json generated
View file

@ -4,27 +4,27 @@
"Deps": [ "Deps": [
{ {
"ImportPath": "github.com/chihaya/bencode", "ImportPath": "github.com/chihaya/bencode",
"Rev": "1df51811f3440202aa39df93596654319ea5ff57" "Rev": "e60878f635e1a61315c413492e133dd39769b1d1"
}, },
{ {
"ImportPath": "github.com/golang/glog", "ImportPath": "github.com/golang/glog",
"Rev": "d1c4472bf2efd3826f2b5bdcc02d8416798d678c" "Rev": "44145f04b68cf362d9c4df2182967c2275eaefed"
}, },
{ {
"ImportPath": "github.com/julienschmidt/httprouter", "ImportPath": "github.com/julienschmidt/httprouter",
"Rev": "46807412fe50aaceb73bb57061c2230fd26a1640" "Rev": "00ce1c6a267162792c367acc43b1681a884e1872"
}, },
{ {
"ImportPath": "github.com/pushrax/faststats", "ImportPath": "github.com/pushrax/faststats",
"Rev": "4c069408629f728eb3fe856f0966508492393a90" "Rev": "0fc2c5e41a187240ffaa09320eea7df9f8071388"
}, },
{ {
"ImportPath": "github.com/pushrax/flatjson", "ImportPath": "github.com/pushrax/flatjson",
"Rev": "cce9ff039f0bec46d67ed9ab4ce846b8355c94bb" "Rev": "86044f1c998d49053e13293029414ddb63f3a422"
}, },
{ {
"ImportPath": "github.com/stretchr/graceful", "ImportPath": "github.com/stretchr/graceful",
"Rev": "0b67b304c1354aa3d1b2c4353a630ee0f589c34f" "Rev": "8e780ba3fe3d3e7ab15fc52e3d60a996587181dc"
}, },
{ {
"ImportPath": "github.com/stretchr/pat/stop", "ImportPath": "github.com/stretchr/pat/stop",
@ -32,8 +32,7 @@
}, },
{ {
"ImportPath": "golang.org/x/net/netutil", "ImportPath": "golang.org/x/net/netutil",
"Comment": "null-204", "Rev": "c84eff7014eba178f68bd4c05b86780efe0fbf35"
"Rev": "8fd8d3a0313cb59e495106ac76df5da29381fa24"
} }
] ]
} }

View file

@ -1,6 +1,6 @@
bencode is released under a BSD 2-Clause license, reproduced below. bencode is released under a BSD 2-Clause license, reproduced below.
Copyright (c) 2014, The Chihaya Authors Copyright (c) 2015, The Chihaya Authors
All rights reserved. All rights reserved.
Redistribution and use in source and binary forms, with or without Redistribution and use in source and binary forms, with or without

View file

@ -1,4 +1,4 @@
// Copyright 2014 The Chihaya Authors. All rights reserved. // Copyright 2015 The Chihaya Authors. All rights reserved.
// Use of this source code is governed by the BSD 2-Clause license, // Use of this source code is governed by the BSD 2-Clause license,
// which can be found in the LICENSE file. // which can be found in the LICENSE file.

View file

@ -1,4 +1,4 @@
// Copyright 2014 The Chihaya Authors. All rights reserved. // Copyright 2015 The Chihaya Authors. All rights reserved.
// Use of this source code is governed by the BSD 2-Clause license, // Use of this source code is governed by the BSD 2-Clause license,
// which can be found in the LICENSE file. // which can be found in the LICENSE file.

View file

@ -1,4 +1,4 @@
// Copyright 2014 The Chihaya Authors. All rights reserved. // Copyright 2015 The Chihaya Authors. All rights reserved.
// Use of this source code is governed by the BSD 2-Clause license, // Use of this source code is governed by the BSD 2-Clause license,
// which can be found in the LICENSE file. // which can be found in the LICENSE file.

View file

@ -1,4 +1,4 @@
// Copyright 2014 The Chihaya Authors. All rights reserved. // Copyright 2015 The Chihaya Authors. All rights reserved.
// Use of this source code is governed by the BSD 2-Clause license, // Use of this source code is governed by the BSD 2-Clause license,
// which can be found in the LICENSE file. // which can be found in the LICENSE file.

View file

@ -1,4 +1,4 @@
// Copyright 2014 The Chihaya Authors. All rights reserved. // Copyright 2015 The Chihaya Authors. All rights reserved.
// Use of this source code is governed by the BSD 2-Clause license, // Use of this source code is governed by the BSD 2-Clause license,
// which can be found in the LICENSE file. // which can be found in the LICENSE file.

View file

@ -77,6 +77,7 @@ import (
"flag" "flag"
"fmt" "fmt"
"io" "io"
stdLog "log"
"os" "os"
"path/filepath" "path/filepath"
"runtime" "runtime"
@ -93,6 +94,9 @@ import (
// the corresponding constants in C++. // the corresponding constants in C++.
type severity int32 // sync/atomic int32 type severity int32 // sync/atomic int32
// These constants identify the log levels in order of increasing severity.
// A message written to a high-severity log file is also written to each
// lower-severity log file.
const ( const (
infoLog severity = iota infoLog severity = iota
warningLog warningLog
@ -311,7 +315,7 @@ func (m *moduleSpec) Set(value string) error {
// isLiteral reports whether the pattern is a literal string, that is, has no metacharacters // isLiteral reports whether the pattern is a literal string, that is, has no metacharacters
// that require filepath.Match to be called to match the pattern. // that require filepath.Match to be called to match the pattern.
func isLiteral(pattern string) bool { func isLiteral(pattern string) bool {
return !strings.ContainsAny(pattern, `*?[]\`) return !strings.ContainsAny(pattern, `\*?[]`)
} }
// traceLocation represents the setting of the -log_backtrace_at flag. // traceLocation represents the setting of the -log_backtrace_at flag.
@ -466,7 +470,7 @@ func (l *loggingT) setVState(verbosity Level, filter []modulePat, setFilter bool
// Turn verbosity off so V will not fire while we are in transition. // Turn verbosity off so V will not fire while we are in transition.
logging.verbosity.set(0) logging.verbosity.set(0)
// Ditto for filter length. // Ditto for filter length.
logging.filterLength = 0 atomic.StoreInt32(&logging.filterLength, 0)
// Set the new filters and wipe the pc->Level map if the filter has changed. // Set the new filters and wipe the pc->Level map if the filter has changed.
if setFilter { if setFilter {
@ -513,7 +517,8 @@ var timeNow = time.Now // Stubbed out for testing.
/* /*
header formats a log header as defined by the C++ implementation. header formats a log header as defined by the C++ implementation.
It returns a buffer containing the formatted header. It returns a buffer containing the formatted header and the user's file and line number.
The depth specifies how many stack frames above lives the source line to be identified in the log message.
Log lines have this form: Log lines have this form:
Lmmdd hh:mm:ss.uuuuuu threadid file:line] msg... Lmmdd hh:mm:ss.uuuuuu threadid file:line] msg...
@ -527,10 +532,8 @@ where the fields are defined as follows:
line The line number line The line number
msg The user-supplied message msg The user-supplied message
*/ */
func (l *loggingT) header(s severity) *buffer { func (l *loggingT) header(s severity, depth int) (*buffer, string, int) {
// Lmmdd hh:mm:ss.uuuuuu threadid file:line] _, file, line, ok := runtime.Caller(3 + depth)
now := timeNow()
_, file, line, ok := runtime.Caller(3) // It's always the same number of frames to the user's call.
if !ok { if !ok {
file = "???" file = "???"
line = 1 line = 1
@ -540,6 +543,12 @@ func (l *loggingT) header(s severity) *buffer {
file = file[slash+1:] file = file[slash+1:]
} }
} }
return l.formatHeader(s, file, line), file, line
}
// formatHeader formats a log header using the provided file name and line number.
func (l *loggingT) formatHeader(s severity, file string, line int) *buffer {
now := timeNow()
if line < 0 { if line < 0 {
line = 0 // not a real line number, but acceptable to someDigits line = 0 // not a real line number, but acceptable to someDigits
} }
@ -552,6 +561,7 @@ func (l *loggingT) header(s severity) *buffer {
// It's worth about 3X. Fprintf is hard. // It's worth about 3X. Fprintf is hard.
_, month, day := now.Date() _, month, day := now.Date()
hour, minute, second := now.Clock() hour, minute, second := now.Clock()
// Lmmdd hh:mm:ss.uuuuuu threadid file:line]
buf.tmp[0] = severityChar[s] buf.tmp[0] = severityChar[s]
buf.twoDigits(1, int(month)) buf.twoDigits(1, int(month))
buf.twoDigits(3, day) buf.twoDigits(3, day)
@ -562,11 +572,11 @@ func (l *loggingT) header(s severity) *buffer {
buf.tmp[11] = ':' buf.tmp[11] = ':'
buf.twoDigits(12, second) buf.twoDigits(12, second)
buf.tmp[14] = '.' buf.tmp[14] = '.'
buf.nDigits(6, 15, now.Nanosecond()/1000) buf.nDigits(6, 15, now.Nanosecond()/1000, '0')
buf.tmp[21] = ' ' buf.tmp[21] = ' '
buf.nDigits(5, 22, pid) // TODO: should be TID buf.nDigits(7, 22, pid, ' ') // TODO: should be TID
buf.tmp[27] = ' ' buf.tmp[29] = ' '
buf.Write(buf.tmp[:28]) buf.Write(buf.tmp[:30])
buf.WriteString(file) buf.WriteString(file)
buf.tmp[0] = ':' buf.tmp[0] = ':'
n := buf.someDigits(1, line) n := buf.someDigits(1, line)
@ -587,12 +597,18 @@ func (buf *buffer) twoDigits(i, d int) {
buf.tmp[i] = digits[d%10] buf.tmp[i] = digits[d%10]
} }
// nDigits formats a zero-prefixed n-digit integer at buf.tmp[i]. // nDigits formats an n-digit integer at buf.tmp[i],
func (buf *buffer) nDigits(n, i, d int) { // padding with pad on the left.
for j := n - 1; j >= 0; j-- { // It assumes d >= 0.
func (buf *buffer) nDigits(n, i, d int, pad byte) {
j := n - 1
for ; j >= 0 && d > 0; j-- {
buf.tmp[i+j] = digits[d%10] buf.tmp[i+j] = digits[d%10]
d /= 10 d /= 10
} }
for ; j >= 0; j-- {
buf.tmp[i+j] = pad
}
} }
// someDigits formats a zero-prefixed variable-width integer at buf.tmp[i]. // someDigits formats a zero-prefixed variable-width integer at buf.tmp[i].
@ -612,35 +628,50 @@ func (buf *buffer) someDigits(i, d int) int {
} }
func (l *loggingT) println(s severity, args ...interface{}) { func (l *loggingT) println(s severity, args ...interface{}) {
buf := l.header(s) buf, file, line := l.header(s, 0)
fmt.Fprintln(buf, args...) fmt.Fprintln(buf, args...)
l.output(s, buf) l.output(s, buf, file, line, false)
} }
func (l *loggingT) print(s severity, args ...interface{}) { func (l *loggingT) print(s severity, args ...interface{}) {
buf := l.header(s) l.printDepth(s, 1, args...)
}
func (l *loggingT) printDepth(s severity, depth int, args ...interface{}) {
buf, file, line := l.header(s, depth)
fmt.Fprint(buf, args...) fmt.Fprint(buf, args...)
if buf.Bytes()[buf.Len()-1] != '\n' { if buf.Bytes()[buf.Len()-1] != '\n' {
buf.WriteByte('\n') buf.WriteByte('\n')
} }
l.output(s, buf) l.output(s, buf, file, line, false)
} }
func (l *loggingT) printf(s severity, format string, args ...interface{}) { func (l *loggingT) printf(s severity, format string, args ...interface{}) {
buf := l.header(s) buf, file, line := l.header(s, 0)
fmt.Fprintf(buf, format, args...) fmt.Fprintf(buf, format, args...)
if buf.Bytes()[buf.Len()-1] != '\n' { if buf.Bytes()[buf.Len()-1] != '\n' {
buf.WriteByte('\n') buf.WriteByte('\n')
} }
l.output(s, buf) l.output(s, buf, file, line, false)
}
// printWithFileLine behaves like print but uses the provided file and line number. If
// alsoLogToStderr is true, the log message always appears on standard error; it
// will also appear in the log file unless --logtostderr is set.
func (l *loggingT) printWithFileLine(s severity, file string, line int, alsoToStderr bool, args ...interface{}) {
buf := l.formatHeader(s, file, line)
fmt.Fprint(buf, args...)
if buf.Bytes()[buf.Len()-1] != '\n' {
buf.WriteByte('\n')
}
l.output(s, buf, file, line, alsoToStderr)
} }
// output writes the data to the log files and releases the buffer. // output writes the data to the log files and releases the buffer.
func (l *loggingT) output(s severity, buf *buffer) { func (l *loggingT) output(s severity, buf *buffer, file string, line int, alsoToStderr bool) {
l.mu.Lock() l.mu.Lock()
if l.traceLocation.isSet() { if l.traceLocation.isSet() {
_, file, line, ok := runtime.Caller(3) // It's always the same number of frames to the user's call (same as header). if l.traceLocation.match(file, line) {
if ok && l.traceLocation.match(file, line) {
buf.Write(stacks(false)) buf.Write(stacks(false))
} }
} }
@ -648,7 +679,7 @@ func (l *loggingT) output(s severity, buf *buffer) {
if l.toStderr { if l.toStderr {
os.Stderr.Write(data) os.Stderr.Write(data)
} else { } else {
if l.alsoToStderr || s >= l.stderrThreshold.get() { if alsoToStderr || l.alsoToStderr || s >= l.stderrThreshold.get() {
os.Stderr.Write(data) os.Stderr.Write(data)
} }
if l.file[s] == nil { if l.file[s] == nil {
@ -672,7 +703,16 @@ func (l *loggingT) output(s severity, buf *buffer) {
} }
} }
if s == fatalLog { if s == fatalLog {
// Make sure we see the trace for the current goroutine on standard error. // If we got here via Exit rather than Fatal, print no stacks.
if atomic.LoadUint32(&fatalNoStacks) > 0 {
l.mu.Unlock()
timeoutFlush(10 * time.Second)
os.Exit(1)
}
// Dump all goroutine stacks before exiting.
// First, make sure we see the trace for the current goroutine on standard error.
// If -logtostderr has been specified, the loop below will do that anyway
// as the first stack in the full dump.
if !l.toStderr { if !l.toStderr {
os.Stderr.Write(stacks(false)) os.Stderr.Write(stacks(false))
} }
@ -861,6 +901,54 @@ func (l *loggingT) flushAll() {
} }
} }
// CopyStandardLogTo arranges for messages written to the Go "log" package's
// default logs to also appear in the Google logs for the named and lower
// severities. Subsequent changes to the standard log's default output location
// or format may break this behavior.
//
// Valid names are "INFO", "WARNING", "ERROR", and "FATAL". If the name is not
// recognized, CopyStandardLogTo panics.
func CopyStandardLogTo(name string) {
sev, ok := severityByName(name)
if !ok {
panic(fmt.Sprintf("log.CopyStandardLogTo(%q): unrecognized severity name", name))
}
// Set a log format that captures the user's file and line:
// d.go:23: message
stdLog.SetFlags(stdLog.Lshortfile)
stdLog.SetOutput(logBridge(sev))
}
// logBridge provides the Write method that enables CopyStandardLogTo to connect
// Go's standard logs to the logs provided by this package.
type logBridge severity
// Write parses the standard logging line and passes its components to the
// logger for severity(lb).
func (lb logBridge) Write(b []byte) (n int, err error) {
var (
file = "???"
line = 1
text string
)
// Split "d.go:23: message" into "d.go", "23", and "message".
if parts := bytes.SplitN(b, []byte{':'}, 3); len(parts) != 3 || len(parts[0]) < 1 || len(parts[2]) < 1 {
text = fmt.Sprintf("bad log format: %s", b)
} else {
file = string(parts[0])
text = string(parts[2][1:]) // skip leading space
line, err = strconv.Atoi(string(parts[1]))
if err != nil {
text = fmt.Sprintf("bad line number: %s", b)
line = 1
}
}
// printWithFileLine with alsoToStderr=true, so standard log messages
// always appear on standard error.
logging.printWithFileLine(severity(lb), file, line, true, text)
return len(b), nil
}
// setV computes and remembers the V level for a given PC // setV computes and remembers the V level for a given PC
// when vmodule is enabled. // when vmodule is enabled.
// File pattern matching takes the basename of the file, stripped // File pattern matching takes the basename of the file, stripped
@ -964,6 +1052,12 @@ func Info(args ...interface{}) {
logging.print(infoLog, args...) logging.print(infoLog, args...)
} }
// InfoDepth acts as Info but uses depth to determine which call frame to log.
// InfoDepth(0, "msg") is the same as Info("msg").
func InfoDepth(depth int, args ...interface{}) {
logging.printDepth(infoLog, depth, args...)
}
// Infoln logs to the INFO log. // Infoln logs to the INFO log.
// Arguments are handled in the manner of fmt.Println; a newline is appended if missing. // Arguments are handled in the manner of fmt.Println; a newline is appended if missing.
func Infoln(args ...interface{}) { func Infoln(args ...interface{}) {
@ -982,6 +1076,12 @@ func Warning(args ...interface{}) {
logging.print(warningLog, args...) logging.print(warningLog, args...)
} }
// WarningDepth acts as Warning but uses depth to determine which call frame to log.
// WarningDepth(0, "msg") is the same as Warning("msg").
func WarningDepth(depth int, args ...interface{}) {
logging.printDepth(warningLog, depth, args...)
}
// Warningln logs to the WARNING and INFO logs. // Warningln logs to the WARNING and INFO logs.
// Arguments are handled in the manner of fmt.Println; a newline is appended if missing. // Arguments are handled in the manner of fmt.Println; a newline is appended if missing.
func Warningln(args ...interface{}) { func Warningln(args ...interface{}) {
@ -1000,6 +1100,12 @@ func Error(args ...interface{}) {
logging.print(errorLog, args...) logging.print(errorLog, args...)
} }
// ErrorDepth acts as Error but uses depth to determine which call frame to log.
// ErrorDepth(0, "msg") is the same as Error("msg").
func ErrorDepth(depth int, args ...interface{}) {
logging.printDepth(errorLog, depth, args...)
}
// Errorln logs to the ERROR, WARNING, and INFO logs. // Errorln logs to the ERROR, WARNING, and INFO logs.
// Arguments are handled in the manner of fmt.Println; a newline is appended if missing. // Arguments are handled in the manner of fmt.Println; a newline is appended if missing.
func Errorln(args ...interface{}) { func Errorln(args ...interface{}) {
@ -1019,6 +1125,12 @@ func Fatal(args ...interface{}) {
logging.print(fatalLog, args...) logging.print(fatalLog, args...)
} }
// FatalDepth acts as Fatal but uses depth to determine which call frame to log.
// FatalDepth(0, "msg") is the same as Fatal("msg").
func FatalDepth(depth int, args ...interface{}) {
logging.printDepth(fatalLog, depth, args...)
}
// Fatalln logs to the FATAL, ERROR, WARNING, and INFO logs, // Fatalln logs to the FATAL, ERROR, WARNING, and INFO logs,
// including a stack trace of all running goroutines, then calls os.Exit(255). // including a stack trace of all running goroutines, then calls os.Exit(255).
// Arguments are handled in the manner of fmt.Println; a newline is appended if missing. // Arguments are handled in the manner of fmt.Println; a newline is appended if missing.
@ -1032,3 +1144,34 @@ func Fatalln(args ...interface{}) {
func Fatalf(format string, args ...interface{}) { func Fatalf(format string, args ...interface{}) {
logging.printf(fatalLog, format, args...) logging.printf(fatalLog, format, args...)
} }
// fatalNoStacks is non-zero if we are to exit without dumping goroutine stacks.
// It allows Exit and relatives to use the Fatal logs.
var fatalNoStacks uint32
// Exit logs to the FATAL, ERROR, WARNING, and INFO logs, then calls os.Exit(1).
// Arguments are handled in the manner of fmt.Print; a newline is appended if missing.
func Exit(args ...interface{}) {
atomic.StoreUint32(&fatalNoStacks, 1)
logging.print(fatalLog, args...)
}
// ExitDepth acts as Exit but uses depth to determine which call frame to log.
// ExitDepth(0, "msg") is the same as Exit("msg").
func ExitDepth(depth int, args ...interface{}) {
atomic.StoreUint32(&fatalNoStacks, 1)
logging.printDepth(fatalLog, depth, args...)
}
// Exitln logs to the FATAL, ERROR, WARNING, and INFO logs, then calls os.Exit(1).
func Exitln(args ...interface{}) {
atomic.StoreUint32(&fatalNoStacks, 1)
logging.println(fatalLog, args...)
}
// Exitf logs to the FATAL, ERROR, WARNING, and INFO logs, then calls os.Exit(1).
// Arguments are handled in the manner of fmt.Printf; a newline is appended if missing.
func Exitf(format string, args ...interface{}) {
atomic.StoreUint32(&fatalNoStacks, 1)
logging.printf(fatalLog, format, args...)
}

View file

@ -19,8 +19,10 @@ package glog
import ( import (
"bytes" "bytes"
"fmt" "fmt"
stdLog "log"
"path/filepath" "path/filepath"
"runtime" "runtime"
"strconv"
"strings" "strings"
"testing" "testing"
"time" "time"
@ -96,20 +98,99 @@ func TestInfo(t *testing.T) {
} }
} }
func TestInfoDepth(t *testing.T) {
setFlags()
defer logging.swap(logging.newBuffers())
f := func() { InfoDepth(1, "depth-test1") }
// The next three lines must stay together
_, _, wantLine, _ := runtime.Caller(0)
InfoDepth(0, "depth-test0")
f()
msgs := strings.Split(strings.TrimSuffix(contents(infoLog), "\n"), "\n")
if len(msgs) != 2 {
t.Fatalf("Got %d lines, expected 2", len(msgs))
}
for i, m := range msgs {
if !strings.HasPrefix(m, "I") {
t.Errorf("InfoDepth[%d] has wrong character: %q", i, m)
}
w := fmt.Sprintf("depth-test%d", i)
if !strings.Contains(m, w) {
t.Errorf("InfoDepth[%d] missing %q: %q", i, w, m)
}
// pull out the line number (between : and ])
msg := m[strings.LastIndex(m, ":")+1:]
x := strings.Index(msg, "]")
if x < 0 {
t.Errorf("InfoDepth[%d]: missing ']': %q", i, m)
continue
}
line, err := strconv.Atoi(msg[:x])
if err != nil {
t.Errorf("InfoDepth[%d]: bad line number: %q", i, m)
continue
}
wantLine++
if wantLine != line {
t.Errorf("InfoDepth[%d]: got line %d, want %d", i, line, wantLine)
}
}
}
func init() {
CopyStandardLogTo("INFO")
}
// Test that CopyStandardLogTo panics on bad input.
func TestCopyStandardLogToPanic(t *testing.T) {
defer func() {
if s, ok := recover().(string); !ok || !strings.Contains(s, "LOG") {
t.Errorf(`CopyStandardLogTo("LOG") should have panicked: %v`, s)
}
}()
CopyStandardLogTo("LOG")
}
// Test that using the standard log package logs to INFO.
func TestStandardLog(t *testing.T) {
setFlags()
defer logging.swap(logging.newBuffers())
stdLog.Print("test")
if !contains(infoLog, "I", t) {
t.Errorf("Info has wrong character: %q", contents(infoLog))
}
if !contains(infoLog, "test", t) {
t.Error("Info failed")
}
}
// Test that the header has the correct format. // Test that the header has the correct format.
func TestHeader(t *testing.T) { func TestHeader(t *testing.T) {
setFlags() setFlags()
defer logging.swap(logging.newBuffers()) defer logging.swap(logging.newBuffers())
defer func(previous func() time.Time) { timeNow = previous }(timeNow) defer func(previous func() time.Time) { timeNow = previous }(timeNow)
timeNow = func() time.Time { timeNow = func() time.Time {
return time.Date(2006, 1, 2, 15, 4, 5, .678901e9, time.Local) return time.Date(2006, 1, 2, 15, 4, 5, .067890e9, time.Local)
} }
pid = 1234
Info("test") Info("test")
var line, pid int var line int
n, err := fmt.Sscanf(contents(infoLog), "I0102 15:04:05.678901 %d glog_test.go:%d] test\n", &pid, &line) format := "I0102 15:04:05.067890 1234 glog_test.go:%d] test\n"
if n != 2 || err != nil { n, err := fmt.Sscanf(contents(infoLog), format, &line)
if n != 1 || err != nil {
t.Errorf("log format error: %d elements, error %s:\n%s", n, err, contents(infoLog)) t.Errorf("log format error: %d elements, error %s:\n%s", n, err, contents(infoLog))
} }
// Scanf treats multiple spaces as equivalent to a single space,
// so check for correct space-padding also.
want := fmt.Sprintf(format, line)
if contents(infoLog) != want {
t.Errorf("log format error: got:\n\t%q\nwant:\t%q", contents(infoLog), want)
}
} }
// Test that an Error log goes to Warning and Info. // Test that an Error log goes to Warning and Info.
@ -328,6 +409,7 @@ func TestLogBacktraceAt(t *testing.T) {
func BenchmarkHeader(b *testing.B) { func BenchmarkHeader(b *testing.B) {
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
logging.putBuffer(logging.header(infoLog)) buf, _, _ := logging.header(infoLog, 0)
logging.putBuffer(buf)
} }
} }

View file

@ -3,4 +3,5 @@ go:
- 1.1 - 1.1
- 1.2 - 1.2
- 1.3 - 1.3
- 1.4
- tip - tip

View file

@ -189,8 +189,9 @@ for example the [Gorilla handlers](http://www.gorillatoolkit.org/pkg/handlers).
Or you could [just write your own](http://justinas.org/writing-http-middleware-in-go/), Or you could [just write your own](http://justinas.org/writing-http-middleware-in-go/),
it's very easy! it's very easy!
Alternatively, you could try [a framework building upon HttpRouter](#web-frameworks-building-upon-httprouter). Alternatively, you could try [a framework building upon HttpRouter](#web-frameworks--co-based-on-httprouter).
### Multi-domain / Sub-domains
Here is a quick example: Does your server serve multiple domains / hosts? Here is a quick example: Does your server serve multiple domains / hosts?
You want to use sub-domains? You want to use sub-domains?
Define a router per host! Define a router per host!
@ -228,7 +229,85 @@ func main() {
} }
``` ```
## Web Frameworks building upon HttpRouter ### Basic Authentication
Another quick example: Basic Authentification (RFC 2617) for handles:
```go
package main
import (
"bytes"
"encoding/base64"
"fmt"
"github.com/julienschmidt/httprouter"
"net/http"
"log"
"strings"
)
func BasicAuth(h httprouter.Handle, user, pass []byte) httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
const basicAuthPrefix string = "Basic "
// Get the Basic Authentication credentials
auth := r.Header.Get("Authorization")
if strings.HasPrefix(auth, basicAuthPrefix) {
// Check credentials
payload, err := base64.StdEncoding.DecodeString(auth[len(basicAuthPrefix):])
if err == nil {
pair := bytes.SplitN(payload, []byte(":"), 2)
if len(pair) == 2 && bytes.Equal(pair[0], user) && bytes.Equal(pair[1], pass) {
// Delegate request to the given handle
h(w, r, ps)
return
}
}
}
// Request Basic Authentication otherwise
w.Header().Set("WWW-Authenticate", "Basic realm=Restricted")
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
}
}
func Index(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
fmt.Fprint(w, "Not protected!\n")
}
func Protected(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
fmt.Fprint(w, "Protected!\n")
}
func main() {
user := []byte("gordon")
pass := []byte("secret!")
router := httprouter.New()
router.GET("/", Index)
router.GET("/protected/", BasicAuth(Protected, user, pass))
log.Fatal(http.ListenAndServe(":8080", router))
}
```
## Chaining with the NotFound handler
**NOTE: It might be required to set [Router.HandleMethodNotAllowed](http://godoc.org/github.com/julienschmidt/httprouter#Router.HandleMethodNotAllowed) to `false` to avoid problems.**
You can use another [http.HandlerFunc](http://golang.org/pkg/net/http/#HandlerFunc), for example another router, to handle requests which could not be matched by this router by using the [Router.NotFound](http://godoc.org/github.com/julienschmidt/httprouter#Router.NotFound) handler. This allows chaining.
### Static files
The `NotFound` handler can for example be used to serve static files from the root path `/` (like an index.html file along with other assets):
```go
// Serve static files from the ./public directory
router.NotFound = http.FileServer(http.Dir("public")).ServeHTTP
```
But this approach sidesteps the strict core rules of this router to avoid routing problems. A cleaner approach is to use a distinct sub-path for serving files, like `/static/*filepath` or `/files/*filepath`.
## Web Frameworks & Co based on HttpRouter
If the HttpRouter is a bit too minimalistic for you, you might try one of the following more high-level 3rd-party web frameworks building upon the HttpRouter package: If the HttpRouter is a bit too minimalistic for you, you might try one of the following more high-level 3rd-party web frameworks building upon the HttpRouter package:
* [Gin](https://github.com/gin-gonic/gin): Features a martini-like API with much better performance * [Gin](https://github.com/gin-gonic/gin): Features a martini-like API with much better performance
* [Hikaru](https://github.com/najeira/hikaru): Supports standalone and Google AppEngine * [Hikaru](https://github.com/najeira/hikaru): Supports standalone and Google AppEngine
* [Hitch](https://github.com/nbio/hitch): Hitch ties httprouter, [httpcontext](https://github.com/nbio/httpcontext), and middleware up in a bow
* [Neko](https://github.com/rocwong/neko): A lightweight web application framework for Golang

View file

@ -123,13 +123,21 @@ type Router struct {
// handle is registered for it. // handle is registered for it.
// First superfluous path elements like ../ or // are removed. // First superfluous path elements like ../ or // are removed.
// Afterwards the router does a case-insensitive lookup of the cleaned path. // Afterwards the router does a case-insensitive lookup of the cleaned path.
// If a handle can be found for this route, the router makes a redirection // If a handle can be found for this route, the router makes a redirection
// to the corrected path with status code 301 for GET requests and 307 for // to the corrected path with status code 301 for GET requests and 307 for
// all other request methods. // all other request methods.
// For example /FOO and /..//Foo could be redirected to /foo. // For example /FOO and /..//Foo could be redirected to /foo.
// RedirectTrailingSlash is independent of this option. // RedirectTrailingSlash is independent of this option.
RedirectFixedPath bool RedirectFixedPath bool
// If enabled, the router checks if another method is allowed for the
// current route, if the current request can not be routed.
// If this is the case, the request is answered with 'Method Not Allowed'
// and HTTP status code 405.
// If no other Method is allowed, the request is delegated to the NotFound
// handler.
HandleMethodNotAllowed bool
// Configurable http.HandlerFunc which is called when no matching route is // Configurable http.HandlerFunc which is called when no matching route is
// found. If it is not set, http.NotFound is used. // found. If it is not set, http.NotFound is used.
NotFound http.HandlerFunc NotFound http.HandlerFunc
@ -149,8 +157,9 @@ var _ http.Handler = New()
// Path auto-correction, including trailing slashes, is enabled by default. // Path auto-correction, including trailing slashes, is enabled by default.
func New() *Router { func New() *Router {
return &Router{ return &Router{
RedirectTrailingSlash: true, RedirectTrailingSlash: true,
RedirectFixedPath: true, RedirectFixedPath: true,
HandleMethodNotAllowed: true,
} }
} }
@ -159,6 +168,11 @@ func (r *Router) GET(path string, handle Handle) {
r.Handle("GET", path, handle) r.Handle("GET", path, handle)
} }
// HEAD is a shortcut for router.Handle("HEAD", path, handle)
func (r *Router) HEAD(path string, handle Handle) {
r.Handle("HEAD", path, handle)
}
// POST is a shortcut for router.Handle("POST", path, handle) // POST is a shortcut for router.Handle("POST", path, handle)
func (r *Router) POST(path string, handle Handle) { func (r *Router) POST(path string, handle Handle) {
r.Handle("POST", path, handle) r.Handle("POST", path, handle)
@ -256,6 +270,9 @@ func (r *Router) recv(w http.ResponseWriter, req *http.Request) {
// Lookup allows the manual lookup of a method + path combo. // Lookup allows the manual lookup of a method + path combo.
// This is e.g. useful to build a framework around this router. // This is e.g. useful to build a framework around this router.
// If the path was found, it returns the handle function and the path parameter
// values. Otherwise the third return value indicates whether a redirection to
// the same path with an extra / without the trailing slash should be performed.
func (r *Router) Lookup(method, path string) (Handle, Params, bool) { func (r *Router) Lookup(method, path string) (Handle, Params, bool) {
if root := r.trees[method]; root != nil { if root := r.trees[method]; root != nil {
return root.getValue(path) return root.getValue(path)
@ -284,7 +301,7 @@ func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
} }
if tsr && r.RedirectTrailingSlash { if tsr && r.RedirectTrailingSlash {
if path[len(path)-1] == '/' { if len(path) > 1 && path[len(path)-1] == '/' {
req.URL.Path = path[:len(path)-1] req.URL.Path = path[:len(path)-1]
} else { } else {
req.URL.Path = path + "/" req.URL.Path = path + "/"
@ -308,6 +325,25 @@ func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
} }
} }
// Handle 405
if r.HandleMethodNotAllowed {
for method := range r.trees {
// Skip the requested method - we already tried this one
if method == req.Method {
continue
}
handle, _, _ := r.trees[method].getValue(req.URL.Path)
if handle != nil {
http.Error(w,
http.StatusText(http.StatusMethodNotAllowed),
http.StatusMethodNotAllowed,
)
return
}
}
}
// Handle 404 // Handle 404
if r.NotFound != nil { if r.NotFound != nil {
r.NotFound(w, req) r.NotFound(w, req)

View file

@ -76,7 +76,7 @@ func (h handlerStruct) ServeHTTP(w http.ResponseWriter, r *http.Request) {
} }
func TestRouterAPI(t *testing.T) { func TestRouterAPI(t *testing.T) {
var get, post, put, patch, delete, handler, handlerFunc bool var get, head, post, put, patch, delete, handler, handlerFunc bool
httpHandler := handlerStruct{&handler} httpHandler := handlerStruct{&handler}
@ -84,6 +84,9 @@ func TestRouterAPI(t *testing.T) {
router.GET("/GET", func(w http.ResponseWriter, r *http.Request, _ Params) { router.GET("/GET", func(w http.ResponseWriter, r *http.Request, _ Params) {
get = true get = true
}) })
router.HEAD("/GET", func(w http.ResponseWriter, r *http.Request, _ Params) {
head = true
})
router.POST("/POST", func(w http.ResponseWriter, r *http.Request, _ Params) { router.POST("/POST", func(w http.ResponseWriter, r *http.Request, _ Params) {
post = true post = true
}) })
@ -109,6 +112,12 @@ func TestRouterAPI(t *testing.T) {
t.Error("routing GET failed") t.Error("routing GET failed")
} }
r, _ = http.NewRequest("HEAD", "/GET", nil)
router.ServeHTTP(w, r)
if !head {
t.Error("routing HEAD failed")
}
r, _ = http.NewRequest("POST", "/POST", nil) r, _ = http.NewRequest("POST", "/POST", nil)
router.ServeHTTP(w, r) router.ServeHTTP(w, r)
if !post { if !post {
@ -156,12 +165,28 @@ func TestRouterRoot(t *testing.T) {
} }
} }
func TestRouterNotAllowed(t *testing.T) {
handlerFunc := func(_ http.ResponseWriter, _ *http.Request, _ Params) {}
router := New()
router.POST("/path", handlerFunc)
// Test not allowed
r, _ := http.NewRequest("GET", "/path", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, r)
if !(w.Code == http.StatusMethodNotAllowed) {
t.Errorf("NotAllowed handling route %s failed: Code=%d, Header=%v", w.Code, w.Header())
}
}
func TestRouterNotFound(t *testing.T) { func TestRouterNotFound(t *testing.T) {
handlerFunc := func(_ http.ResponseWriter, _ *http.Request, _ Params) {} handlerFunc := func(_ http.ResponseWriter, _ *http.Request, _ Params) {}
router := New() router := New()
router.GET("/path", handlerFunc) router.GET("/path", handlerFunc)
router.GET("/dir/", handlerFunc) router.GET("/dir/", handlerFunc)
router.GET("/", handlerFunc)
testRoutes := []struct { testRoutes := []struct {
route string route string
@ -170,6 +195,7 @@ func TestRouterNotFound(t *testing.T) {
}{ }{
{"/path/", 301, "map[Location:[/path]]"}, // TSR -/ {"/path/", 301, "map[Location:[/path]]"}, // TSR -/
{"/dir", 301, "map[Location:[/dir/]]"}, // TSR +/ {"/dir", 301, "map[Location:[/dir/]]"}, // TSR +/
{"", 301, "map[Location:[/]]"}, // TSR +/
{"/PATH", 301, "map[Location:[/path]]"}, // Fixed Case {"/PATH", 301, "map[Location:[/path]]"}, // Fixed Case
{"/DIR/", 301, "map[Location:[/dir/]]"}, // Fixed Case {"/DIR/", 301, "map[Location:[/dir/]]"}, // Fixed Case
{"/PATH/", 301, "map[Location:[/path]]"}, // Fixed Case -/ {"/PATH/", 301, "map[Location:[/path]]"}, // Fixed Case -/

View file

@ -379,7 +379,7 @@ walk: // Outer loop for walking the tree
return return
default: default:
panic("Unknown node type") panic("Invalid node type")
} }
} }
} else if path == n.path { } else if path == n.path {
@ -490,7 +490,7 @@ func (n *node) findCaseInsensitivePath(path string, fixTrailingSlash bool) (ciPa
return append(ciPath, path...), true return append(ciPath, path...), true
default: default:
panic("Unknown node type") panic("Invalid node type")
} }
} }
} else { } else {

View file

@ -557,3 +557,28 @@ func TestTreeFindCaseInsensitivePath(t *testing.T) {
} }
} }
} }
func TestTreeInvalidNodeType(t *testing.T) {
tree := &node{}
tree.addRoute("/", fakeHandler("/"))
tree.addRoute("/:page", fakeHandler("/:page"))
// set invalid node type
tree.children[0].nType = 42
// normal lookup
recv := catchPanic(func() {
tree.getValue("/test")
})
if rs, ok := recv.(string); !ok || rs != "Invalid node type" {
t.Fatalf(`Expected panic "Invalid node type", got "%v"`, recv)
}
// case-insensitive lookup
recv = catchPanic(func() {
tree.findCaseInsensitivePath("/test", true)
})
if rs, ok := recv.(string); !ok || rs != "Invalid node type" {
t.Fatalf(`Expected panic "Invalid node type", got "%v"`, recv)
}
}

View file

@ -1,6 +1,6 @@
faststats is released under a BSD 2-Clause license, reproduced below. faststats is released under a BSD 2-Clause license, reproduced below.
Copyright (c) 2014, The faststats Authors Copyright (c) 2015, The faststats Authors
All rights reserved. All rights reserved.
Redistribution and use in source and binary forms, with or without modification, Redistribution and use in source and binary forms, with or without modification,

View file

@ -1,4 +1,4 @@
// Copyright 2014 The faststats Authors. All rights reserved. // Copyright 2015 The faststats Authors. All rights reserved.
// Use of this source code is governed by the BSD 2-Clause license, // Use of this source code is governed by the BSD 2-Clause license,
// which can be found in the LICENSE file. // which can be found in the LICENSE file.

View file

@ -1,4 +1,4 @@
// Copyright 2014 The faststats Authors. All rights reserved. // Copyright 2015 The faststats Authors. All rights reserved.
// Use of this source code is governed by the BSD 2-Clause license, // Use of this source code is governed by the BSD 2-Clause license,
// which can be found in the LICENSE file. // which can be found in the LICENSE file.

View file

@ -1,4 +1,4 @@
// Copyright 2014 The faststats Authors. All rights reserved. // Copyright 2015 The faststats Authors. All rights reserved.
// Use of this source code is governed by the BSD 2-Clause license, // Use of this source code is governed by the BSD 2-Clause license,
// which can be found in the LICENSE file. // which can be found in the LICENSE file.

View file

@ -1,4 +1,4 @@
// Copyright 2014 The faststats Authors. All rights reserved. // Copyright 2015 The faststats Authors. All rights reserved.
// Use of this source code is governed by the BSD 2-Clause license, // Use of this source code is governed by the BSD 2-Clause license,
// which can be found in the LICENSE file. // which can be found in the LICENSE file.

View file

@ -1,4 +1,4 @@
// Copyright 2014 The faststats Authors. All rights reserved. // Copyright 2015 The faststats Authors. All rights reserved.
// Use of this source code is governed by the BSD 2-Clause license, // Use of this source code is governed by the BSD 2-Clause license,
// which can be found in the LICENSE file. // which can be found in the LICENSE file.

View file

@ -1,6 +1,6 @@
Chihaya is released under a BSD 2-Clause license, reproduced below. flatjson is released under a BSD 2-Clause license, reproduced below.
Copyright (c) 2014, The Chihaya Authors Copyright (c) 2015, The flatjson Authors
All rights reserved. All rights reserved.
Redistribution and use in source and binary forms, with or without modification, Redistribution and use in source and binary forms, with or without modification,

View file

@ -7,7 +7,7 @@ Example use case:
```json ```json
{ {
"Connections" { "Connections": {
"Open": 2, "Open": 2,
"Accepted": 4 "Accepted": 4
}, },

View file

@ -1,4 +1,4 @@
// Copyright 2014 The flatjson Authors. All rights reserved. // Copyright 2015 The flatjson Authors. All rights reserved.
// Use of this source code is governed by the BSD 2-Clause license, // Use of this source code is governed by the BSD 2-Clause license,
// which can be found in the LICENSE file. // which can be found in the LICENSE file.

View file

@ -14,14 +14,11 @@ type Child struct {
} }
func TestBasicFlatten(t *testing.T) { func TestBasicFlatten(t *testing.T) {
val := &struct { val := &Child{10, "str"}
A int
B string
}{10, "str"}
expected := flatjson.Map{ expected := flatjson.Map{
"A": 10.0, // JSON numbers are all float64. "CC": 10.0, // JSON numbers are all float64.
"B": "str", "CD": "str",
} }
testFlattening(t, val, expected) testFlattening(t, val, expected)

View file

@ -12,7 +12,7 @@ import (
"time" "time"
"github.com/stretchr/pat/stop" "github.com/stretchr/pat/stop"
"code.google.com/p/go.net/netutil" "golang.org/x/net/netutil"
) )
// Server wraps an http.Server with graceful connection handling. // Server wraps an http.Server with graceful connection handling.
@ -115,8 +115,13 @@ func (srv *Server) ListenAndServe() error {
// timeout is the duration to wait until killing active requests and stopping the server. // timeout is the duration to wait until killing active requests and stopping the server.
// If timeout is 0, the server never times out. It waits for all active requests to finish. // If timeout is 0, the server never times out. It waits for all active requests to finish.
func ListenAndServeTLS(server *http.Server, certFile, keyFile string, timeout time.Duration) error { func ListenAndServeTLS(server *http.Server, certFile, keyFile string, timeout time.Duration) error {
// Create the listener ourselves so we can control its lifetime
srv := &Server{Timeout: timeout, Server: server} srv := &Server{Timeout: timeout, Server: server}
return srv.ListenAndServeTLS(certFile, keyFile)
}
// ListenAndServeTLS is equivalent to http.Server.ListenAndServeTLS with graceful shutdown enabled.
func (srv *Server) ListenAndServeTLS(certFile, keyFile string) error {
// Create the listener ourselves so we can control its lifetime
addr := srv.Addr addr := srv.Addr
if addr == "" { if addr == "" {
addr = ":https" addr = ":https"

View file

@ -4,7 +4,7 @@
// Package netutil provides network utility functions, complementing the more // Package netutil provides network utility functions, complementing the more
// common ones in the net package. // common ones in the net package.
package netutil package netutil // import "golang.org/x/net/netutil"
import ( import (
"net" "net"

View file

@ -11,6 +11,7 @@
"real_ip_header": "", "real_ip_header": "",
"respect_af": false, "respect_af": false,
"client_whitelist_enabled": false, "client_whitelist_enabled": false,
"client_whitelist": ["OP1011"],
"http_listen_addr": ":6881", "http_listen_addr": ":6881",
"http_request_timeout": "10s", "http_request_timeout": "10s",
"http_read_timeout": "10s", "http_read_timeout": "10s",

View file

@ -83,6 +83,7 @@ func newRouter(s *Server) *httprouter.Router {
} }
if s.config.ClientWhitelistEnabled { if s.config.ClientWhitelistEnabled {
r.GET("/clients/:clientID", makeHandler(s.getClient))
r.PUT("/clients/:clientID", makeHandler(s.putClient)) r.PUT("/clients/:clientID", makeHandler(s.putClient))
r.DELETE("/clients/:clientID", makeHandler(s.delClient)) r.DELETE("/clients/:clientID", makeHandler(s.delClient))
} }

View file

@ -178,6 +178,13 @@ func (s *Server) delUser(w http.ResponseWriter, r *http.Request, p httprouter.Pa
return http.StatusOK, nil return http.StatusOK, nil
} }
func (s *Server) getClient(w http.ResponseWriter, r *http.Request, p httprouter.Params) (int, error) {
if err := s.tracker.ClientApproved(p.ByName("clientID")); err != nil {
return http.StatusNotFound, err
}
return http.StatusOK, nil
}
func (s *Server) putClient(w http.ResponseWriter, r *http.Request, p httprouter.Params) (int, error) { func (s *Server) putClient(w http.ResponseWriter, r *http.Request, p httprouter.Params) (int, error) {
s.tracker.PutClient(p.ByName("clientID")) s.tracker.PutClient(p.ByName("clientID"))
return http.StatusOK, nil return http.StatusOK, nil