Skip to content

Commit

Permalink
Add syslog.Client.
Browse files Browse the repository at this point in the history
  • Loading branch information
ericsnowcurrently committed Jun 25, 2016
1 parent 127d094 commit 4381899
Show file tree
Hide file tree
Showing 2 changed files with 371 additions and 0 deletions.
181 changes: 181 additions & 0 deletions logfwd/syslog/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
// Copyright 2016 Canonical Ltd.
// Licensed under the AGPLv3, see LICENCE file for details.

package syslog

import (
"fmt"
"io"
"time"

"github.com/juju/errors"
"github.com/juju/loggo"

"github.com/juju/juju/logfwd"
"github.com/juju/juju/standards/rfc5424"
"github.com/juju/juju/standards/rfc5424/sdelements"
"github.com/juju/juju/standards/tls"
)

// Sender exposes the underlying functionality needed by Client.
type Sender interface {
io.Closer

// Send sends the RFC 5424 message over its connection.
Send(rfc5424.Message) error
}

// SenderOpener supports opening a syslog connection.
type SenderOpener interface {
DialFunc(cfg tls.Config, timeout time.Duration) (rfc5424.DialFunc, error)

Open(host string, cfg rfc5424.ClientConfig, dial rfc5424.DialFunc) (Sender, error)
}

type senderOpener struct{}

func (senderOpener) DialFunc(cfg tls.Config, timeout time.Duration) (rfc5424.DialFunc, error) {
dial, err := rfc5424.TLSDialFunc(cfg, timeout)
return dial, errors.Trace(err)
}

func (senderOpener) Open(host string, cfg rfc5424.ClientConfig, dial rfc5424.DialFunc) (Sender, error) {
sender, err := rfc5424.Open(host, cfg, dial)
return sender, errors.Trace(err)
}

// Client is the wrapper around a syslog (RFC 5424) connection.
type Client struct {
// Sender is the message sender this client wraps.
Sender Sender
}

// Open connects to a remote syslog host and wraps that connection
// in a new client.
func Open(cfg RawConfig) (*Client, error) {
client, err := OpenForSender(cfg, &senderOpener{})
return client, errors.Trace(err)
}

// OpenForSender connects to a remote syslog host and wraps that
// connection in a new client.
func OpenForSender(cfg RawConfig, opener SenderOpener) (*Client, error) {
if err := cfg.Validate(); err != nil {
return nil, errors.Trace(err)
}

sender, err := open(cfg, opener)
if err != nil {
return nil, errors.Trace(err)
}

client := &Client{
Sender: sender,
}
return client, nil
}

func open(cfg RawConfig, opener SenderOpener) (Sender, error) {
tlsCfg := tls.Config{
RawCert: tls.RawCert{
CertPEM: cfg.ClientCert,
KeyPEM: cfg.ClientKey,
CACertPEM: cfg.ClientCACert,
},
//ServerName: "",
ExpectedServerCertPEM: cfg.ExpectedServerCert,
}
var timeout time.Duration
dial, err := opener.DialFunc(tlsCfg, timeout)
if err != nil {
return nil, errors.Trace(err)
}

var clientCfg rfc5424.ClientConfig
client, err := opener.Open(cfg.Host, clientCfg, dial)
return client, errors.Trace(err)
}

// Close closes the client's connection.
func (client Client) Close() error {
err := client.Sender.Close()
return errors.Trace(err)
}

// Send sends the record to the remote syslog host.
func (client Client) Send(rec logfwd.Record) error {
msg, err := messageFromRecord(rec)
if err != nil {
return errors.Trace(err)
}
if err := client.Sender.Send(msg); err != nil {
return errors.Trace(err)
}
return nil
}

func messageFromRecord(rec logfwd.Record) (rfc5424.Message, error) {
msg := rfc5424.Message{
Header: rfc5424.Header{
Priority: rfc5424.Priority{
Severity: rfc5424.SeverityWarning,
Facility: rfc5424.FacilityUser,
},
Timestamp: rfc5424.Timestamp{rec.Timestamp},
Hostname: rfc5424.Hostname{
FQDN: rec.Origin.Hostname,
},
AppName: rfc5424.AppName((rec.Origin.Software.Name + "-" + rec.Origin.ModelUUID)[:48]),
},
StructuredData: rfc5424.StructuredData{
&sdelements.Origin{
EnterpriseID: sdelements.OriginEnterpriseID{
Number: sdelements.PrivateEnterpriseNumber(rec.Origin.Software.PrivateEnterpriseNumber),
},
SoftwareName: rec.Origin.Software.Name,
SoftwareVersion: rec.Origin.Software.Version,
},
&sdelements.Private{
Name: "model",
PEN: sdelements.PrivateEnterpriseNumber(rec.Origin.Software.PrivateEnterpriseNumber),
Data: []rfc5424.StructuredDataParam{{
Name: "controller-uuid",
Value: rfc5424.StructuredDataParamValue(rec.Origin.ControllerUUID),
}, {
Name: "model-uuid",
Value: rfc5424.StructuredDataParamValue(rec.Origin.ModelUUID),
}},
},
&sdelements.Private{
Name: "log",
PEN: sdelements.PrivateEnterpriseNumber(rec.Origin.Software.PrivateEnterpriseNumber),
Data: []rfc5424.StructuredDataParam{{
Name: "module",
Value: rfc5424.StructuredDataParamValue(rec.Location.Module),
}, {
Name: "source",
Value: rfc5424.StructuredDataParamValue(fmt.Sprintf("%s:%d", rec.Location.Filename, rec.Location.Line)),
}},
},
},
Msg: rec.Message,
}

switch rec.Level {
case loggo.ERROR:
msg.Priority.Severity = rfc5424.SeverityError
case loggo.WARNING:
msg.Priority.Severity = rfc5424.SeverityError
case loggo.INFO:
msg.Priority.Severity = rfc5424.SeverityError
case loggo.DEBUG, loggo.TRACE:
msg.Priority.Severity = rfc5424.SeverityError
default:
return msg, errors.Errorf("unsupported log level %q", rec.Level)
}

if err := msg.Validate(); err != nil {
return msg, errors.Trace(err)
}
return msg, nil
}
190 changes: 190 additions & 0 deletions logfwd/syslog/client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
// Copyright 2016 Canonical Ltd.
// Licensed under the AGPLv3, see LICENCE file for details.

package syslog_test

import (
"time"

"github.com/juju/loggo"
"github.com/juju/testing"
jc "github.com/juju/testing/checkers"
"github.com/juju/version"
gc "gopkg.in/check.v1"
"gopkg.in/juju/names.v2"

"github.com/juju/juju/logfwd"
"github.com/juju/juju/logfwd/syslog"
"github.com/juju/juju/standards/rfc5424"
"github.com/juju/juju/standards/rfc5424/sdelements"
"github.com/juju/juju/standards/tls"
coretesting "github.com/juju/juju/testing"
)

type ClientSuite struct {
testing.IsolationSuite

stub *testing.Stub
sender *stubSender
}

var _ = gc.Suite(&ClientSuite{})

func (s *ClientSuite) SetUpTest(c *gc.C) {
s.IsolationSuite.SetUpTest(c)

s.stub = &testing.Stub{}
s.sender = &stubSender{stub: s.stub}
}

func (s *ClientSuite) DialFunc(cfg tls.Config, timeout time.Duration) (rfc5424.DialFunc, error) {
s.stub.AddCall("DialFunc", cfg, timeout)
if err := s.stub.NextErr(); err != nil {
return nil, err
}

dial := func(network, address string) (rfc5424.Conn, error) {
s.stub.AddCall("dial", network, address)
if err := s.stub.NextErr(); err != nil {
return nil, err
}

return nil, nil
}
return dial, nil
}

func (s *ClientSuite) Open(host string, cfg rfc5424.ClientConfig, dial rfc5424.DialFunc) (syslog.Sender, error) {
s.stub.AddCall("Open", host, cfg, dial)
if err := s.stub.NextErr(); err != nil {
return nil, err
}

return s.sender, nil
}

func (s *ClientSuite) TestOpen(c *gc.C) {
cfg := syslog.RawConfig{
Host: "a.b.c:9876",
ExpectedServerCert: validCert2,
ClientCACert: coretesting.CACert,
ClientCert: coretesting.ServerCert,
ClientKey: coretesting.ServerKey,
}

client, err := syslog.OpenForSender(cfg, s)
c.Assert(err, jc.ErrorIsNil)

s.stub.CheckCallNames(c, "DialFunc", "Open")
tlsConfig := tls.Config{
RawCert: tls.RawCert{
CertPEM: coretesting.ServerCert,
KeyPEM: coretesting.ServerKey,
CACertPEM: coretesting.CACert,
},
ExpectedServerCertPEM: validCert2,
}
s.stub.CheckCall(c, 0, "DialFunc", tlsConfig, time.Duration(0))
c.Check(client.Sender, gc.Equals, s.sender)
}

func (s *ClientSuite) TestClose(c *gc.C) {
client := syslog.Client{Sender: s.sender}

err := client.Close()
c.Assert(err, jc.ErrorIsNil)

s.stub.CheckCallNames(c, "Close")
}

func (s *ClientSuite) TestSend(c *gc.C) {
tag := names.NewMachineTag("99")
cID := "9f484882-2f18-4fd2-967d-db9663db7bea"
mID := "deadbeef-2f18-4fd2-967d-db9663db7bea"
ver := version.MustParse("1.2.3")
ts := time.Unix(12345, 0)
rec := logfwd.Record{
Origin: logfwd.OriginForMachineAgent(tag, cID, mID, ver),
Timestamp: time.Unix(12345, 0),
Level: loggo.ERROR,
Location: logfwd.SourceLocation{
Module: "juju.x.y",
Filename: "x/y/spam.go",
Line: 42,
},
Message: "(╯°□°)╯︵ ┻━┻",
}
client := syslog.Client{Sender: s.sender}

err := client.Send(rec)
c.Assert(err, jc.ErrorIsNil)

s.stub.CheckCallNames(c, "Send")
s.stub.CheckCall(c, 0, "Send", rfc5424.Message{
Header: rfc5424.Header{
Priority: rfc5424.Priority{
Severity: rfc5424.SeverityError,
Facility: rfc5424.FacilityUser,
},
Timestamp: rfc5424.Timestamp{ts},
Hostname: rfc5424.Hostname{
FQDN: "machine-99.deadbeef-2f18-4fd2-967d-db9663db7bea",
},
AppName: "jujud-machine-agent-deadbeef-2f18-4fd2-967d-db96",
},
StructuredData: rfc5424.StructuredData{
&sdelements.Origin{
EnterpriseID: sdelements.OriginEnterpriseID{
Number: 28978,
},
SoftwareName: "jujud-machine-agent",
SoftwareVersion: ver,
},
&sdelements.Private{
Name: "model",
PEN: 28978,
Data: []rfc5424.StructuredDataParam{{
Name: "controller-uuid",
Value: "9f484882-2f18-4fd2-967d-db9663db7bea",
}, {
Name: "model-uuid",
Value: "deadbeef-2f18-4fd2-967d-db9663db7bea",
}},
},
&sdelements.Private{
Name: "log",
PEN: 28978,
Data: []rfc5424.StructuredDataParam{{
Name: "module",
Value: "juju.x.y",
}, {
Name: "source",
Value: "x/y/spam.go:42",
}},
},
},
Msg: "(╯°□°)╯︵ ┻━┻",
})
}

type stubSender struct {
stub *testing.Stub
}

func (s *stubSender) Send(msg rfc5424.Message) error {
s.stub.AddCall("Send", msg)
if err := s.stub.NextErr(); err != nil {
return err
}

return nil
}

func (s *stubSender) Close() error {
s.stub.AddCall("Close")
if err := s.stub.NextErr(); err != nil {
return err
}

return nil
}

0 comments on commit 4381899

Please sign in to comment.