261 lines
6.6 KiB
Go
261 lines
6.6 KiB
Go
|
// Licensed to Elasticsearch B.V. under one or more contributor
|
||
|
// license agreements. See the NOTICE file distributed with
|
||
|
// this work for additional information regarding copyright
|
||
|
// ownership. Elasticsearch B.V. licenses this file to you under
|
||
|
// the Apache License, Version 2.0 (the "License"); you may
|
||
|
// not use this file except in compliance with the License.
|
||
|
// You may obtain a copy of the License at
|
||
|
//
|
||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||
|
//
|
||
|
// Unless required by applicable law or agreed to in writing,
|
||
|
// software distributed under the License is distributed on an
|
||
|
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||
|
// KIND, either express or implied. See the License for the
|
||
|
// specific language governing permissions and limitations
|
||
|
// under the License.
|
||
|
|
||
|
//+build linux,cgo
|
||
|
|
||
|
package reader
|
||
|
|
||
|
import (
|
||
|
"fmt"
|
||
|
"io"
|
||
|
"os"
|
||
|
"strconv"
|
||
|
"strings"
|
||
|
"time"
|
||
|
|
||
|
"github.com/coreos/go-systemd/sdjournal"
|
||
|
"github.com/pkg/errors"
|
||
|
|
||
|
"github.com/elastic/beats/journalbeat/checkpoint"
|
||
|
"github.com/elastic/beats/journalbeat/cmd/instance"
|
||
|
"github.com/elastic/beats/libbeat/beat"
|
||
|
"github.com/elastic/beats/libbeat/common"
|
||
|
"github.com/elastic/beats/libbeat/logp"
|
||
|
)
|
||
|
|
||
|
// Reader reads entries from journal(s).
|
||
|
type Reader struct {
|
||
|
journal *sdjournal.Journal
|
||
|
config Config
|
||
|
done chan struct{}
|
||
|
logger *logp.Logger
|
||
|
backoff *common.Backoff
|
||
|
}
|
||
|
|
||
|
// New creates a new journal reader and moves the FP to the configured position.
|
||
|
func New(c Config, done chan struct{}, state checkpoint.JournalState, logger *logp.Logger) (*Reader, error) {
|
||
|
f, err := os.Stat(c.Path)
|
||
|
if err != nil {
|
||
|
return nil, errors.Wrap(err, "failed to open file")
|
||
|
}
|
||
|
|
||
|
var j *sdjournal.Journal
|
||
|
if f.IsDir() {
|
||
|
j, err = sdjournal.NewJournalFromDir(c.Path)
|
||
|
if err != nil {
|
||
|
return nil, errors.Wrap(err, "failed to open journal directory")
|
||
|
}
|
||
|
} else {
|
||
|
j, err = sdjournal.NewJournalFromFiles(c.Path)
|
||
|
if err != nil {
|
||
|
return nil, errors.Wrap(err, "failed to open journal file")
|
||
|
}
|
||
|
}
|
||
|
|
||
|
l := logger.With("path", c.Path)
|
||
|
l.Debug("New journal is opened for reading")
|
||
|
|
||
|
return newReader(l, done, c, j, state)
|
||
|
}
|
||
|
|
||
|
// NewLocal creates a reader to read form the local journal and moves the FP
|
||
|
// to the configured position.
|
||
|
func NewLocal(c Config, done chan struct{}, state checkpoint.JournalState, logger *logp.Logger) (*Reader, error) {
|
||
|
j, err := sdjournal.NewJournal()
|
||
|
if err != nil {
|
||
|
return nil, errors.Wrap(err, "failed to open local journal")
|
||
|
}
|
||
|
|
||
|
l := logger.With("path", "local")
|
||
|
l.Debug("New local journal is opened for reading")
|
||
|
|
||
|
return newReader(l, done, c, j, state)
|
||
|
}
|
||
|
|
||
|
func newReader(logger *logp.Logger, done chan struct{}, c Config, journal *sdjournal.Journal, state checkpoint.JournalState) (*Reader, error) {
|
||
|
err := setupMatches(journal, c.Matches)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
r := &Reader{
|
||
|
journal: journal,
|
||
|
config: c,
|
||
|
done: done,
|
||
|
logger: logger,
|
||
|
backoff: common.NewBackoff(done, c.Backoff, c.MaxBackoff),
|
||
|
}
|
||
|
r.seek(state.Cursor)
|
||
|
|
||
|
instance.AddJournalToMonitor(c.Path, journal)
|
||
|
|
||
|
return r, nil
|
||
|
}
|
||
|
|
||
|
func setupMatches(j *sdjournal.Journal, matches []string) error {
|
||
|
for _, m := range matches {
|
||
|
elems := strings.Split(m, "=")
|
||
|
if len(elems) != 2 {
|
||
|
return fmt.Errorf("invalid match format: %s", m)
|
||
|
}
|
||
|
|
||
|
var p string
|
||
|
for journalKey, eventField := range journaldEventFields {
|
||
|
if elems[0] == eventField.name {
|
||
|
p = journalKey + "=" + elems[1]
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// pass custom fields as is
|
||
|
if p == "" {
|
||
|
p = m
|
||
|
}
|
||
|
|
||
|
logp.Debug("journal", "Added matcher expression: %s", p)
|
||
|
|
||
|
err := j.AddMatch(p)
|
||
|
if err != nil {
|
||
|
return fmt.Errorf("error adding match to journal %v", err)
|
||
|
}
|
||
|
|
||
|
err = j.AddDisjunction()
|
||
|
if err != nil {
|
||
|
return fmt.Errorf("error adding disjunction to journal: %v", err)
|
||
|
}
|
||
|
}
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// seek seeks to the position determined by the coniguration and cursor state.
|
||
|
func (r *Reader) seek(cursor string) {
|
||
|
if r.config.Seek == "cursor" {
|
||
|
if cursor == "" {
|
||
|
r.journal.SeekHead()
|
||
|
r.logger.Debug("Seeking method set to cursor, but no state is saved for reader. Starting to read from the beginning")
|
||
|
return
|
||
|
}
|
||
|
r.journal.SeekCursor(cursor)
|
||
|
_, err := r.journal.Next()
|
||
|
if err != nil {
|
||
|
r.logger.Error("Error while seeking to cursor")
|
||
|
}
|
||
|
r.logger.Debug("Seeked to position defined in cursor")
|
||
|
} else if r.config.Seek == "tail" {
|
||
|
r.journal.SeekTail()
|
||
|
r.logger.Debug("Tailing the journal file")
|
||
|
} else if r.config.Seek == "head" {
|
||
|
r.journal.SeekHead()
|
||
|
r.logger.Debug("Reading from the beginning of the journal file")
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Next waits until a new event shows up and returns it.
|
||
|
// It blocks until an event is returned or an error occurs.
|
||
|
func (r *Reader) Next() (*beat.Event, error) {
|
||
|
for {
|
||
|
select {
|
||
|
case <-r.done:
|
||
|
return nil, nil
|
||
|
default:
|
||
|
event, err := r.readEvent()
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
if event == nil {
|
||
|
r.backoff.Wait()
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
r.backoff.Reset()
|
||
|
return event, nil
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (r *Reader) readEvent() (*beat.Event, error) {
|
||
|
n, err := r.journal.Next()
|
||
|
if err != nil && err != io.EOF {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
for n == 1 {
|
||
|
entry, err := r.journal.GetEntry()
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
event := r.toEvent(entry)
|
||
|
return event, nil
|
||
|
}
|
||
|
return nil, nil
|
||
|
}
|
||
|
|
||
|
// toEvent creates a beat.Event from journal entries.
|
||
|
func (r *Reader) toEvent(entry *sdjournal.JournalEntry) *beat.Event {
|
||
|
fields := common.MapStr{}
|
||
|
custom := common.MapStr{}
|
||
|
|
||
|
for entryKey, v := range entry.Fields {
|
||
|
if fieldConversionInfo, ok := journaldEventFields[entryKey]; !ok {
|
||
|
normalized := strings.ToLower(strings.TrimLeft(entryKey, "_"))
|
||
|
custom.Put(normalized, v)
|
||
|
} else if !fieldConversionInfo.dropped {
|
||
|
value := r.convertNamedField(fieldConversionInfo, v)
|
||
|
fields.Put(fieldConversionInfo.name, value)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if len(custom) != 0 {
|
||
|
fields["custom"] = custom
|
||
|
}
|
||
|
|
||
|
state := checkpoint.JournalState{
|
||
|
Path: r.config.Path,
|
||
|
Cursor: entry.Cursor,
|
||
|
RealtimeTimestamp: entry.RealtimeTimestamp,
|
||
|
MonotonicTimestamp: entry.MonotonicTimestamp,
|
||
|
}
|
||
|
|
||
|
fields["read_timestamp"] = time.Now()
|
||
|
receivedByJournal := time.Unix(0, int64(entry.RealtimeTimestamp)*1000)
|
||
|
|
||
|
event := beat.Event{
|
||
|
Timestamp: receivedByJournal,
|
||
|
Fields: fields,
|
||
|
Private: state,
|
||
|
}
|
||
|
return &event
|
||
|
}
|
||
|
|
||
|
func (r *Reader) convertNamedField(fc fieldConversion, value string) interface{} {
|
||
|
if fc.isInteger {
|
||
|
v, err := strconv.ParseInt(value, 10, 64)
|
||
|
if err != nil {
|
||
|
r.logger.Debugf("Failed to convert field: %s \"%v\" to int: %v", fc.name, value, err)
|
||
|
return value
|
||
|
}
|
||
|
return v
|
||
|
}
|
||
|
return value
|
||
|
}
|
||
|
|
||
|
// Close closes the underlying journal reader.
|
||
|
func (r *Reader) Close() {
|
||
|
instance.StopMonitoringJournal(r.config.Path)
|
||
|
r.journal.Close()
|
||
|
}
|