prometheus/index.go

777 lines
18 KiB
Go
Raw Normal View History

2016-12-09 11:45:46 -08:00
package tsdb
import (
"bufio"
2016-12-09 12:23:34 -08:00
"encoding/binary"
"fmt"
"hash"
2016-12-09 11:45:46 -08:00
"hash/crc32"
"io"
"os"
"path/filepath"
2016-12-09 12:23:34 -08:00
"sort"
"strings"
"github.com/coreos/etcd/pkg/fileutil"
2016-12-21 00:39:01 -08:00
"github.com/fabxc/tsdb/labels"
"github.com/pkg/errors"
2016-12-09 11:45:46 -08:00
)
const (
// MagicIndex 4 bytes at the head of an index file.
MagicIndex = 0xBAAAD700
indexFormatV1 = 1
)
const compactionPageBytes = minSectorSize * 64
2016-12-09 11:45:46 -08:00
// IndexWriter serialized the index for a block of series data.
// The methods must generally be called in order they are specified.
type IndexWriter interface {
2016-12-10 00:44:00 -08:00
// AddSeries populates the index writer witha series and its offsets
// of chunks that the index can reference.
// The reference number is used to resolve a series against the postings
// list iterator. It only has to be available during the write processing.
AddSeries(ref uint32, l labels.Labels, chunks ...*ChunkMeta) error
2016-12-09 11:45:46 -08:00
// WriteLabelIndex serializes an index from label names to values.
// The passed in values chained tuples of strings of the length of names.
WriteLabelIndex(names []string, values []string) error
// WritePostings writes a postings list for a single label pair.
2016-12-14 12:58:29 -08:00
WritePostings(name, value string, it Postings) error
2016-12-09 11:45:46 -08:00
2016-12-09 12:23:34 -08:00
// Close writes any finalization and closes theresources associated with
2016-12-09 11:45:46 -08:00
// the underlying writer.
Close() error
}
2016-12-10 00:44:00 -08:00
type indexWriterSeries struct {
2016-12-21 00:39:01 -08:00
labels labels.Labels
chunks []*ChunkMeta // series file offset of chunks
offset uint32 // index file offset of series reference
2016-12-10 00:44:00 -08:00
}
2016-12-09 11:45:46 -08:00
// indexWriter implements the IndexWriter interface for the standard
// serialization format.
type indexWriter struct {
f *os.File
bufw *bufio.Writer
n int64
started bool
2016-12-09 11:45:46 -08:00
2017-03-02 14:35:02 -08:00
// Reusable memory.
b []byte
uint32s []uint32
series map[uint32]*indexWriterSeries
2016-12-09 12:40:38 -08:00
symbols map[string]uint32 // symbol offsets
labelIndexes []hashEntry // label index offsets
2016-12-10 00:44:00 -08:00
postings []hashEntry // postings lists offsets
crc32 hash.Hash
2016-12-09 12:23:34 -08:00
}
func newIndexWriter(dir string) (*indexWriter, error) {
df, err := fileutil.OpenDir(dir)
if err != nil {
return nil, err
}
f, err := os.OpenFile(filepath.Join(dir, "index"), os.O_CREATE|os.O_WRONLY, 0666)
if err != nil {
return nil, err
}
if err := fileutil.Fsync(df); err != nil {
return nil, errors.Wrap(err, "sync dir")
}
iw := &indexWriter{
2017-03-02 14:35:02 -08:00
f: f,
bufw: bufio.NewWriterSize(f, 1<<22),
n: 0,
// Reusable memory.
b: make([]byte, 0, 1<<23),
uint32s: make([]uint32, 0, 1<<15),
// Caches.
symbols: make(map[string]uint32, 1<<13),
series: make(map[uint32]*indexWriterSeries, 1<<16),
crc32: crc32.New(crc32.MakeTable(crc32.Castagnoli)),
2016-12-09 12:23:34 -08:00
}
if err := iw.writeMeta(); err != nil {
return nil, err
}
return iw, nil
2016-12-09 12:23:34 -08:00
}
func (w *indexWriter) write(wr io.Writer, b []byte) error {
n, err := wr.Write(b)
w.n += int64(n)
return err
}
2016-12-09 13:36:31 -08:00
// section writes a CRC32 checksummed section of length l and guarded by flag.
2017-03-02 14:35:02 -08:00
func (w *indexWriter) section(l int, flag byte, f func(w io.Writer) error) error {
w.crc32.Reset()
wr := io.MultiWriter(w.crc32, w.bufw)
2016-12-09 13:36:31 -08:00
b := [5]byte{flag, 0, 0, 0, 0}
2017-03-02 14:35:02 -08:00
binary.BigEndian.PutUint32(b[1:], uint32(l))
if err := w.write(wr, b[:]); err != nil {
return errors.Wrap(err, "writing header")
2016-12-09 13:36:31 -08:00
}
if err := f(wr); err != nil {
return errors.Wrap(err, "write contents")
}
if err := w.write(w.bufw, w.crc32.Sum(nil)); err != nil {
return errors.Wrap(err, "writing checksum")
2016-12-09 13:36:31 -08:00
}
return nil
2016-12-09 13:36:31 -08:00
}
2016-12-09 12:23:34 -08:00
func (w *indexWriter) writeMeta() error {
2016-12-11 06:49:24 -08:00
b := [8]byte{}
binary.BigEndian.PutUint32(b[:4], MagicIndex)
b[4] = flagStd
2016-12-09 12:23:34 -08:00
return w.write(w.bufw, b[:])
2016-12-09 11:45:46 -08:00
}
func (w *indexWriter) AddSeries(ref uint32, lset labels.Labels, chunks ...*ChunkMeta) error {
if _, ok := w.series[ref]; ok {
return errors.Errorf("series with reference %d already added", ref)
}
2016-12-09 12:23:34 -08:00
// Populate the symbol table from all label sets we have to reference.
for _, l := range lset {
w.symbols[l.Name] = 0
w.symbols[l.Value] = 0
}
2016-12-10 00:44:00 -08:00
w.series[ref] = &indexWriterSeries{
labels: lset,
chunks: chunks,
2016-12-10 00:44:00 -08:00
}
return nil
2016-12-09 11:45:46 -08:00
}
2016-12-09 12:23:34 -08:00
func (w *indexWriter) writeSymbols() error {
// Generate sorted list of strings we will store as reference table.
symbols := make([]string, 0, len(w.symbols))
for s := range w.symbols {
symbols = append(symbols, s)
}
sort.Strings(symbols)
// The start of the section plus a 5 byte section header are our base.
// TODO(fabxc): switch to relative offsets and hold sections in a TOC.
base := uint32(w.n) + 5
2016-12-09 13:27:43 -08:00
buf := [binary.MaxVarintLen32]byte{}
2017-03-02 14:35:02 -08:00
w.b = append(w.b[:0], flagStd)
2016-12-09 12:23:34 -08:00
for _, s := range symbols {
2017-03-02 14:35:02 -08:00
w.symbols[s] = base + uint32(len(w.b))
2016-12-09 13:27:43 -08:00
n := binary.PutUvarint(buf[:], uint64(len(s)))
2017-03-02 14:35:02 -08:00
w.b = append(w.b, buf[:n]...)
w.b = append(w.b, s...)
}
2016-12-09 12:23:34 -08:00
2017-03-02 14:35:02 -08:00
return w.section(len(w.b), flagStd, func(wr io.Writer) error {
return w.write(wr, w.b)
2016-12-09 13:36:31 -08:00
})
2016-12-09 13:27:43 -08:00
}
2017-03-02 14:35:02 -08:00
type indexWriterSeriesSlice []*indexWriterSeries
func (s indexWriterSeriesSlice) Len() int { return len(s) }
func (s indexWriterSeriesSlice) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
func (s indexWriterSeriesSlice) Less(i, j int) bool {
return labels.Compare(s[i].labels, s[j].labels) < 0
}
2016-12-09 13:27:43 -08:00
func (w *indexWriter) writeSeries() error {
// Series must be stored sorted along their labels.
2017-03-02 14:35:02 -08:00
series := make(indexWriterSeriesSlice, 0, len(w.series))
2016-12-09 13:27:43 -08:00
2016-12-10 00:44:00 -08:00
for _, s := range w.series {
series = append(series, s)
}
2017-03-02 14:35:02 -08:00
sort.Sort(series)
// Current end of file plus 5 bytes for section header.
// TODO(fabxc): switch to relative offsets.
base := uint32(w.n) + 5
2017-03-02 14:35:02 -08:00
w.b = w.b[:0]
buf := make([]byte, binary.MaxVarintLen64)
for _, s := range series {
// Write label set symbol references.
2017-03-02 14:35:02 -08:00
s.offset = base + uint32(len(w.b))
2016-12-10 00:44:00 -08:00
n := binary.PutUvarint(buf, uint64(len(s.labels)))
2017-03-02 14:35:02 -08:00
w.b = append(w.b, buf[:n]...)
2016-12-09 13:27:43 -08:00
2016-12-10 00:44:00 -08:00
for _, l := range s.labels {
n = binary.PutUvarint(buf, uint64(w.symbols[l.Name]))
2017-03-02 14:35:02 -08:00
w.b = append(w.b, buf[:n]...)
n = binary.PutUvarint(buf, uint64(w.symbols[l.Value]))
2017-03-02 14:35:02 -08:00
w.b = append(w.b, buf[:n]...)
}
// Write chunks meta data including reference into chunk file.
n = binary.PutUvarint(buf, uint64(len(s.chunks)))
2017-03-02 14:35:02 -08:00
w.b = append(w.b, buf[:n]...)
for _, c := range s.chunks {
n = binary.PutVarint(buf, c.MinTime)
2017-03-02 14:35:02 -08:00
w.b = append(w.b, buf[:n]...)
n = binary.PutVarint(buf, c.MaxTime)
2017-03-02 14:35:02 -08:00
w.b = append(w.b, buf[:n]...)
n = binary.PutUvarint(buf, uint64(c.Ref))
2017-03-02 14:35:02 -08:00
w.b = append(w.b, buf[:n]...)
2016-12-09 13:27:43 -08:00
}
}
2017-03-02 14:35:02 -08:00
return w.section(len(w.b), flagStd, func(wr io.Writer) error {
return w.write(wr, w.b)
2016-12-09 13:36:31 -08:00
})
2016-12-09 11:45:46 -08:00
}
func (w *indexWriter) init() error {
if err := w.writeSymbols(); err != nil {
return err
}
if err := w.writeSeries(); err != nil {
return err
}
w.started = true
return nil
}
2016-12-09 11:45:46 -08:00
func (w *indexWriter) WriteLabelIndex(names []string, values []string) error {
2017-01-19 05:01:38 -08:00
if !w.started {
if err := w.init(); err != nil {
2017-01-19 05:01:38 -08:00
return err
}
}
valt, err := newStringTuples(values, len(names))
if err != nil {
return err
2016-12-09 12:40:38 -08:00
}
sort.Sort(valt)
2016-12-09 12:40:38 -08:00
w.labelIndexes = append(w.labelIndexes, hashEntry{
name: strings.Join(names, string(sep)),
offset: uint32(w.n),
})
buf := make([]byte, binary.MaxVarintLen32)
n := binary.PutUvarint(buf, uint64(len(names)))
2017-03-02 14:35:02 -08:00
l := n + len(values)*4
2016-12-09 13:36:31 -08:00
return w.section(l, flagStd, func(wr io.Writer) error {
// First byte indicates tuple size for index.
if err := w.write(wr, buf[:n]); err != nil {
return err
}
for _, v := range valt.s {
binary.BigEndian.PutUint32(buf, w.symbols[v])
2016-12-09 12:40:38 -08:00
if err := w.write(wr, buf[:4]); err != nil {
2016-12-09 13:36:31 -08:00
return err
}
2016-12-09 12:40:38 -08:00
}
2016-12-09 13:36:31 -08:00
return nil
})
2016-12-09 11:45:46 -08:00
}
2016-12-14 12:58:29 -08:00
func (w *indexWriter) WritePostings(name, value string, it Postings) error {
if !w.started {
if err := w.init(); err != nil {
return err
}
}
2016-12-10 00:44:00 -08:00
key := name + string(sep) + value
w.postings = append(w.postings, hashEntry{
name: key,
offset: uint32(w.n),
})
// Order of the references in the postings list does not imply order
// of the series references within the persisted block they are mapped to.
// We have to sort the new references again.
2017-03-02 14:35:02 -08:00
refs := w.uint32s[:0]
2016-12-10 00:44:00 -08:00
for it.Next() {
2017-01-02 04:27:52 -08:00
s, ok := w.series[it.At()]
if !ok {
2017-01-02 04:27:52 -08:00
return errors.Errorf("series for reference %d not found", it.At())
}
refs = append(refs, s.offset)
}
if err := it.Err(); err != nil {
return err
}
2017-03-02 14:35:02 -08:00
sort.Sort(uint32slice(refs))
w.b = w.b[:0]
buf := make([]byte, 4)
for _, r := range refs {
2017-03-02 14:35:02 -08:00
binary.BigEndian.PutUint32(buf, r)
w.b = append(w.b, buf...)
2016-12-10 00:44:00 -08:00
}
2017-03-02 14:35:02 -08:00
w.uint32s = refs[:0]
return w.section(len(w.b), flagStd, func(wr io.Writer) error {
return w.write(wr, w.b)
2016-12-10 00:44:00 -08:00
})
2016-12-09 11:45:46 -08:00
}
2017-03-02 14:35:02 -08:00
type uint32slice []uint32
func (s uint32slice) Len() int { return len(s) }
func (s uint32slice) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
func (s uint32slice) Less(i, j int) bool { return s[i] < s[j] }
type hashEntry struct {
name string
offset uint32
}
func (w *indexWriter) writeHashmap(h []hashEntry) error {
2017-03-02 14:35:02 -08:00
w.b = w.b[:0]
buf := [binary.MaxVarintLen32]byte{}
for _, e := range h {
n := binary.PutUvarint(buf[:], uint64(len(e.name)))
2017-03-02 14:35:02 -08:00
w.b = append(w.b, buf[:n]...)
w.b = append(w.b, e.name...)
n = binary.PutUvarint(buf[:], uint64(e.offset))
2017-03-02 14:35:02 -08:00
w.b = append(w.b, buf[:n]...)
}
2017-03-02 14:35:02 -08:00
return w.section(len(w.b), flagStd, func(wr io.Writer) error {
return w.write(wr, w.b)
2016-12-09 13:36:31 -08:00
})
}
2016-12-10 00:44:00 -08:00
func (w *indexWriter) finalize() error {
// Write out hash maps to jump to correct label index and postings sections.
lo := uint32(w.n)
if err := w.writeHashmap(w.labelIndexes); err != nil {
2016-12-10 00:44:00 -08:00
return err
}
po := uint32(w.n)
if err := w.writeHashmap(w.postings); err != nil {
return err
}
2016-12-10 00:44:00 -08:00
// Terminate index file with offsets to hashmaps. This is the entry Pointer
// for any index query.
// TODO(fabxc): also store offset to series section to allow plain
// iteration over all existing series?
b := [8]byte{}
binary.BigEndian.PutUint32(b[:4], lo)
binary.BigEndian.PutUint32(b[4:], po)
2016-12-10 00:44:00 -08:00
return w.write(w.bufw, b[:])
}
2016-12-09 11:45:46 -08:00
func (w *indexWriter) Close() error {
2017-01-09 09:34:29 -08:00
if err := w.finalize(); err != nil {
return err
}
if err := w.bufw.Flush(); err != nil {
return err
}
if err := fileutil.Fsync(w.f); err != nil {
return err
}
return w.f.Close()
2016-12-09 11:45:46 -08:00
}
// IndexReader provides reading access of serialized index data.
type IndexReader interface {
// LabelValues returns the possible label values
LabelValues(names ...string) (StringTuples, error)
// Postings returns the postings list iterator for the label pair.
Postings(name, value string) (Postings, error)
// Series returns the series for the given reference.
Series(ref uint32) (labels.Labels, []*ChunkMeta, error)
// LabelIndices returns the label pairs for which indices exist.
LabelIndices() ([][]string, error)
// Close released the underlying resources of the reader.
Close() error
}
// StringTuples provides access to a sorted list of string tuples.
type StringTuples interface {
// Total number of tuples in the list.
Len() int
// At returns the tuple at position i.
At(i int) ([]string, error)
}
type indexReader struct {
// The underlying byte slice holding the encoded series data.
b []byte
// Close that releases the underlying resources of the byte slice.
c io.Closer
// Cached hashmaps of section offsets.
labels map[string]uint32
postings map[string]uint32
}
var (
errInvalidSize = fmt.Errorf("invalid size")
errInvalidFlag = fmt.Errorf("invalid flag")
)
// newIndexReader returns a new indexReader on the given directory.
func newIndexReader(dir string) (*indexReader, error) {
f, err := openMmapFile(filepath.Join(dir, "index"))
if err != nil {
return nil, err
}
r := &indexReader{b: f.b, c: f}
// Verify magic number.
if len(f.b) < 4 {
return nil, errors.Wrap(errInvalidSize, "index header")
}
if m := binary.BigEndian.Uint32(r.b[:4]); m != MagicIndex {
return nil, errors.Errorf("invalid magic number %x", m)
}
// The last two 4 bytes hold the pointers to the hashmaps.
loff := binary.BigEndian.Uint32(r.b[len(r.b)-8 : len(r.b)-4])
poff := binary.BigEndian.Uint32(r.b[len(r.b)-4:])
flag, b, err := r.section(loff)
if err != nil {
return nil, errors.Wrapf(err, "label index hashmap section at %d", loff)
}
if r.labels, err = readHashmap(flag, b); err != nil {
return nil, errors.Wrap(err, "read label index hashmap")
}
flag, b, err = r.section(poff)
if err != nil {
return nil, errors.Wrapf(err, "postings hashmap section at %d", loff)
}
if r.postings, err = readHashmap(flag, b); err != nil {
return nil, errors.Wrap(err, "read postings hashmap")
}
return r, nil
}
func readHashmap(flag byte, b []byte) (map[string]uint32, error) {
if flag != flagStd {
return nil, errInvalidFlag
}
h := make(map[string]uint32, 512)
for len(b) > 0 {
l, n := binary.Uvarint(b)
if n < 1 {
return nil, errors.Wrap(errInvalidSize, "read key length")
}
b = b[n:]
if len(b) < int(l) {
return nil, errors.Wrap(errInvalidSize, "read key")
}
s := string(b[:l])
b = b[l:]
o, n := binary.Uvarint(b)
if n < 1 {
return nil, errors.Wrap(errInvalidSize, "read offset value")
}
b = b[n:]
h[s] = uint32(o)
}
return h, nil
}
func (r *indexReader) Close() error {
return r.c.Close()
}
func (r *indexReader) section(o uint32) (byte, []byte, error) {
b := r.b[o:]
if len(b) < 5 {
return 0, nil, errors.Wrap(errInvalidSize, "read header")
}
flag := b[0]
l := binary.BigEndian.Uint32(b[1:5])
b = b[5:]
// b must have the given length plus 4 bytes for the CRC32 checksum.
if len(b) < int(l)+4 {
return 0, nil, errors.Wrap(errInvalidSize, "section content")
}
return flag, b[:l], nil
}
func (r *indexReader) lookupSymbol(o uint32) (string, error) {
if int(o) > len(r.b) {
return "", errors.Errorf("invalid symbol offset %d", o)
}
l, n := binary.Uvarint(r.b[o:])
if n < 0 {
return "", errors.New("reading symbol length failed")
}
end := int(o) + n + int(l)
if end > len(r.b) {
return "", errors.New("invalid length")
}
b := r.b[int(o)+n : end]
return yoloString(b), nil
}
func (r *indexReader) LabelValues(names ...string) (StringTuples, error) {
key := strings.Join(names, string(sep))
off, ok := r.labels[key]
if !ok {
return nil, fmt.Errorf("label index doesn't exist")
}
flag, b, err := r.section(off)
if err != nil {
return nil, errors.Wrapf(err, "section at %d", off)
}
if flag != flagStd {
return nil, errInvalidFlag
}
l, n := binary.Uvarint(b)
if n < 1 {
return nil, errors.Wrap(errInvalidSize, "read label index size")
}
st := &serializedStringTuples{
l: int(l),
b: b[n:],
lookup: r.lookupSymbol,
}
return st, nil
}
func (r *indexReader) LabelIndices() ([][]string, error) {
res := [][]string{}
for s := range r.labels {
res = append(res, strings.Split(s, string(sep)))
}
return res, nil
}
func (r *indexReader) Series(ref uint32) (labels.Labels, []*ChunkMeta, error) {
k, n := binary.Uvarint(r.b[ref:])
if n < 1 {
return nil, nil, errors.Wrap(errInvalidSize, "number of labels")
}
b := r.b[int(ref)+n:]
lbls := make(labels.Labels, 0, k)
for i := 0; i < 2*int(k); i += 2 {
o, m := binary.Uvarint(b)
if m < 1 {
return nil, nil, errors.Wrap(errInvalidSize, "symbol offset")
}
n, err := r.lookupSymbol(uint32(o))
if err != nil {
return nil, nil, errors.Wrap(err, "symbol lookup")
}
b = b[m:]
o, m = binary.Uvarint(b)
if m < 1 {
return nil, nil, errors.Wrap(errInvalidSize, "symbol offset")
}
v, err := r.lookupSymbol(uint32(o))
if err != nil {
return nil, nil, errors.Wrap(err, "symbol lookup")
}
b = b[m:]
lbls = append(lbls, labels.Label{
Name: n,
Value: v,
})
}
// Read the chunks meta data.
k, n = binary.Uvarint(b)
if n < 1 {
return nil, nil, errors.Wrap(errInvalidSize, "number of chunks")
}
b = b[n:]
chunks := make([]*ChunkMeta, 0, k)
for i := 0; i < int(k); i++ {
firstTime, n := binary.Varint(b)
if n < 1 {
return nil, nil, errors.Wrap(errInvalidSize, "first time")
}
b = b[n:]
lastTime, n := binary.Varint(b)
if n < 1 {
return nil, nil, errors.Wrap(errInvalidSize, "last time")
}
b = b[n:]
o, n := binary.Uvarint(b)
if n < 1 {
return nil, nil, errors.Wrap(errInvalidSize, "chunk offset")
}
b = b[n:]
chunks = append(chunks, &ChunkMeta{
Ref: o,
MinTime: firstTime,
MaxTime: lastTime,
})
}
return lbls, chunks, nil
}
func (r *indexReader) Postings(name, value string) (Postings, error) {
key := name + string(sep) + value
off, ok := r.postings[key]
if !ok {
return nil, ErrNotFound
}
flag, b, err := r.section(off)
if err != nil {
return nil, errors.Wrapf(err, "section at %d", off)
}
if flag != flagStd {
return nil, errors.Wrapf(errInvalidFlag, "section at %d", off)
}
// TODO(fabxc): just read into memory as an intermediate solution.
// Add iterator over serialized data.
var l []uint32
for len(b) > 0 {
if len(b) < 4 {
return nil, errors.Wrap(errInvalidSize, "plain postings entry")
}
l = append(l, binary.BigEndian.Uint32(b[:4]))
b = b[4:]
}
return &listPostings{list: l, idx: -1}, nil
}
type stringTuples struct {
l int // tuple length
s []string // flattened tuple entries
}
func newStringTuples(s []string, l int) (*stringTuples, error) {
if len(s)%l != 0 {
return nil, errors.Wrap(errInvalidSize, "string tuple list")
}
return &stringTuples{s: s, l: l}, nil
}
func (t *stringTuples) Len() int { return len(t.s) / t.l }
func (t *stringTuples) At(i int) ([]string, error) { return t.s[i : i+t.l], nil }
func (t *stringTuples) Swap(i, j int) {
c := make([]string, t.l)
copy(c, t.s[i:i+t.l])
for k := 0; k < t.l; k++ {
t.s[i+k] = t.s[j+k]
t.s[j+k] = c[k]
}
}
func (t *stringTuples) Less(i, j int) bool {
for k := 0; k < t.l; k++ {
d := strings.Compare(t.s[i+k], t.s[j+k])
if d < 0 {
return true
}
if d > 0 {
return false
}
}
return false
}
type serializedStringTuples struct {
l int
b []byte
lookup func(uint32) (string, error)
}
func (t *serializedStringTuples) Len() int {
// TODO(fabxc): Cache this?
return len(t.b) / (4 * t.l)
}
func (t *serializedStringTuples) At(i int) ([]string, error) {
if len(t.b) < (i+t.l)*4 {
return nil, errInvalidSize
}
res := make([]string, 0, t.l)
for k := 0; k < t.l; k++ {
offset := binary.BigEndian.Uint32(t.b[(i+k)*4:])
s, err := t.lookup(offset)
if err != nil {
return nil, errors.Wrap(err, "symbol lookup")
}
res = append(res, s)
}
return res, nil
}