ref/lib/apilog: include the file:line # in the msg, add a unit test

- see the package comment for an explanation of the overlooked
  difference between this new package and vlog.LogCall - i.e.
  the log header no longer includes the api callsite, rather it
  includes the callsite in this package.

Change-Id: Ib3e388f66128ef04e2a31fa72beda64f10c67dae
diff --git a/lib/apilog/log.go b/lib/apilog/apilog.go
similarity index 75%
rename from lib/apilog/log.go
rename to lib/apilog/apilog.go
index 3eb4f92..83c11b0 100644
--- a/lib/apilog/log.go
+++ b/lib/apilog/apilog.go
@@ -2,16 +2,25 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-// Package apilog provides a function to be used in conjunction with vtrace
-// and logcop automatically injected logging calls. These are called on
-// entry/exit for methods that implements a v23 API call.
+// Package apilog provides functions to be used in conjunction with logcop.
+// In particular, logcop will inject calls to these functions as the first
+// statement in methods that implement the v23 API. The output can
+// be controlled by vlog verbosity or vtrace.
+// The log lines generated have a header that refers to this file
+// and the message includes the file and line # of the caller. This is
+// currently the best we can do given the functionality of the underlying
+// vlog package. It has the advantage the api logging can be selectively
+// enabled/disabled globally using --vmodule=apilog=<level>, but the
+// disadvantage that it can't be controlled at the per file level.
 package apilog
 
 import (
 	"fmt"
 	"path"
+	"path/filepath"
 	"reflect"
 	"runtime"
+	"strconv"
 	"sync/atomic"
 
 	"v.io/x/lib/vlog"
@@ -22,17 +31,23 @@
 // logCallLogLevel is the log level beyond which calls are logged.
 const logCallLogLevel = 1
 
-func callerFuncName() string {
+var logger vlog.Logger
+
+func init() {
+	logger = vlog.Log
+}
+
+func callerLocation() string {
 	var funcName string
 	const stackSkip = 1
-	pc, _, _, ok := runtime.Caller(stackSkip + 1)
+	pc, file, line, ok := runtime.Caller(stackSkip + 1)
 	if ok {
 		function := runtime.FuncForPC(pc)
 		if function != nil {
 			funcName = path.Base(function.Name())
 		}
 	}
-	return funcName
+	return filepath.Base(file) + ":" + strconv.Itoa(line) + " " + funcName
 }
 
 // TODO(cnicolaou): remove LogCall from vlog.
@@ -89,27 +104,28 @@
 //     }
 //
 func LogCall(ctx *context.T, v ...interface{}) func(*context.T, ...interface{}) {
-	if !vlog.V(logCallLogLevel) { // TODO(mattr): add call to vtrace.
+	if !logger.V(logCallLogLevel) { // TODO(mattr): add call to vtrace.
 		return func(*context.T, ...interface{}) {}
 	}
-	callerFuncName := callerFuncName()
+	callerLocation := callerLocation()
 	invocationId := newInvocationIdentifier()
 	var output string
 	if len(v) > 0 {
-		output = fmt.Sprintf("call[%s %s]: args:%v", callerFuncName, invocationId, v)
+		output = fmt.Sprintf("call[%s %s]: args:%v", callerLocation, invocationId, v)
 	} else {
-		output = fmt.Sprintf("call[%s %s]", callerFuncName, invocationId)
+		output = fmt.Sprintf("call[%s %s]", callerLocation, invocationId)
 	}
-	vlog.Info(output)
+	logger.Info(output)
+
 	// TODO(mattr): annotate vtrace span.
 	return func(ctx *context.T, v ...interface{}) {
 		var output string
 		if len(v) > 0 {
-			output = fmt.Sprintf("return[%s %s]: %v", callerFuncName, invocationId, derefSlice(v))
+			output = fmt.Sprintf("return[%s %s]: %v", callerLocation, invocationId, derefSlice(v))
 		} else {
-			output = fmt.Sprintf("return[%s %s]", callerFuncName, invocationId)
+			output = fmt.Sprintf("return[%s %s]", callerLocation, invocationId)
 		}
-		vlog.Info(output)
+		logger.Info(output)
 		// TODO(mattr): annotate vtrace span.
 	}
 }
@@ -124,17 +140,17 @@
 //     }
 //
 func LogCallf(ctx *context.T, format string, v ...interface{}) func(*context.T, string, ...interface{}) {
-	if !vlog.V(logCallLogLevel) { // TODO(mattr): add call to vtrace.
+	if !logger.V(logCallLogLevel) { // TODO(mattr): add call to vtrace.
 		return func(*context.T, string, ...interface{}) {}
 	}
-	callerFuncName := callerFuncName()
+	callerLocation := callerLocation()
 	invocationId := newInvocationIdentifier()
-	output := fmt.Sprintf("call[%s %s]: %s", callerFuncName, invocationId, fmt.Sprintf(format, v...))
-	vlog.Info(output)
+	output := fmt.Sprintf("call[%s %s]: %s", callerLocation, invocationId, fmt.Sprintf(format, v...))
+	logger.Info(output)
 	// TODO(mattr): annotate vtrace span.
 	return func(ctx *context.T, format string, v ...interface{}) {
-		output := fmt.Sprintf("return[%s %s]: %v", callerFuncName, invocationId, fmt.Sprintf(format, derefSlice(v)...))
-		vlog.Info(output)
+		output := fmt.Sprintf("return[%s %s]: %v", callerLocation, invocationId, fmt.Sprintf(format, derefSlice(v)...))
+		logger.Info(output)
 		// TODO(mattr): annotate vtrace span.
 	}
 }
diff --git a/lib/apilog/apilog_test.go b/lib/apilog/apilog_test.go
new file mode 100644
index 0000000..d085efc
--- /dev/null
+++ b/lib/apilog/apilog_test.go
@@ -0,0 +1,93 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package apilog_test
+
+import (
+	"bufio"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"regexp"
+	"strings"
+	"testing"
+
+	"v.io/x/lib/vlog"
+
+	"v.io/x/ref/lib/apilog"
+)
+
+func readLogFiles(dir string) ([]string, error) {
+	files, err := ioutil.ReadDir(dir)
+	if err != nil {
+		return nil, err
+	}
+	var contents []string
+	for _, fi := range files {
+		// Skip symlinks to avoid double-counting log lines.
+		if !fi.Mode().IsRegular() {
+			continue
+		}
+		file, err := os.Open(filepath.Join(dir, fi.Name()))
+		if err != nil {
+			return nil, err
+		}
+		scanner := bufio.NewScanner(file)
+		for scanner.Scan() {
+			if line := scanner.Text(); len(line) > 0 && line[0] == 'I' {
+				contents = append(contents, line)
+			}
+		}
+	}
+	return contents, nil
+}
+
+func myLoggedFunc() {
+	f := apilog.LogCall(nil, "entry")
+	f(nil, "exit")
+}
+
+func TestLogCall(t *testing.T) {
+	dir, err := ioutil.TempDir("", "logtest")
+	defer os.RemoveAll(dir)
+	if err != nil {
+		t.Fatalf("unexpected error: %s", err)
+	}
+	logger := vlog.NewLogger("testHeader")
+	logger.Configure(vlog.LogDir(dir), vlog.Level(2))
+	saveLog := apilog.Log()
+	defer func() { apilog.SetLog(saveLog) }()
+	apilog.SetLog(logger)
+	myLoggedFunc()
+	logger.FlushLog()
+	contents, err := readLogFiles(dir)
+	if err != nil {
+		t.Fatalf("unexpected error: %s", err)
+	}
+	if want, got := 2, len(contents); want != got {
+		t.Errorf("Expected %d info lines, got %d instead", want, got)
+	}
+	logCallLineRE := regexp.MustCompile(`\S+ \S+ \S+ ([^:]*):.*(call|return)\[(\S*) (\S*)`)
+	for _, line := range contents {
+		match := logCallLineRE.FindStringSubmatch(line)
+		if len(match) != 5 {
+			t.Errorf("failed to match %s", line)
+			continue
+		}
+		fileName, callType, location, funcName := match[1], match[2], match[3], match[4]
+		if fileName != "apilog.go" {
+			t.Errorf("unexpected file name: %s", fileName)
+			continue
+		}
+		if callType != "call" && callType != "return" {
+			t.Errorf("unexpected call type: %s", callType)
+		}
+		if !strings.HasPrefix(location, "apilog_test.go:") {
+			t.Errorf("unexpected location: %s", location)
+		}
+		if funcName != "apilog_test.myLoggedFunc" {
+			t.Errorf("unexpected func name: %s", funcName)
+		}
+	}
+}
diff --git a/lib/apilog/util_test.go b/lib/apilog/util_test.go
new file mode 100644
index 0000000..9d2ea7d
--- /dev/null
+++ b/lib/apilog/util_test.go
@@ -0,0 +1,15 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package apilog
+
+import "v.io/x/lib/vlog"
+
+func SetLog(l vlog.Logger) {
+	logger = l
+}
+
+func Log() vlog.Logger {
+	return logger
+}