Merge "veyron/runtimes/google/ipc,veyron/runtimes/google/rt: expose stats for security."
diff --git a/lib/testutil/glob.go b/lib/testutil/glob.go
new file mode 100644
index 0000000..ecd078f
--- /dev/null
+++ b/lib/testutil/glob.go
@@ -0,0 +1,37 @@
+package testutil
+
+import (
+	"io"
+	"sort"
+
+	"veyron.io/veyron/veyron2/ipc"
+	"veyron.io/veyron/veyron2/rt"
+	"veyron.io/veyron/veyron2/services/mounttable/types"
+)
+
+// GlobName calls __Glob on the given object with the given pattern and returns
+// a sorted list of matching object names, or an error.
+func GlobName(name, pattern string) ([]string, error) {
+	call, err := rt.R().Client().StartCall(rt.R().NewContext(), name, ipc.GlobMethod, []interface{}{pattern})
+	if err != nil {
+		return nil, err
+	}
+	results := []string{}
+Loop:
+	for {
+		var me types.MountEntry
+		switch err := call.Recv(&me); err {
+		case nil:
+			results = append(results, me.Name)
+		case io.EOF:
+			break Loop
+		default:
+			return nil, err
+		}
+	}
+	sort.Strings(results)
+	if ferr := call.Finish(&err); ferr != nil {
+		err = ferr
+	}
+	return results, err
+}
diff --git a/profiles/roaming/roaming.go b/profiles/roaming/roaming.go
index 4aa190f..33a1c20 100644
--- a/profiles/roaming/roaming.go
+++ b/profiles/roaming/roaming.go
@@ -72,7 +72,7 @@
 func (p *profile) Init(rt veyron2.Runtime, publisher *config.Publisher) error {
 	log := rt.Logger()
 
-	rt.ConfigureReservedName(debug.NewDispatcher(log.LogDir(), sflag.NewAuthorizerOrDie()))
+	rt.ConfigureReservedName(debug.NewDispatcher(log.LogDir(), sflag.NewAuthorizerOrDie(), rt.VtraceStore()))
 
 	lf := commonFlags.ListenFlags()
 	ListenSpec = ipc.ListenSpec{
diff --git a/profiles/static/static.go b/profiles/static/static.go
index 87b1780..7ff75b1 100644
--- a/profiles/static/static.go
+++ b/profiles/static/static.go
@@ -58,7 +58,7 @@
 func (p *static) Init(rt veyron2.Runtime, _ *config.Publisher) error {
 	log := rt.Logger()
 
-	rt.ConfigureReservedName(debug.NewDispatcher(log.LogDir(), sflag.NewAuthorizerOrDie()))
+	rt.ConfigureReservedName(debug.NewDispatcher(log.LogDir(), sflag.NewAuthorizerOrDie(), rt.VtraceStore()))
 
 	lf := commonFlags.ListenFlags()
 	ListenSpec = ipc.ListenSpec{
diff --git a/runtimes/google/ipc/debug_test.go b/runtimes/google/ipc/debug_test.go
index 054696d..c537fec 100644
--- a/runtimes/google/ipc/debug_test.go
+++ b/runtimes/google/ipc/debug_test.go
@@ -16,6 +16,7 @@
 	"veyron.io/veyron/veyron/runtimes/google/ipc/stream/sectest"
 	"veyron.io/veyron/veyron/runtimes/google/ipc/stream/vc"
 	tnaming "veyron.io/veyron/veyron/runtimes/google/testing/mocks/naming"
+	"veyron.io/veyron/veyron/runtimes/google/vtrace"
 	"veyron.io/veyron/veyron/services/mgmt/debug"
 )
 
@@ -30,7 +31,8 @@
 	pclient.AddToRoots(bclient)                    // Client recognizes "server" as a root of blessings.
 	pclient.BlessingStore().Set(bclient, "server") // Client presents bclient to server
 
-	debugDisp := debug.NewDispatcher(vlog.Log.LogDir(), nil)
+	store := vtrace.NewStore(10)
+	debugDisp := debug.NewDispatcher(vlog.Log.LogDir(), nil, store)
 
 	sm := manager.InternalNew(naming.FixedRoutingID(0x555555555))
 	defer sm.Shutdown()
@@ -94,12 +96,12 @@
 	}{
 		{"", "*", []string{}},
 		{"", "__*", []string{"__debug"}},
-		{"", "__*/*", []string{"__debug/logs", "__debug/pprof", "__debug/stats"}},
-		{"__debug", "*", []string{"logs", "pprof", "stats"}},
+		{"", "__*/*", []string{"__debug/logs", "__debug/pprof", "__debug/stats", "__debug/vtrace"}},
+		{"__debug", "*", []string{"logs", "pprof", "stats", "vtrace"}},
 	}
 	for _, tc := range testcases {
 		addr := naming.JoinAddressName(ep.String(), "//"+tc.name)
-		call, err := client.StartCall(ctx, addr, "Glob", []interface{}{tc.pattern})
+		call, err := client.StartCall(ctx, addr, ipc.GlobMethod, []interface{}{tc.pattern})
 		if err != nil {
 			t.Fatalf("client.StartCall failed: %v", err)
 		}
diff --git a/runtimes/google/ipc/glob_test.go b/runtimes/google/ipc/glob_test.go
index 654709d..44b7720 100644
--- a/runtimes/google/ipc/glob_test.go
+++ b/runtimes/google/ipc/glob_test.go
@@ -3,7 +3,6 @@
 import (
 	"fmt"
 	"reflect"
-	"sort"
 	"strings"
 	"testing"
 
@@ -12,10 +11,10 @@
 	"veyron.io/veyron/veyron2/naming"
 	"veyron.io/veyron/veyron2/rt"
 	"veyron.io/veyron/veyron2/security"
-	"veyron.io/veyron/veyron2/services/mounttable"
 	"veyron.io/veyron/veyron2/services/mounttable/types"
 
 	"veyron.io/veyron/veyron/lib/glob"
+	"veyron.io/veyron/veyron/lib/testutil"
 	"veyron.io/veyron/veyron/profiles"
 )
 
@@ -159,27 +158,15 @@
 		}},
 	}
 	for _, tc := range testcases {
-		c := mounttable.GlobbableClient(naming.JoinAddressName(ep, tc.name))
-
-		stream, err := c.Glob(runtime.NewContext(), tc.pattern)
+		name := naming.JoinAddressName(ep, tc.name)
+		results, err := testutil.GlobName(name, tc.pattern)
 		if err != nil {
-			t.Fatalf("Glob failed: %v", err)
+			t.Errorf("unexpected Glob error for (%q, %q): %v", tc.name, tc.pattern, err)
+			continue
 		}
-		results := []string{}
-		iterator := stream.RecvStream()
-		for iterator.Advance() {
-			results = append(results, iterator.Value().Name)
-		}
-		sort.Strings(results)
 		if !reflect.DeepEqual(results, tc.expected) {
 			t.Errorf("unexpected result for (%q, %q). Got %q, want %q", tc.name, tc.pattern, results, tc.expected)
 		}
-		if err := iterator.Err(); err != nil {
-			t.Errorf("unexpected stream error for %q: %v", tc.name, err)
-		}
-		if err := stream.Finish(); err != nil {
-			t.Errorf("Finish failed for %q: %v", tc.name, err)
-		}
 	}
 }
 
diff --git a/runtimes/google/ipc/server.go b/runtimes/google/ipc/server.go
index d149ee6..c30c097 100644
--- a/runtimes/google/ipc/server.go
+++ b/runtimes/google/ipc/server.go
@@ -839,9 +839,7 @@
 // value may be modified to match the actual name and method to use.
 func (fs *flowServer) lookup(name, method *string) (ipc.Invoker, security.Authorizer, verror.E) {
 	*name = strings.TrimLeft(*name, "/")
-	// TODO(rthellend): Remove "Glob" from the condition below after
-	// everything has transitioned to the new name.
-	if *method == "Glob" || *method == ipc.GlobMethod {
+	if *method == ipc.GlobMethod {
 		*method = "Glob"
 		return ipc.ReflectInvoker(&globInternal{fs, *name}), &acceptAllAuthorizer{}, nil
 	}
diff --git a/runtimes/google/naming/namespace/glob.go b/runtimes/google/naming/namespace/glob.go
index 4083bb3..ed6cda2 100644
--- a/runtimes/google/naming/namespace/glob.go
+++ b/runtimes/google/naming/namespace/glob.go
@@ -8,6 +8,7 @@
 	"veyron.io/veyron/veyron/lib/glob"
 
 	"veyron.io/veyron/veyron2/context"
+	"veyron.io/veyron/veyron2/ipc"
 	"veyron.io/veyron/veyron2/naming"
 	"veyron.io/veyron/veyron2/options"
 	"veyron.io/veyron/veyron2/services/mounttable/types"
@@ -60,7 +61,7 @@
 		// Don't further resolve s.Server.
 		callCtx, _ := ctx.WithTimeout(callTimeout)
 		client := ns.rt.Client()
-		call, err := client.StartCall(callCtx, s.Server, "Glob", []interface{}{pstr}, options.NoResolve(true))
+		call, err := client.StartCall(callCtx, s.Server, ipc.GlobMethod, []interface{}{pstr}, options.NoResolve(true))
 		if err != nil {
 			lastErr = err
 			continue // try another instance
diff --git a/runtimes/google/rt/rt.go b/runtimes/google/rt/rt.go
index 2cff75d..4b02edf 100644
--- a/runtimes/google/rt/rt.go
+++ b/runtimes/google/rt/rt.go
@@ -158,6 +158,10 @@
 	copy(rt.reservedOpts, opts)
 }
 
+func (rt *vrt) VtraceStore() vtrace.Store {
+	return rt.traceStore
+}
+
 func (rt *vrt) Cleanup() {
 	if rt.flags.Vtrace.DumpOnShutdown {
 		vtrace.FormatTraces(os.Stderr, rt.traceStore.TraceRecords(), nil)
diff --git a/runtimes/google/testing/mocks/runtime/panic_runtime.go b/runtimes/google/testing/mocks/runtime/panic_runtime.go
index f5c7c2c..1c28e1d 100644
--- a/runtimes/google/testing/mocks/runtime/panic_runtime.go
+++ b/runtimes/google/testing/mocks/runtime/panic_runtime.go
@@ -48,4 +48,5 @@
 func (*PanicRuntime) ConfigureReservedName(ipc.Dispatcher, ...ipc.ServerOpt) {
 	panic(badRuntime)
 }
-func (*PanicRuntime) Cleanup() { panic(badRuntime) }
+func (*PanicRuntime) VtraceStore() vtrace.Store { panic(badRuntime) }
+func (*PanicRuntime) Cleanup()                  { panic(badRuntime) }
diff --git a/runtimes/google/vtrace/collector.go b/runtimes/google/vtrace/collector.go
index 6c883cc..fc06731 100644
--- a/runtimes/google/vtrace/collector.go
+++ b/runtimes/google/vtrace/collector.go
@@ -27,10 +27,9 @@
 type collector struct {
 	traceID uniqueid.ID
 	store   *Store
-
-	mu     sync.Mutex
-	method vtrace.TraceMethod                 // GUARDED_BY(mu)
-	spans  map[uniqueid.ID]*vtrace.SpanRecord // GUARDED_BY(mu)
+	mu      sync.Mutex
+	method  vtrace.TraceMethod                 // GUARDED_BY(mu)
+	spans   map[uniqueid.ID]*vtrace.SpanRecord // GUARDED_BY(mu)
 }
 
 // newCollector returns a new collector for the given traceID.
diff --git a/runtimes/google/vtrace/store.go b/runtimes/google/vtrace/store.go
index 182dea5..f70dc2a 100644
--- a/runtimes/google/vtrace/store.go
+++ b/runtimes/google/vtrace/store.go
@@ -58,20 +58,20 @@
 }
 
 // TraceRecords returns TraceRecords for all traces saved in the store.
-func (s *Store) TraceRecords() []*vtrace.TraceRecord {
+func (s *Store) TraceRecords() []vtrace.TraceRecord {
 	s.mu.Lock()
 	defer s.mu.Unlock()
 
-	out := make([]*vtrace.TraceRecord, s.size)
+	out := make([]vtrace.TraceRecord, s.size)
 	i := 0
 	for _, ts := range s.traces {
-		out[i] = ts.traceRecord()
+		ts.traceRecord(&out[i])
 		i++
 	}
 	return out
 }
 
-// TraceRecord returns a TraceRecord for a given uniqueid.  Returns
+// TraceRecord returns a TraceRecord for a given ID.  Returns
 // nil if the given id is not present.
 func (s *Store) TraceRecord(id uniqueid.ID) *vtrace.TraceRecord {
 	s.mu.Lock()
@@ -80,7 +80,9 @@
 	if ts == nil {
 		return nil
 	}
-	return ts.traceRecord()
+	out := vtrace.TraceRecord{}
+	ts.traceRecord(&out)
+	return &out
 }
 
 // trimLocked removes elements from the store LRU first until we are
@@ -135,9 +137,7 @@
 	panic("unreachable")
 }
 
-func (ts *traceSet) traceRecord() *vtrace.TraceRecord {
-	var out vtrace.TraceRecord
-
+func (ts *traceSet) traceRecord(out *vtrace.TraceRecord) {
 	// It is possible to have duplicate copies of spans.  Consider the
 	// case where a server calls itself (even indirectly) we'll have one
 	// Trace in the set for the parent call and one Trace in the set for
@@ -156,5 +156,4 @@
 			out.Spans = append(out.Spans, span)
 		}
 	}
-	return &out
 }
diff --git a/runtimes/google/vtrace/store_test.go b/runtimes/google/vtrace/store_test.go
index 9fdc06b..910dd07 100644
--- a/runtimes/google/vtrace/store_test.go
+++ b/runtimes/google/vtrace/store_test.go
@@ -27,7 +27,7 @@
 	return traces
 }
 
-func recordids(records ...*vtrace.TraceRecord) map[uniqueid.ID]bool {
+func recordids(records ...vtrace.TraceRecord) map[uniqueid.ID]bool {
 	out := make(map[uniqueid.ID]bool)
 	for _, trace := range records {
 		out[trace.ID] = true
diff --git a/security/blessingroots.go b/security/blessingroots.go
index 5f314d8..9883122 100644
--- a/security/blessingroots.go
+++ b/security/blessingroots.go
@@ -11,17 +11,12 @@
 	"veyron.io/veyron/veyron2/security"
 )
 
-const (
-	blessingRootsDataFile = "blessingroots.data"
-	blessingRootsSigFile  = "blessingroots.sig"
-)
-
 // blessingRoots implements security.BlessingRoots.
 type blessingRoots struct {
-	dir    string
-	signer serialization.Signer
-	mu     sync.RWMutex
-	store  map[string][]security.BlessingPattern // GUARDED_BY(mu)
+	persistedData SerializerReaderWriter
+	signer        serialization.Signer
+	mu            sync.RWMutex
+	store         map[string][]security.BlessingPattern // GUARDED_BY(mu)
 }
 
 func storeMapKey(root security.PublicKey) (string, error) {
@@ -93,10 +88,14 @@
 }
 
 func (br *blessingRoots) save() error {
-	if (br.signer == nil) && (br.dir == "") {
+	if (br.signer == nil) && (br.persistedData == nil) {
 		return nil
 	}
-	return encodeAndStore(br.store, br.dir, blessingRootsDataFile, blessingRootsSigFile, br.signer)
+	data, signature, err := br.persistedData.Writers()
+	if err != nil {
+		return err
+	}
+	return encodeAndStore(br.store, data, signature, br.signer)
 }
 
 // newInMemoryBlessingRoots returns an in-memory security.BlessingRoots.
@@ -108,27 +107,26 @@
 	}
 }
 
-// newPersistingBlessingRoots returns a security.BlessingRoots that signs
-// and persists all updates to the provided directory. Signing is carried
-// out using the provided signer.
-//
-// The returned BlessingRoots is initialized from the existing data present
-// in the directory. The data is verified to have been written by a persisting
-// BlessingRoots object constructed from the same signer.
-//
-// Any errors obtained in reading or verifying the data are returned.
-func newPersistingBlessingRoots(directory string, signer serialization.Signer) (security.BlessingRoots, error) {
-	if directory == "" || signer == nil {
-		return nil, errors.New("directory or signer is not specified")
+// newPersistingBlessingRoots returns a security.BlessingRoots for a principal
+// that is initialized with the persisted data. The returned security.BlessingRoots
+// also persists any updates to its state.
+func newPersistingBlessingRoots(persistedData SerializerReaderWriter, signer serialization.Signer) (security.BlessingRoots, error) {
+	if persistedData == nil || signer == nil {
+		return nil, errors.New("persisted data or signer is not specified")
 	}
 	br := &blessingRoots{
-		store:  make(map[string][]security.BlessingPattern),
-		dir:    directory,
-		signer: signer,
+		store:         make(map[string][]security.BlessingPattern),
+		persistedData: persistedData,
+		signer:        signer,
 	}
-
-	if err := decodeFromStorage(&br.store, br.dir, blessingRootsDataFile, blessingRootsSigFile, br.signer.PublicKey()); err != nil {
+	data, signature, err := br.persistedData.Readers()
+	if err != nil {
 		return nil, err
 	}
+	if (data != nil) && (signature != nil) {
+		if err := decodeFromStorage(&br.store, data, signature, br.signer.PublicKey()); err != nil {
+			return nil, err
+		}
+	}
 	return br, nil
 }
diff --git a/security/blessingstore.go b/security/blessingstore.go
index b135580..2a03456 100644
--- a/security/blessingstore.go
+++ b/security/blessingstore.go
@@ -13,11 +13,6 @@
 	"veyron.io/veyron/veyron2/vlog"
 )
 
-const (
-	blessingStoreDataFile = "blessingstore.data"
-	blessingStoreSigFile  = "blessingstore.sig"
-)
-
 var errStoreAddMismatch = errors.New("blessing's public key does not match store's public key")
 
 type persistentState struct {
@@ -33,45 +28,45 @@
 
 // blessingStore implements security.BlessingStore.
 type blessingStore struct {
-	publicKey security.PublicKey
-	dir       string
-	signer    serialization.Signer
-	mu        sync.RWMutex
-	state     persistentState // GUARDED_BY(mu)
+	publicKey     security.PublicKey
+	persistedData SerializerReaderWriter
+	signer        serialization.Signer
+	mu            sync.RWMutex
+	state         persistentState // GUARDED_BY(mu)
 }
 
-func (s *blessingStore) Set(blessings security.Blessings, forPeers security.BlessingPattern) (security.Blessings, error) {
+func (bs *blessingStore) Set(blessings security.Blessings, forPeers security.BlessingPattern) (security.Blessings, error) {
 	if !forPeers.IsValid() {
 		return nil, fmt.Errorf("%q is an invalid BlessingPattern", forPeers)
 	}
-	if blessings != nil && !reflect.DeepEqual(blessings.PublicKey(), s.publicKey) {
+	if blessings != nil && !reflect.DeepEqual(blessings.PublicKey(), bs.publicKey) {
 		return nil, errStoreAddMismatch
 	}
-	s.mu.Lock()
-	defer s.mu.Unlock()
-	old, hadold := s.state.Store[forPeers]
+	bs.mu.Lock()
+	defer bs.mu.Unlock()
+	old, hadold := bs.state.Store[forPeers]
 	if blessings != nil {
-		s.state.Store[forPeers] = blessings
+		bs.state.Store[forPeers] = blessings
 	} else {
-		delete(s.state.Store, forPeers)
+		delete(bs.state.Store, forPeers)
 	}
-	if err := s.save(); err != nil {
+	if err := bs.save(); err != nil {
 		if hadold {
-			s.state.Store[forPeers] = old
+			bs.state.Store[forPeers] = old
 		} else {
-			delete(s.state.Store, forPeers)
+			delete(bs.state.Store, forPeers)
 		}
 		return nil, err
 	}
 	return old, nil
 }
 
-func (s *blessingStore) ForPeer(peerBlessings ...string) security.Blessings {
-	s.mu.RLock()
-	defer s.mu.RUnlock()
+func (bs *blessingStore) ForPeer(peerBlessings ...string) security.Blessings {
+	bs.mu.RLock()
+	defer bs.mu.RUnlock()
 
 	var ret security.Blessings
-	for pattern, blessings := range s.state.Store {
+	for pattern, blessings := range bs.state.Store {
 		if pattern.MatchedBy(peerBlessings...) {
 			if union, err := security.UnionOfBlessings(ret, blessings); err != nil {
 				vlog.Errorf("UnionOfBlessings(%v, %v) failed: %v, dropping the latter from BlessingStore.ForPeers(%v)", ret, blessings, err, peerBlessings)
@@ -83,35 +78,35 @@
 	return ret
 }
 
-func (s *blessingStore) Default() security.Blessings {
-	s.mu.RLock()
-	defer s.mu.RUnlock()
-	if s.state.Default != nil {
-		return s.state.Default
+func (bs *blessingStore) Default() security.Blessings {
+	bs.mu.RLock()
+	defer bs.mu.RUnlock()
+	if bs.state.Default != nil {
+		return bs.state.Default
 	}
-	return s.ForPeer()
+	return bs.ForPeer()
 }
 
-func (s *blessingStore) SetDefault(blessings security.Blessings) error {
-	s.mu.Lock()
-	defer s.mu.Unlock()
-	if !reflect.DeepEqual(blessings.PublicKey(), s.publicKey) {
+func (bs *blessingStore) SetDefault(blessings security.Blessings) error {
+	bs.mu.Lock()
+	defer bs.mu.Unlock()
+	if !reflect.DeepEqual(blessings.PublicKey(), bs.publicKey) {
 		return errStoreAddMismatch
 	}
-	oldDefault := s.state.Default
-	s.state.Default = blessings
-	if err := s.save(); err != nil {
-		s.state.Default = oldDefault
+	oldDefault := bs.state.Default
+	bs.state.Default = blessings
+	if err := bs.save(); err != nil {
+		bs.state.Default = oldDefault
 	}
 	return nil
 }
 
-func (s *blessingStore) PublicKey() security.PublicKey {
-	return s.publicKey
+func (bs *blessingStore) PublicKey() security.PublicKey {
+	return bs.publicKey
 }
 
-func (s *blessingStore) String() string {
-	return fmt.Sprintf("{state: %v, publicKey: %v, dir: %v}", s.state, s.publicKey, s.dir)
+func (bs *blessingStore) String() string {
+	return fmt.Sprintf("{state: %v, publicKey: %v}", bs.state, bs.publicKey)
 }
 
 // DebugString return a human-readable string encoding of the store
@@ -122,22 +117,26 @@
 // <pattern>    : <blessings>
 // ...
 // <pattern>    : <blessings>
-func (br *blessingStore) DebugString() string {
+func (bs *blessingStore) DebugString() string {
 	const format = "%-30s : %s\n"
-	b := bytes.NewBufferString(fmt.Sprintf("Default blessings: %v\n", br.state.Default))
+	b := bytes.NewBufferString(fmt.Sprintf("Default blessings: %v\n", bs.state.Default))
 
 	b.WriteString(fmt.Sprintf(format, "Peer pattern", "Blessings"))
-	for pattern, blessings := range br.state.Store {
+	for pattern, blessings := range bs.state.Store {
 		b.WriteString(fmt.Sprintf(format, pattern, blessings))
 	}
 	return b.String()
 }
 
-func (s *blessingStore) save() error {
-	if (s.signer == nil) && (s.dir == "") {
+func (bs *blessingStore) save() error {
+	if (bs.signer == nil) && (bs.persistedData == nil) {
 		return nil
 	}
-	return encodeAndStore(s.state, s.dir, blessingStoreDataFile, blessingStoreSigFile, s.signer)
+	data, signature, err := bs.persistedData.Writers()
+	if err != nil {
+		return err
+	}
+	return encodeAndStore(bs.state, data, signature, bs.signer)
 }
 
 // newInMemoryBlessingStore returns an in-memory security.BlessingStore for a
@@ -152,33 +151,31 @@
 }
 
 // newPersistingBlessingStore returns a security.BlessingStore for a principal
-// that persists all updates to the specified directory and uses the provided
-// signer to ensure integrity of data read from the filesystem.
-//
-// The returned BlessingStore is initialized from the existing data present in
-// the directory. The data is verified to have been written by a persisting
-// BlessingStore object constructed from the same signer.
-//
-// Any errors obtained in reading or verifying the data are returned.
-func newPersistingBlessingStore(directory string, signer serialization.Signer) (security.BlessingStore, error) {
-	if directory == "" || signer == nil {
-		return nil, errors.New("directory or signer is not specified")
+// that is initialized with the persisted data. The returned security.BlessingStore
+// also persists any updates to its state.
+func newPersistingBlessingStore(persistedData SerializerReaderWriter, signer serialization.Signer) (security.BlessingStore, error) {
+	if persistedData == nil || signer == nil {
+		return nil, errors.New("persisted data or signer is not specified")
 	}
-	s := &blessingStore{
-		publicKey: signer.PublicKey(),
-		state:     persistentState{Store: make(map[security.BlessingPattern]security.Blessings)},
-		dir:       directory,
-		signer:    signer,
+	bs := &blessingStore{
+		publicKey:     signer.PublicKey(),
+		state:         persistentState{Store: make(map[security.BlessingPattern]security.Blessings)},
+		persistedData: persistedData,
+		signer:        signer,
 	}
-
-	if err := decodeFromStorage(&s.state, s.dir, blessingStoreDataFile, blessingStoreSigFile, s.signer.PublicKey()); err != nil {
+	data, signature, err := bs.persistedData.Readers()
+	if err != nil {
 		return nil, err
 	}
-
-	for _, b := range s.state.Store {
-		if !reflect.DeepEqual(b.PublicKey(), s.publicKey) {
-			return nil, fmt.Errorf("directory contains Blessings: %v that are not for the provided PublicKey: %v", b, s.publicKey)
+	if data != nil && signature != nil {
+		if err := decodeFromStorage(&bs.state, data, signature, bs.signer.PublicKey()); err != nil {
+			return nil, err
 		}
 	}
-	return s, nil
+	for _, b := range bs.state.Store {
+		if !reflect.DeepEqual(b.PublicKey(), bs.publicKey) {
+			return nil, fmt.Errorf("directory contains Blessings: %v that are not for the provided PublicKey: %v", b, bs.publicKey)
+		}
+	}
+	return bs, nil
 }
diff --git a/security/principal.go b/security/principal.go
index f0dba02..7247434 100644
--- a/security/principal.go
+++ b/security/principal.go
@@ -9,7 +9,15 @@
 	"veyron.io/veyron/veyron2/security"
 )
 
-const privateKeyFile = "privatekey.pem"
+const (
+	blessingStoreDataFile = "blessingstore.data"
+	blessingStoreSigFile  = "blessingstore.sig"
+
+	blessingRootsDataFile = "blessingroots.data"
+	blessingRootsSigFile  = "blessingroots.sig"
+
+	privateKeyFile = "privatekey.pem"
+)
 
 // NewPrincipal mints a new private key and generates a principal based on
 // this key, storing its BlessingRoots and BlessingStore in memory.
@@ -27,7 +35,7 @@
 	return security.CreatePrincipal(signer, newInMemoryBlessingStore(signer.PublicKey()), newInMemoryBlessingRoots())
 }
 
-// NewPersistentPrincipalForSigner creates a new principal using the provided Signer and a
+// NewPersistentPrincipalFromSigner creates a new principal using the provided Signer and a
 // partial state (i.e., BlessingRoots, BlessingStore) that is read from the provided directory 'dir'.
 // Changes to the partial state are persisted and commited to the same directory; the provided
 // signer isn't persisted: the caller is expected to persist it separately or use the
@@ -100,11 +108,22 @@
 	if err != nil {
 		return nil, fmt.Errorf("failed to create serialization.Signer: %v", err)
 	}
-	roots, err := newPersistingBlessingRoots(dir, serializationSigner)
+	dataFile := path.Join(dir, blessingRootsDataFile)
+	signatureFile := path.Join(dir, blessingRootsSigFile)
+	fs, err := NewFileSerializer(dataFile, signatureFile)
+	if err != nil {
+		return nil, err
+	}
+	roots, err := newPersistingBlessingRoots(fs, serializationSigner)
 	if err != nil {
 		return nil, fmt.Errorf("failed to load BlessingRoots from %q: %v", dir, err)
 	}
-	store, err := newPersistingBlessingStore(dir, serializationSigner)
+	dataFile = path.Join(dir, blessingStoreDataFile)
+	signatureFile = path.Join(dir, blessingStoreSigFile)
+	if fs, err = NewFileSerializer(dataFile, signatureFile); err != nil {
+		return nil, err
+	}
+	store, err := newPersistingBlessingStore(fs, serializationSigner)
 	if err != nil {
 		return nil, fmt.Errorf("failed to load BlessingStore from %q: %v", dir, err)
 	}
diff --git a/security/serializer_reader_writer.go b/security/serializer_reader_writer.go
index 55248e4..633456d 100644
--- a/security/serializer_reader_writer.go
+++ b/security/serializer_reader_writer.go
@@ -2,6 +2,7 @@
 
 import (
 	"io"
+	"os"
 )
 
 // SerializerReaderWriter is a factory for managing the readers and writers used for
@@ -14,3 +15,51 @@
 	// integrity signature.
 	Writers() (data io.WriteCloser, signature io.WriteCloser, err error)
 }
+
+// FileSerializer implements SerializerReaderWriter that persists state to files.
+type FileSerializer struct {
+	data      *os.File
+	signature *os.File
+
+	dataFilePath      string
+	signatureFilePath string
+}
+
+// NewFileSerializer creates a FileSerializer with the given data and signature files.
+func NewFileSerializer(dataFilePath, signatureFilePath string) (*FileSerializer, error) {
+	data, err := os.Open(dataFilePath)
+	if err != nil && !os.IsNotExist(err) {
+		return nil, err
+	}
+	signature, err := os.Open(signatureFilePath)
+	if err != nil && !os.IsNotExist(err) {
+		return nil, err
+	}
+	return &FileSerializer{
+		data:              data,
+		signature:         signature,
+		dataFilePath:      dataFilePath,
+		signatureFilePath: signatureFilePath,
+	}, nil
+}
+
+func (fs *FileSerializer) Readers() (io.ReadCloser, io.ReadCloser, error) {
+	if fs.data == nil || fs.signature == nil {
+		return nil, nil, nil
+	}
+	return fs.data, fs.signature, nil
+}
+
+func (fs *FileSerializer) Writers() (io.WriteCloser, io.WriteCloser, error) {
+	// Remove previous version of the files
+	os.Remove(fs.dataFilePath)
+	os.Remove(fs.signatureFilePath)
+	var err error
+	if fs.data, err = os.Create(fs.dataFilePath); err != nil {
+		return nil, nil, err
+	}
+	if fs.signature, err = os.Create(fs.signatureFilePath); err != nil {
+		return nil, nil, err
+	}
+	return fs.data, fs.signature, nil
+}
diff --git a/security/storage.go b/security/storage.go
index 0dd755b..2fdd8cd 100644
--- a/security/storage.go
+++ b/security/storage.go
@@ -1,32 +1,19 @@
 package security
 
 import (
-	"io/ioutil"
-	"os"
-	"path"
+	"fmt"
+	"io"
 
 	"veyron.io/veyron/veyron/security/serialization"
-
 	"veyron.io/veyron/veyron2/security"
 	"veyron.io/veyron/veyron2/vom"
 )
 
-func encodeAndStore(obj interface{}, dir, dataFile, sigFile string, signer serialization.Signer) error {
-	// Save the object to temporary data and signature files, and then move
-	// those files to the actual data and signature file. This reduces the
-	// risk of loosing all saved data on disk in the event of a Write failure.
-	data, err := ioutil.TempFile(dir, "data")
-	if err != nil {
-		return err
+func encodeAndStore(obj interface{}, data, signature io.WriteCloser, signer serialization.Signer) error {
+	if data == nil || signature == nil {
+		return fmt.Errorf("invalid data/signature handles data:%v sig:%v", data, signature)
 	}
-	defer os.Remove(data.Name())
-	sig, err := ioutil.TempFile(dir, "sig")
-	if err != nil {
-		return err
-	}
-	defer os.Remove(sig.Name())
-
-	swc, err := serialization.NewSigningWriteCloser(data, sig, signer, nil)
+	swc, err := serialization.NewSigningWriteCloser(data, signature, signer, nil)
 	if err != nil {
 		return err
 	}
@@ -34,32 +21,16 @@
 		swc.Close()
 		return err
 	}
-	if err := swc.Close(); err != nil {
-		return err
-	}
-
-	if err := os.Rename(data.Name(), path.Join(dir, dataFile)); err != nil {
-		return err
-	}
-	return os.Rename(sig.Name(), path.Join(dir, sigFile))
+	return swc.Close()
 }
 
-func decodeFromStorage(obj interface{}, dir, dataFile, sigFile string, publicKey security.PublicKey) error {
-	data, dataErr := os.Open(path.Join(dir, dataFile))
-	defer data.Close()
-	sig, sigErr := os.Open(path.Join(dir, sigFile))
-	defer sig.Close()
-
-	switch {
-	case os.IsNotExist(dataErr) && os.IsNotExist(sigErr):
-		return nil
-	case dataErr != nil:
-		return dataErr
-	case sigErr != nil:
-		return sigErr
+func decodeFromStorage(obj interface{}, data, signature io.ReadCloser, publicKey security.PublicKey) error {
+	if data == nil || signature == nil {
+		return fmt.Errorf("invalid data/signature handles data:%v sig:%v", data, signature)
 	}
-
-	vr, err := serialization.NewVerifyingReader(data, sig, publicKey)
+	defer data.Close()
+	defer signature.Close()
+	vr, err := serialization.NewVerifyingReader(data, signature, publicKey)
 	if err != nil {
 		return err
 	}
diff --git a/services/mgmt/debug/dispatcher.go b/services/mgmt/debug/dispatcher.go
index 30a8750..89154c5 100644
--- a/services/mgmt/debug/dispatcher.go
+++ b/services/mgmt/debug/dispatcher.go
@@ -6,22 +6,25 @@
 
 	"veyron.io/veyron/veyron2/ipc"
 	"veyron.io/veyron/veyron2/security"
+	"veyron.io/veyron/veyron2/vtrace"
 
 	logreaderimpl "veyron.io/veyron/veyron/services/mgmt/logreader/impl"
 	pprofimpl "veyron.io/veyron/veyron/services/mgmt/pprof/impl"
 	statsimpl "veyron.io/veyron/veyron/services/mgmt/stats/impl"
+	vtraceimpl "veyron.io/veyron/veyron/services/mgmt/vtrace/impl"
 )
 
 // dispatcher holds the state of the debug dispatcher.
 type dispatcher struct {
 	logsDir string // The root of the logs directory.
 	auth    security.Authorizer
+	store   vtrace.Store
 }
 
 var _ ipc.Dispatcher = (*dispatcher)(nil)
 
-func NewDispatcher(logsDir string, authorizer security.Authorizer) *dispatcher {
-	return &dispatcher{logsDir, authorizer}
+func NewDispatcher(logsDir string, authorizer security.Authorizer, store vtrace.Store) *dispatcher {
+	return &dispatcher{logsDir, authorizer, store}
 }
 
 // The first part of the names of the objects served by this dispatcher.
@@ -41,7 +44,7 @@
 		return NewSignatureInvoker(suffix), d.auth, nil
 	}
 	if suffix == "" {
-		return ipc.VChildrenGlobberInvoker("logs", "pprof", "stats"), d.auth, nil
+		return ipc.VChildrenGlobberInvoker("logs", "pprof", "stats", "vtrace"), d.auth, nil
 	}
 	parts := strings.SplitN(suffix, "/", 2)
 	if len(parts) == 2 {
@@ -59,6 +62,8 @@
 		return pprofimpl.NewInvoker(), d.auth, nil
 	case "stats":
 		return statsimpl.NewStatsInvoker(suffix, 10*time.Second), d.auth, nil
+	case "vtrace":
+		return vtraceimpl.NewVtraceService(d.store), d.auth, nil
 	}
 	return nil, d.auth, nil
 }
diff --git a/services/mgmt/debug/dispatcher_test.go b/services/mgmt/debug/dispatcher_test.go
index 4d6f6b9..2a19ee3 100644
--- a/services/mgmt/debug/dispatcher_test.go
+++ b/services/mgmt/debug/dispatcher_test.go
@@ -2,6 +2,7 @@
 
 import (
 	"fmt"
+	"io"
 	"io/ioutil"
 	"os"
 	"path/filepath"
@@ -17,10 +18,11 @@
 	"veyron.io/veyron/veyron2/rt"
 	"veyron.io/veyron/veyron2/services/mgmt/logreader"
 	"veyron.io/veyron/veyron2/services/mgmt/stats"
-	"veyron.io/veyron/veyron2/services/mounttable"
+	"veyron.io/veyron/veyron2/services/mgmt/vtrace"
 	"veyron.io/veyron/veyron2/verror"
 
 	libstats "veyron.io/veyron/veyron/lib/stats"
+	"veyron.io/veyron/veyron/lib/testutil"
 	"veyron.io/veyron/veyron/profiles"
 )
 
@@ -29,7 +31,7 @@
 	if len(logsDir) == 0 {
 		return "", nil, fmt.Errorf("logs directory missing")
 	}
-	disp := NewDispatcher(logsDir, nil)
+	disp := NewDispatcher(logsDir, nil, rt.VtraceStore())
 	server, err := rt.NewServer()
 	if err != nil {
 		return "", nil, fmt.Errorf("failed to start debug server: %v", err)
@@ -66,44 +68,23 @@
 
 	// Access a logs directory that exists.
 	{
-		ld := mounttable.GlobbableClient(naming.JoinAddressName(endpoint, "debug/logs"))
-		stream, err := ld.Glob(runtime.NewContext(), "*")
+		results, err := testutil.GlobName(naming.JoinAddressName(endpoint, "debug/logs"), "*")
 		if err != nil {
 			t.Errorf("Glob failed: %v", err)
 		}
-		results := []string{}
-		iterator := stream.RecvStream()
-		for count := 0; iterator.Advance(); count++ {
-			results = append(results, iterator.Value().Name)
-		}
 		if len(results) != 1 || results[0] != "test.INFO" {
 			t.Errorf("unexpected result. Got %v, want 'test.INFO'", results)
 		}
-		if err := iterator.Err(); err != nil {
-			t.Errorf("unexpected stream error: %v", iterator.Err())
-		}
-		if err := stream.Finish(); err != nil {
-			t.Errorf("Finish failed: %v", err)
-		}
 	}
 
 	// Access a logs directory that doesn't exist.
 	{
-		ld := mounttable.GlobbableClient(naming.JoinAddressName(endpoint, "debug/logs/nowheretobefound"))
-		stream, err := ld.Glob(runtime.NewContext(), "*")
-		if err != nil {
-			t.Errorf("Glob failed: %v", err)
-		}
-		results := []string{}
-		iterator := stream.RecvStream()
-		for count := 0; iterator.Advance(); count++ {
-			results = append(results, iterator.Value().Name)
-		}
+		results, err := testutil.GlobName(naming.JoinAddressName(endpoint, "debug/logs/nowheretobefound"), "*")
 		if len(results) != 0 {
 			t.Errorf("unexpected result. Got %v, want ''", results)
 		}
-		if expected, got := verror.NoExist, stream.Finish(); !verror.Is(got, expected) {
-			t.Errorf("unexpected error value, got %v, want: %v", got, expected)
+		if expected := verror.NoExist; !verror.Is(err, expected) {
+			t.Errorf("unexpected error value, got %v, want: %v", err, expected)
 		}
 	}
 
@@ -152,6 +133,27 @@
 		}
 	}
 
+	// Access vtrace.
+	{
+		vt := vtrace.StoreClient(naming.JoinAddressName(endpoint, "debug/vtrace"))
+		call, err := vt.AllTraces(runtime.NewContext())
+		if err != nil {
+			t.Errorf("AllTraces failed: %v", err)
+		}
+		ntraces := 0
+		stream := call.RecvStream()
+		for stream.Advance() {
+			stream.Value()
+			ntraces++
+		}
+		if err = stream.Err(); err != nil && err != io.EOF {
+			t.Fatalf("Unexpected error reading trace stream: %s", err)
+		}
+		if ntraces < 1 {
+			t.Errorf("We expected at least one trace, got: %d", ntraces)
+		}
+	}
+
 	// Glob from the root.
 	{
 		ns := rt.R().Namespace()
@@ -204,6 +206,7 @@
 			"logs",
 			"pprof",
 			"stats",
+			"vtrace",
 		}
 		if !reflect.DeepEqual(expected, results) {
 			t.Errorf("unexpected result. Got %v, want %v", results, expected)
@@ -229,6 +232,7 @@
 			"pprof",
 			"stats",
 			"stats/testing/foo",
+			"vtrace",
 		}
 		if !reflect.DeepEqual(expected, results) {
 			t.Errorf("unexpected result. Got %v, want %v", results, expected)
diff --git a/services/mgmt/node/impl/impl_test.go b/services/mgmt/node/impl/impl_test.go
index 7c8c79f..8367a64 100644
--- a/services/mgmt/node/impl/impl_test.go
+++ b/services/mgmt/node/impl/impl_test.go
@@ -36,7 +36,6 @@
 	"veyron.io/veyron/veyron2/services/mgmt/node"
 	"veyron.io/veyron/veyron2/services/mgmt/pprof"
 	"veyron.io/veyron/veyron2/services/mgmt/stats"
-	"veyron.io/veyron/veyron2/services/mounttable"
 	"veyron.io/veyron/veyron2/services/mounttable/types"
 	"veyron.io/veyron/veyron2/verror"
 	"veyron.io/veyron/veyron2/vlog"
@@ -1020,7 +1019,11 @@
 	logFileRemoveErrorFatalWarningRE := regexp.MustCompile("(ERROR|FATAL|WARNING)")
 	statsTrimRE := regexp.MustCompile("/stats/(ipc|system(/start-time.*)?)$")
 	for _, tc := range testcases {
-		results := doGlob(t, tc.name, tc.pattern)
+		results, err := testutil.GlobName(tc.name, tc.pattern)
+		if err != nil {
+			t.Errorf("unexpected glob error for (%q, %q): %v", tc.name, tc.pattern, err)
+			continue
+		}
 		filteredResults := []string{}
 		for _, name := range results {
 			// Keep only the stats object names that match this RE.
@@ -1044,7 +1047,10 @@
 	}
 
 	// Call Size() on the log file objects.
-	files := doGlob(t, "nm", "apps/google naps/"+installID+"/"+instance1ID+"/logs/*")
+	files, err := testutil.GlobName("nm", "apps/google naps/"+installID+"/"+instance1ID+"/logs/*")
+	if err != nil {
+		t.Errorf("unexpected glob error: %v", err)
+	}
 	if want, got := 4, len(files); got < want {
 		t.Errorf("Unexpected number of matches. Got %d, want at least %d", got, want)
 	}
@@ -1057,7 +1063,10 @@
 	}
 
 	// Call Value() on some of the stats objects.
-	objects := doGlob(t, "nm", "apps/google naps/"+installID+"/"+instance1ID+"/stats/system/start-time*")
+	objects, err := testutil.GlobName("nm", "apps/google naps/"+installID+"/"+instance1ID+"/stats/system/start-time*")
+	if err != nil {
+		t.Errorf("unexpected glob error: %v", err)
+	}
 	if want, got := 2, len(objects); got != want {
 		t.Errorf("Unexpected number of matches. Got %d, want %d", got, want)
 	}
@@ -1086,26 +1095,6 @@
 	}
 }
 
-func doGlob(t *testing.T, name, pattern string) []string {
-	c := mounttable.GlobbableClient(name)
-	stream, err := c.Glob(rt.R().NewContext(), pattern)
-	if err != nil {
-		t.Errorf("Glob failed: %v", err)
-	}
-	results := []string{}
-	iterator := stream.RecvStream()
-	for iterator.Advance() {
-		results = append(results, iterator.Value().Name)
-	}
-	if err := iterator.Err(); err != nil {
-		t.Errorf("unexpected stream error: %v", err)
-	}
-	if err := stream.Finish(); err != nil {
-		t.Errorf("Finish failed: %v", err)
-	}
-	return results
-}
-
 func listAndVerifyAssociations(t *testing.T, stub node.NodeClientMethods, run veyron2.Runtime, expected []node.Association) {
 	assocs, err := stub.ListAssociations(run.NewContext())
 	if err != nil {
diff --git a/services/mgmt/vtrace/impl/vtrace_invoker.go b/services/mgmt/vtrace/impl/vtrace_invoker.go
new file mode 100644
index 0000000..d9cbe0a
--- /dev/null
+++ b/services/mgmt/vtrace/impl/vtrace_invoker.go
@@ -0,0 +1,40 @@
+package impl
+
+import (
+	"veyron.io/veyron/veyron2/ipc"
+	"veyron.io/veyron/veyron2/uniqueid"
+	"veyron.io/veyron/veyron2/verror2"
+	"veyron.io/veyron/veyron2/vtrace"
+)
+
+type vtraceServer struct {
+	store vtrace.Store
+}
+
+func (v *vtraceServer) Trace(call ipc.ServerCall, id uniqueid.ID) (vtrace.TraceRecord, error) {
+	tr := v.store.TraceRecord(id)
+	if tr == nil {
+		return vtrace.TraceRecord{}, verror2.Make(verror2.NoExist, call, "No trace with id %x", id)
+	}
+	return *tr, nil
+}
+
+func (v *vtraceServer) AllTraces(call ipc.ServerCall) error {
+	// TODO(mattr): Consider changing the store to allow us to iterate through traces
+	// when there are many.
+	traces := v.store.TraceRecords()
+	for i := range traces {
+		if err := call.Send(traces[i]); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func NewVtraceInvoker(store vtrace.Store) ipc.Invoker {
+	return ipc.ReflectInvoker(&vtraceServer{store})
+}
+
+func NewVtraceService(store vtrace.Store) interface{} {
+	return &vtraceServer{store}
+}
diff --git a/services/mgmt/vtrace/impl/vtrace_invoker_test.go b/services/mgmt/vtrace/impl/vtrace_invoker_test.go
new file mode 100644
index 0000000..c967d79
--- /dev/null
+++ b/services/mgmt/vtrace/impl/vtrace_invoker_test.go
@@ -0,0 +1,90 @@
+package impl_test
+
+import (
+	"io"
+	"testing"
+
+	"veyron.io/veyron/veyron2"
+	"veyron.io/veyron/veyron2/ipc"
+	"veyron.io/veyron/veyron2/naming"
+	"veyron.io/veyron/veyron2/rt"
+	service "veyron.io/veyron/veyron2/services/mgmt/vtrace"
+	"veyron.io/veyron/veyron2/vtrace"
+
+	"veyron.io/veyron/veyron/profiles"
+	"veyron.io/veyron/veyron/services/mgmt/vtrace/impl"
+)
+
+func setup(t *testing.T) (string, ipc.Server, veyron2.Runtime) {
+	runtime, err := rt.New()
+	if err != nil {
+		t.Fatalf("Could not create runtime: %s", err)
+	}
+
+	server, err := runtime.NewServer()
+	if err != nil {
+		t.Fatalf("Could not create server: %s", err)
+	}
+	endpoint, err := server.Listen(profiles.LocalListenSpec)
+	if err != nil {
+		t.Fatalf("Listen failed: %s", err)
+	}
+	if err := server.Serve("", impl.NewVtraceService(runtime.VtraceStore()), nil); err != nil {
+		t.Fatalf("Serve failed: %s", err)
+	}
+	return endpoint.String(), server, runtime
+}
+
+func TestVtraceInvoker(t *testing.T) {
+	endpoint, server, runtime := setup(t)
+	defer server.Stop()
+
+	sctx := runtime.NewContext()
+	sctx, span := runtime.WithNewSpan(sctx, "The Span")
+	span.Trace().ForceCollect()
+	span.Finish()
+	id := span.Trace().ID()
+
+	client := service.StoreClient(naming.JoinAddressName(endpoint, ""))
+
+	trace, err := client.Trace(runtime.NewContext(), id)
+	if err != nil {
+		t.Fatalf("Unexpected error getting trace: %s", err)
+	}
+	if len(trace.Spans) != 1 {
+		t.Errorf("Returned trace should have 1 span, found %#v", trace)
+	}
+	if trace.Spans[0].Name != "The Span" {
+		t.Errorf("Returned span has wrong name: %#v", trace)
+	}
+
+	call, err := client.AllTraces(runtime.NewContext())
+	if err != nil {
+		t.Fatalf("Unexpected error getting traces: %s", err)
+	}
+	ntraces := 0
+	stream := call.RecvStream()
+	var tr *vtrace.TraceRecord
+	for stream.Advance() {
+		trace := stream.Value()
+		if trace.ID == id {
+			tr = &trace
+		}
+		ntraces++
+	}
+	if err = stream.Err(); err != nil && err != io.EOF {
+		t.Fatalf("Unexpected error reading trace stream: %s", err)
+	}
+	if ntraces < 2 {
+		t.Fatalf("Expected at least 2 traces, got %#v", ntraces)
+	}
+	if tr == nil {
+		t.Fatalf("Desired trace %x not found.", id)
+	}
+	if len(tr.Spans) != 1 {
+		t.Errorf("Returned trace should have 1 span, found %#v", tr)
+	}
+	if tr.Spans[0].Name != "The Span" {
+		t.Fatalf("Returned span has wrong name: %#v", tr)
+	}
+}
diff --git a/tools/debug/test.sh b/tools/debug/test.sh
index afb886e..8c38207 100755
--- a/tools/debug/test.sh
+++ b/tools/debug/test.sh
@@ -39,7 +39,8 @@
     || (dumplogs "${DBGLOG}"; shell_test::fail "line ${LINENO}: failed to run debug")
   WANT="${EP}/__debug/logs
 ${EP}/__debug/pprof
-${EP}/__debug/stats"
+${EP}/__debug/stats
+${EP}/__debug/vtrace"
   shell_test::assert_eq "${GOT}" "${WANT}" "${LINENO}"
 
   # Test logs glob.