Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ease use in (azure) cloud environment #245

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ Configuration directives can also be specified as command-line arguments (below)
-d, --dest-host string Destination syslog hostname or IP
-p, --dest-port int Destination syslog port (default 514)
-t, --dest-token string Destination ingestion token
--dest-uri string Destination as a URI, overrides dest-host, dest-port, and transport
--domain string Domain suffix for hostname
--eventmachine-tail No action, provided for backwards compatibility
-f, --facility string Facility (default "user")
-h, --help Display this help message
Expand Down Expand Up @@ -112,6 +114,21 @@ Additional information about init files (`init.d`, `supervisor`, `systemd` and `
available [in the examples directory](examples/).


## Deploy a kubernetes daemonset to forward container logs

An example for a deployment as a [kubernetes daemonset](https://kubernetes.io/docs/concepts/workloads/controllers/daemonset/)
that consumes the containerd container logs is provided [in the examples directory](examples/k8s.and.containerd/).

To install the daemonset on your cluster, using a pre-built container, edit the `papertrail-destination` secret inside
`daemonset.yml` to point to the log server and port for your [papertrail account](https://papertrailapp.com/systems/setup?type=system&platform=unix) and optionally set the `domain` in the `papertrail-remote-syslog` configmap. Then apply the daemonset to your cluster:

$ kubectl apply -f examples/k8s.and.containerd/daemonset.yml

Optionally you can build the container yourself:

$ docker build . -f examples/k8s.and.containerd/Dockerfile -t remote_syslog2:latest


## Sending messages securely ##

If the receiving system supports sending syslog over TCP with TLS, you can
Expand Down Expand Up @@ -166,6 +183,12 @@ Provide `--hostname somehostname` or use the `hostname` configuration option:

hostname: somehostname

### Add domain suffix to hostname

Provide `--domain some.f.q.d.n` or use the `domain` configuration option:

`worker-1` becomes `worker-1.some.f.q.d.n`


### Detecting new files

Expand Down Expand Up @@ -273,6 +296,13 @@ destination:

This functionality was introduced in version 0.17

#### Using regular expression matching to extract the "tag" from the file name

remote_syslog allows for a regular expression to be provided for each pattern, to
extract a meaningful name from the file names that are matched by a glob expression.

`remote_syslog 're:containers/(?P<tag>.*)-.{64}.log=/var/log/containers/*.log'`

## Troubleshooting

### Generate debug log
Expand Down
94 changes: 91 additions & 3 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import (
"crypto/x509"
"errors"
"fmt"
"net/url"
"os"
"path"
"path/filepath"
"reflect"
"regexp"
Expand Down Expand Up @@ -50,10 +52,12 @@ type Config struct {
TLS bool `mapstructure:"tls"`
Files []LogFile
Hostname string
Domain string
Severity syslog.Priority
Facility syslog.Priority
Poll bool
Destination struct {
URI string
Host string
Port int
Protocol string
Expand All @@ -63,8 +67,25 @@ type Config struct {
}

type LogFile struct {
Path string
Tag string
Path string
Tag string
TagPattern *regexp.Regexp
TagMatchIndex int
}

func (l *LogFile) TagFromFileName(fileName string) (string, bool) {
if l.TagPattern == nil {
if l.Tag == "" {
return path.Base(fileName), true
}
return l.Tag, true
}

if elems := l.TagPattern.FindStringSubmatch(fileName); len(elems) > l.TagMatchIndex {
return elems[l.TagMatchIndex], true
} else {
return "", false
}
}

func init() {
Expand Down Expand Up @@ -92,6 +113,9 @@ func initConfigAndFlags() {
flags.StringP("configfile", "c", defaultConfigFile, "Path to config")
config.BindPFlag("config_file", flags.Lookup("configfile"))

flags.String("dest-uri", "", "Destination as a URI, overrides dest-host, dest-port, and transport")
config.BindPFlag("destination.uri", flags.Lookup("dest-uri"))

flags.StringP("dest-host", "d", "", "Destination syslog hostname or IP")
config.BindPFlag("destination.host", flags.Lookup("dest-host"))

Expand All @@ -108,6 +132,9 @@ func initConfigAndFlags() {
flags.String("hostname", hostname, "Local hostname to send from")
config.BindPFlag("hostname", flags.Lookup("hostname"))

flags.StringP("domain", "", "", "Domain suffix for hostname")
config.BindPFlag("domain", flags.Lookup("domain"))

flags.String("pid-file", "", "Location of the PID file")
config.BindPFlag("pid_file", flags.Lookup("pid-file"))

Expand Down Expand Up @@ -191,6 +218,7 @@ func NewConfigFromEnv() (*Config, error) {
}

// explicitly set destination fields since they are nested
c.Destination.URI = config.GetString("destination.uri")
c.Destination.Host = config.GetString("destination.host")
c.Destination.Port = config.GetInt("destination.port")
c.Destination.Protocol = config.GetString("destination.protocol")
Expand Down Expand Up @@ -223,6 +251,37 @@ func NewConfigFromEnv() (*Config, error) {
}

func (c *Config) Validate() error {
if c.Destination.URI != "" {
u, err := url.Parse(c.Destination.URI)
if err != nil {
return err
}

c.Destination.Host = u.Hostname()
c.Destination.Port, err = strconv.Atoi(u.Port())

if err != nil {
return err
}

sp := strings.SplitN(strings.ToLower(u.Scheme), "+", 2)

if sp[0] != "syslog" {
return fmt.Errorf("destination URI contains invalid protocol")
}

if len(sp) == 1 {
c.Destination.Protocol = "udp"
} else {
c.Destination.Protocol = sp[1]
}
}

if c.Domain != "" {
c.Hostname = strings.TrimRight(c.Hostname, ".") + "." + strings.TrimLeft(c.Domain, ".")
c.Domain = ""
}

if c.Destination.Host == "" {
return fmt.Errorf("No destination hostname specified")
}
Expand Down Expand Up @@ -284,6 +343,26 @@ func decodeRegexps(f interface{}) ([]*regexp.Regexp, error) {
return exps, nil
}

func decodeTagRegex(tag string) (*regexp.Regexp, int, error) {
r, err := regexp.Compile(tag[3:])
if err != nil {
return nil, 0, err
}
for i, n := range r.SubexpNames() {
if n == "tag" {
return r, i, nil
}
}
switch r.NumSubexp() {
case 0:
return r, 0, nil
case 1:
return r, 1, nil
default:
return nil, 0, errors.New("invalid tag expression: more than one sub-expression and none is named 'tag'")
}
}

func decodeLogFiles(f interface{}) ([]LogFile, error) {
var (
files []LogFile
Expand All @@ -300,7 +379,16 @@ func decodeLogFiles(f interface{}) ([]LogFile, error) {
lf := strings.Split(val, "=")
switch len(lf) {
case 2:
files = append(files, LogFile{Tag: lf[0], Path: lf[1]})
tag := lf[0]
if strings.HasPrefix(tag, "re:") {
r, idx, err := decodeTagRegex(tag)
if err != nil {
return files, err
}
files = append(files, LogFile{Tag: "", Path: lf[1], TagPattern: r, TagMatchIndex: idx})
} else {
files = append(files, LogFile{Tag: lf[0], Path: lf[1]})
}
case 1:
files = append(files, LogFile{Path: val})
default:
Expand Down
88 changes: 88 additions & 0 deletions config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,91 @@ func TestNoConfigFile(t *testing.T) {
assert.Equal("udp", c.Destination.Protocol)
assert.Equal("", c.Destination.Token)
}

func TestURIInConfig(t *testing.T) {
assert := assert.New(t)
initConfigAndFlags()

flags.Set("dest-uri", "syslog+tls://localhost:999")

c, err := NewConfigFromEnv()
if err != nil {
t.Fatal(err)
}

assert.NoError(c.Validate())
assert.Equal("localhost", c.Destination.Host)
assert.Equal(999, c.Destination.Port)
assert.Equal("tls", c.Destination.Protocol)
assert.Equal("", c.Destination.Token)
}

func TestLogFileTagPatterns(t *testing.T) {
type tc struct {
pattern string
idx int
file string
tag string
err error
ok bool
}
tcs := []tc{
{
`re:containers/(.*).log=/var/log/containers/*.log`,
1,
"/var/log/containers/azure-ip-masq-agent-aaaaa_kube-system_azure-ip-masq-agent-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.log",
"azure-ip-masq-agent-aaaaa_kube-system_azure-ip-masq-agent-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
nil,
true,
},
{
`re:(containers/)(?P<tag>.*)(.log)=/var/log/containers/*.log`,
2,
"/var/log/containers/azure-ip-masq-agent-aaaaa_kube-system_azure-ip-masq-agent-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.log",
"azure-ip-masq-agent-aaaaa_kube-system_azure-ip-masq-agent-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
nil,
true,
},
{
`re:containers/(?P<tag>.*)-.{64}\.log=/var/log/containers/*.log`,
1,
"/var/log/containers/azure-ip-masq-agent-aaaaa_kube-system_azure-ip-masq-agent-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.log",
"azure-ip-masq-agent-aaaaa_kube-system_azure-ip-masq-agent",
nil,
true,
},
{
`re:pods/[^/]+/([^/]+)/=/var/log/pods/*/*/*.log`,
1,
"/var/log/pods/kube-system_azure-ip-masq-agent-aaaaa_00000000-0000-0000-0000-000000000000/azure-ip-masq-agent/0.log",
"azure-ip-masq-agent",
nil,
true,
},
}

for _, tc := range tcs {
t.Run(tc.pattern, func(t *testing.T) {
t.Helper()

lfs, err := decodeLogFiles([]interface{}{tc.pattern})
if tc.err == nil {
assert.NoError(t, err)
} else {
assert.EqualError(t, err, tc.err.Error())
return
}
assert.Len(t, lfs, 1)
lf := lfs[0]
assert.NotNil(t, lf.TagPattern)
assert.Equal(t, lf.TagMatchIndex, tc.idx)
tag, ok := lfs[0].TagFromFileName(tc.file)
if tc.ok {
assert.True(t, ok)
assert.Equal(t, tag, tc.tag)
} else {
assert.False(t, ok)
}
})
}
}
16 changes: 16 additions & 0 deletions examples/k8s.and.containerd/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
FROM golang:alpine AS builder

WORKDIR $GOPATH/src/github.com/papertrail/remote_syslog2

COPY . .

RUN go get -v

RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -v -o /remote_syslog2

FROM scratch

COPY --from=builder /remote_syslog2 /remote_syslog2
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/

ENTRYPOINT [ "/remote_syslog2", "-D", "--tls" ]
Loading