Initial commit
This commit is contained in:
376
vendor/github.com/hyperhq/hypercli/daemon/logger/awslogs/cloudwatchlogs.go
generated
vendored
Normal file
376
vendor/github.com/hyperhq/hypercli/daemon/logger/awslogs/cloudwatchlogs.go
generated
vendored
Normal file
@@ -0,0 +1,376 @@
|
||||
// Package awslogs provides the logdriver for forwarding container logs to Amazon CloudWatch Logs
|
||||
package awslogs
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/awserr"
|
||||
"github.com/aws/aws-sdk-go/aws/defaults"
|
||||
"github.com/aws/aws-sdk-go/aws/ec2metadata"
|
||||
"github.com/aws/aws-sdk-go/aws/request"
|
||||
"github.com/aws/aws-sdk-go/service/cloudwatchlogs"
|
||||
"github.com/hyperhq/hypercli/daemon/logger"
|
||||
"github.com/hyperhq/hypercli/dockerversion"
|
||||
)
|
||||
|
||||
const (
|
||||
name = "awslogs"
|
||||
regionKey = "awslogs-region"
|
||||
regionEnvKey = "AWS_REGION"
|
||||
logGroupKey = "awslogs-group"
|
||||
logStreamKey = "awslogs-stream"
|
||||
batchPublishFrequency = 5 * time.Second
|
||||
|
||||
// See: http://docs.aws.amazon.com/AmazonCloudWatchLogs/latest/APIReference/API_PutLogEvents.html
|
||||
perEventBytes = 26
|
||||
maximumBytesPerPut = 1048576
|
||||
maximumLogEventsPerPut = 10000
|
||||
|
||||
// See: http://docs.aws.amazon.com/AmazonCloudWatch/latest/DeveloperGuide/cloudwatch_limits.html
|
||||
maximumBytesPerEvent = 262144 - perEventBytes
|
||||
|
||||
resourceAlreadyExistsCode = "ResourceAlreadyExistsException"
|
||||
dataAlreadyAcceptedCode = "DataAlreadyAcceptedException"
|
||||
invalidSequenceTokenCode = "InvalidSequenceTokenException"
|
||||
|
||||
userAgentHeader = "User-Agent"
|
||||
)
|
||||
|
||||
type logStream struct {
|
||||
logStreamName string
|
||||
logGroupName string
|
||||
client api
|
||||
messages chan *logger.Message
|
||||
lock sync.RWMutex
|
||||
closed bool
|
||||
sequenceToken *string
|
||||
}
|
||||
|
||||
type api interface {
|
||||
CreateLogStream(*cloudwatchlogs.CreateLogStreamInput) (*cloudwatchlogs.CreateLogStreamOutput, error)
|
||||
PutLogEvents(*cloudwatchlogs.PutLogEventsInput) (*cloudwatchlogs.PutLogEventsOutput, error)
|
||||
}
|
||||
|
||||
type regionFinder interface {
|
||||
Region() (string, error)
|
||||
}
|
||||
|
||||
type byTimestamp []*cloudwatchlogs.InputLogEvent
|
||||
|
||||
// init registers the awslogs driver and sets the default region, if provided
|
||||
func init() {
|
||||
if os.Getenv(regionEnvKey) != "" {
|
||||
defaults.DefaultConfig.Region = aws.String(os.Getenv(regionEnvKey))
|
||||
}
|
||||
if err := logger.RegisterLogDriver(name, New); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
if err := logger.RegisterLogOptValidator(name, ValidateLogOpt); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// New creates an awslogs logger using the configuration passed in on the
|
||||
// context. Supported context configuration variables are awslogs-region,
|
||||
// awslogs-group, and awslogs-stream. When available, configuration is
|
||||
// also taken from environment variables AWS_REGION, AWS_ACCESS_KEY_ID,
|
||||
// AWS_SECRET_ACCESS_KEY, the shared credentials file (~/.aws/credentials), and
|
||||
// the EC2 Instance Metadata Service.
|
||||
func New(ctx logger.Context) (logger.Logger, error) {
|
||||
logGroupName := ctx.Config[logGroupKey]
|
||||
logStreamName := ctx.ContainerID
|
||||
if ctx.Config[logStreamKey] != "" {
|
||||
logStreamName = ctx.Config[logStreamKey]
|
||||
}
|
||||
client, err := newAWSLogsClient(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
containerStream := &logStream{
|
||||
logStreamName: logStreamName,
|
||||
logGroupName: logGroupName,
|
||||
client: client,
|
||||
messages: make(chan *logger.Message, 4096),
|
||||
}
|
||||
err = containerStream.create()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
go containerStream.collectBatch()
|
||||
|
||||
return containerStream, nil
|
||||
}
|
||||
|
||||
// newRegionFinder is a variable such that the implementation
|
||||
// can be swapped out for unit tests.
|
||||
var newRegionFinder = func() regionFinder {
|
||||
return ec2metadata.New(nil)
|
||||
}
|
||||
|
||||
// newAWSLogsClient creates the service client for Amazon CloudWatch Logs.
|
||||
// Customizations to the default client from the SDK include a Docker-specific
|
||||
// User-Agent string and automatic region detection using the EC2 Instance
|
||||
// Metadata Service when region is otherwise unspecified.
|
||||
func newAWSLogsClient(ctx logger.Context) (api, error) {
|
||||
config := defaults.DefaultConfig
|
||||
if ctx.Config[regionKey] != "" {
|
||||
config = defaults.DefaultConfig.Merge(&aws.Config{
|
||||
Region: aws.String(ctx.Config[regionKey]),
|
||||
})
|
||||
}
|
||||
if config.Region == nil || *config.Region == "" {
|
||||
logrus.Info("Trying to get region from EC2 Metadata")
|
||||
ec2MetadataClient := newRegionFinder()
|
||||
region, err := ec2MetadataClient.Region()
|
||||
if err != nil {
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"error": err,
|
||||
}).Error("Could not get region from EC2 metadata, environment, or log option")
|
||||
return nil, errors.New("Cannot determine region for awslogs driver")
|
||||
}
|
||||
config.Region = ®ion
|
||||
}
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"region": *config.Region,
|
||||
}).Debug("Created awslogs client")
|
||||
client := cloudwatchlogs.New(config)
|
||||
|
||||
client.Handlers.Build.PushBackNamed(request.NamedHandler{
|
||||
Name: "DockerUserAgentHandler",
|
||||
Fn: func(r *request.Request) {
|
||||
currentAgent := r.HTTPRequest.Header.Get(userAgentHeader)
|
||||
r.HTTPRequest.Header.Set(userAgentHeader,
|
||||
fmt.Sprintf("Docker %s (%s) %s",
|
||||
dockerversion.Version, runtime.GOOS, currentAgent))
|
||||
},
|
||||
})
|
||||
return client, nil
|
||||
}
|
||||
|
||||
// Name returns the name of the awslogs logging driver
|
||||
func (l *logStream) Name() string {
|
||||
return name
|
||||
}
|
||||
|
||||
// Log submits messages for logging by an instance of the awslogs logging driver
|
||||
func (l *logStream) Log(msg *logger.Message) error {
|
||||
l.lock.RLock()
|
||||
defer l.lock.RUnlock()
|
||||
if !l.closed {
|
||||
l.messages <- msg
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close closes the instance of the awslogs logging driver
|
||||
func (l *logStream) Close() error {
|
||||
l.lock.Lock()
|
||||
defer l.lock.Unlock()
|
||||
if !l.closed {
|
||||
close(l.messages)
|
||||
}
|
||||
l.closed = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// create creates a log stream for the instance of the awslogs logging driver
|
||||
func (l *logStream) create() error {
|
||||
input := &cloudwatchlogs.CreateLogStreamInput{
|
||||
LogGroupName: aws.String(l.logGroupName),
|
||||
LogStreamName: aws.String(l.logStreamName),
|
||||
}
|
||||
|
||||
_, err := l.client.CreateLogStream(input)
|
||||
|
||||
if err != nil {
|
||||
if awsErr, ok := err.(awserr.Error); ok {
|
||||
fields := logrus.Fields{
|
||||
"errorCode": awsErr.Code(),
|
||||
"message": awsErr.Message(),
|
||||
"origError": awsErr.OrigErr(),
|
||||
"logGroupName": l.logGroupName,
|
||||
"logStreamName": l.logStreamName,
|
||||
}
|
||||
if awsErr.Code() == resourceAlreadyExistsCode {
|
||||
// Allow creation to succeed
|
||||
logrus.WithFields(fields).Info("Log stream already exists")
|
||||
return nil
|
||||
}
|
||||
logrus.WithFields(fields).Error("Failed to create log stream")
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// newTicker is used for time-based batching. newTicker is a variable such
|
||||
// that the implementation can be swapped out for unit tests.
|
||||
var newTicker = func(freq time.Duration) *time.Ticker {
|
||||
return time.NewTicker(freq)
|
||||
}
|
||||
|
||||
// collectBatch executes as a goroutine to perform batching of log events for
|
||||
// submission to the log stream. Batching is performed on time- and size-
|
||||
// bases. Time-based batching occurs at a 5 second interval (defined in the
|
||||
// batchPublishFrequency const). Size-based batching is performed on the
|
||||
// maximum number of events per batch (defined in maximumLogEventsPerPut) and
|
||||
// the maximum number of total bytes in a batch (defined in
|
||||
// maximumBytesPerPut). Log messages are split by the maximum bytes per event
|
||||
// (defined in maximumBytesPerEvent). There is a fixed per-event byte overhead
|
||||
// (defined in perEventBytes) which is accounted for in split- and batch-
|
||||
// calculations.
|
||||
func (l *logStream) collectBatch() {
|
||||
timer := newTicker(batchPublishFrequency)
|
||||
var events []*cloudwatchlogs.InputLogEvent
|
||||
bytes := 0
|
||||
for {
|
||||
select {
|
||||
case <-timer.C:
|
||||
l.publishBatch(events)
|
||||
events = events[:0]
|
||||
bytes = 0
|
||||
case msg, more := <-l.messages:
|
||||
if !more {
|
||||
l.publishBatch(events)
|
||||
return
|
||||
}
|
||||
unprocessedLine := msg.Line
|
||||
for len(unprocessedLine) > 0 {
|
||||
// Split line length so it does not exceed the maximum
|
||||
lineBytes := len(unprocessedLine)
|
||||
if lineBytes > maximumBytesPerEvent {
|
||||
lineBytes = maximumBytesPerEvent
|
||||
}
|
||||
line := unprocessedLine[:lineBytes]
|
||||
unprocessedLine = unprocessedLine[lineBytes:]
|
||||
if (len(events) >= maximumLogEventsPerPut) || (bytes+lineBytes+perEventBytes > maximumBytesPerPut) {
|
||||
// Publish an existing batch if it's already over the maximum number of events or if adding this
|
||||
// event would push it over the maximum number of total bytes.
|
||||
l.publishBatch(events)
|
||||
events = events[:0]
|
||||
bytes = 0
|
||||
}
|
||||
events = append(events, &cloudwatchlogs.InputLogEvent{
|
||||
Message: aws.String(string(line)),
|
||||
Timestamp: aws.Int64(msg.Timestamp.UnixNano() / int64(time.Millisecond)),
|
||||
})
|
||||
bytes += (lineBytes + perEventBytes)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// publishBatch calls PutLogEvents for a given set of InputLogEvents,
|
||||
// accounting for sequencing requirements (each request must reference the
|
||||
// sequence token returned by the previous request).
|
||||
func (l *logStream) publishBatch(events []*cloudwatchlogs.InputLogEvent) {
|
||||
if len(events) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
sort.Sort(byTimestamp(events))
|
||||
|
||||
nextSequenceToken, err := l.putLogEvents(events, l.sequenceToken)
|
||||
|
||||
if err != nil {
|
||||
if awsErr, ok := err.(awserr.Error); ok {
|
||||
if awsErr.Code() == dataAlreadyAcceptedCode {
|
||||
// already submitted, just grab the correct sequence token
|
||||
parts := strings.Split(awsErr.Message(), " ")
|
||||
nextSequenceToken = &parts[len(parts)-1]
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"errorCode": awsErr.Code(),
|
||||
"message": awsErr.Message(),
|
||||
"logGroupName": l.logGroupName,
|
||||
"logStreamName": l.logStreamName,
|
||||
}).Info("Data already accepted, ignoring error")
|
||||
err = nil
|
||||
} else if awsErr.Code() == invalidSequenceTokenCode {
|
||||
// sequence code is bad, grab the correct one and retry
|
||||
parts := strings.Split(awsErr.Message(), " ")
|
||||
token := parts[len(parts)-1]
|
||||
nextSequenceToken, err = l.putLogEvents(events, &token)
|
||||
}
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
logrus.Error(err)
|
||||
} else {
|
||||
l.sequenceToken = nextSequenceToken
|
||||
}
|
||||
}
|
||||
|
||||
// putLogEvents wraps the PutLogEvents API
|
||||
func (l *logStream) putLogEvents(events []*cloudwatchlogs.InputLogEvent, sequenceToken *string) (*string, error) {
|
||||
input := &cloudwatchlogs.PutLogEventsInput{
|
||||
LogEvents: events,
|
||||
SequenceToken: sequenceToken,
|
||||
LogGroupName: aws.String(l.logGroupName),
|
||||
LogStreamName: aws.String(l.logStreamName),
|
||||
}
|
||||
resp, err := l.client.PutLogEvents(input)
|
||||
if err != nil {
|
||||
if awsErr, ok := err.(awserr.Error); ok {
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"errorCode": awsErr.Code(),
|
||||
"message": awsErr.Message(),
|
||||
"origError": awsErr.OrigErr(),
|
||||
"logGroupName": l.logGroupName,
|
||||
"logStreamName": l.logStreamName,
|
||||
}).Error("Failed to put log events")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return resp.NextSequenceToken, nil
|
||||
}
|
||||
|
||||
// ValidateLogOpt looks for awslogs-specific log options awslogs-region,
|
||||
// awslogs-group, and awslogs-stream
|
||||
func ValidateLogOpt(cfg map[string]string) error {
|
||||
for key := range cfg {
|
||||
switch key {
|
||||
case logGroupKey:
|
||||
case logStreamKey:
|
||||
case regionKey:
|
||||
default:
|
||||
return fmt.Errorf("unknown log opt '%s' for %s log driver", key, name)
|
||||
}
|
||||
}
|
||||
if cfg[logGroupKey] == "" {
|
||||
return fmt.Errorf("must specify a value for log opt '%s'", logGroupKey)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Len returns the length of a byTimestamp slice. Len is required by the
|
||||
// sort.Interface interface.
|
||||
func (slice byTimestamp) Len() int {
|
||||
return len(slice)
|
||||
}
|
||||
|
||||
// Less compares two values in a byTimestamp slice by Timestamp. Less is
|
||||
// required by the sort.Interface interface.
|
||||
func (slice byTimestamp) Less(i, j int) bool {
|
||||
iTimestamp, jTimestamp := int64(0), int64(0)
|
||||
if slice != nil && slice[i].Timestamp != nil {
|
||||
iTimestamp = *slice[i].Timestamp
|
||||
}
|
||||
if slice != nil && slice[j].Timestamp != nil {
|
||||
jTimestamp = *slice[j].Timestamp
|
||||
}
|
||||
return iTimestamp < jTimestamp
|
||||
}
|
||||
|
||||
// Swap swaps two values in a byTimestamp slice with each other. Swap is
|
||||
// required by the sort.Interface interface.
|
||||
func (slice byTimestamp) Swap(i, j int) {
|
||||
slice[i], slice[j] = slice[j], slice[i]
|
||||
}
|
||||
627
vendor/github.com/hyperhq/hypercli/daemon/logger/awslogs/cloudwatchlogs_test.go
generated
vendored
Normal file
627
vendor/github.com/hyperhq/hypercli/daemon/logger/awslogs/cloudwatchlogs_test.go
generated
vendored
Normal file
@@ -0,0 +1,627 @@
|
||||
package awslogs
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/awserr"
|
||||
"github.com/aws/aws-sdk-go/aws/request"
|
||||
"github.com/aws/aws-sdk-go/service/cloudwatchlogs"
|
||||
"github.com/hyperhq/hypercli/daemon/logger"
|
||||
"github.com/hyperhq/hypercli/dockerversion"
|
||||
)
|
||||
|
||||
const (
|
||||
groupName = "groupName"
|
||||
streamName = "streamName"
|
||||
sequenceToken = "sequenceToken"
|
||||
nextSequenceToken = "nextSequenceToken"
|
||||
logline = "this is a log line"
|
||||
)
|
||||
|
||||
func TestNewAWSLogsClientUserAgentHandler(t *testing.T) {
|
||||
ctx := logger.Context{
|
||||
Config: map[string]string{
|
||||
regionKey: "us-east-1",
|
||||
},
|
||||
}
|
||||
|
||||
client, err := newAWSLogsClient(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
realClient, ok := client.(*cloudwatchlogs.CloudWatchLogs)
|
||||
if !ok {
|
||||
t.Fatal("Could not cast client to cloudwatchlogs.CloudWatchLogs")
|
||||
}
|
||||
buildHandlerList := realClient.Handlers.Build
|
||||
request := &request.Request{
|
||||
HTTPRequest: &http.Request{
|
||||
Header: http.Header{},
|
||||
},
|
||||
}
|
||||
buildHandlerList.Run(request)
|
||||
expectedUserAgentString := fmt.Sprintf("Docker %s (%s) %s/%s",
|
||||
dockerversion.Version, runtime.GOOS, aws.SDKName, aws.SDKVersion)
|
||||
userAgent := request.HTTPRequest.Header.Get("User-Agent")
|
||||
if userAgent != expectedUserAgentString {
|
||||
t.Errorf("Wrong User-Agent string, expected \"%s\" but was \"%s\"",
|
||||
expectedUserAgentString, userAgent)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewAWSLogsClientRegionDetect(t *testing.T) {
|
||||
ctx := logger.Context{
|
||||
Config: map[string]string{},
|
||||
}
|
||||
|
||||
mockMetadata := newMockMetadataClient()
|
||||
newRegionFinder = func() regionFinder {
|
||||
return mockMetadata
|
||||
}
|
||||
mockMetadata.regionResult <- ®ionResult{
|
||||
successResult: "us-east-1",
|
||||
}
|
||||
|
||||
_, err := newAWSLogsClient(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateSuccess(t *testing.T) {
|
||||
mockClient := newMockClient()
|
||||
stream := &logStream{
|
||||
client: mockClient,
|
||||
logGroupName: groupName,
|
||||
logStreamName: streamName,
|
||||
}
|
||||
mockClient.createLogStreamResult <- &createLogStreamResult{}
|
||||
|
||||
err := stream.create()
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Received unexpected err: %v\n", err)
|
||||
}
|
||||
argument := <-mockClient.createLogStreamArgument
|
||||
if argument.LogGroupName == nil {
|
||||
t.Fatal("Expected non-nil LogGroupName")
|
||||
}
|
||||
if *argument.LogGroupName != groupName {
|
||||
t.Errorf("Expected LogGroupName to be %s", groupName)
|
||||
}
|
||||
if argument.LogStreamName == nil {
|
||||
t.Fatal("Expected non-nil LogGroupName")
|
||||
}
|
||||
if *argument.LogStreamName != streamName {
|
||||
t.Errorf("Expected LogStreamName to be %s", streamName)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateError(t *testing.T) {
|
||||
mockClient := newMockClient()
|
||||
stream := &logStream{
|
||||
client: mockClient,
|
||||
}
|
||||
mockClient.createLogStreamResult <- &createLogStreamResult{
|
||||
errorResult: errors.New("Error!"),
|
||||
}
|
||||
|
||||
err := stream.create()
|
||||
|
||||
if err == nil {
|
||||
t.Fatal("Expected non-nil err")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateAlreadyExists(t *testing.T) {
|
||||
mockClient := newMockClient()
|
||||
stream := &logStream{
|
||||
client: mockClient,
|
||||
}
|
||||
mockClient.createLogStreamResult <- &createLogStreamResult{
|
||||
errorResult: awserr.New(resourceAlreadyExistsCode, "", nil),
|
||||
}
|
||||
|
||||
err := stream.create()
|
||||
|
||||
if err != nil {
|
||||
t.Fatal("Expected nil err")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublishBatchSuccess(t *testing.T) {
|
||||
mockClient := newMockClient()
|
||||
stream := &logStream{
|
||||
client: mockClient,
|
||||
logGroupName: groupName,
|
||||
logStreamName: streamName,
|
||||
sequenceToken: aws.String(sequenceToken),
|
||||
}
|
||||
mockClient.putLogEventsResult <- &putLogEventsResult{
|
||||
successResult: &cloudwatchlogs.PutLogEventsOutput{
|
||||
NextSequenceToken: aws.String(nextSequenceToken),
|
||||
},
|
||||
}
|
||||
|
||||
events := []*cloudwatchlogs.InputLogEvent{
|
||||
{
|
||||
Message: aws.String(logline),
|
||||
},
|
||||
}
|
||||
|
||||
stream.publishBatch(events)
|
||||
if stream.sequenceToken == nil {
|
||||
t.Fatal("Expected non-nil sequenceToken")
|
||||
}
|
||||
if *stream.sequenceToken != nextSequenceToken {
|
||||
t.Errorf("Expected sequenceToken to be %s, but was %s", nextSequenceToken, *stream.sequenceToken)
|
||||
}
|
||||
argument := <-mockClient.putLogEventsArgument
|
||||
if argument == nil {
|
||||
t.Fatal("Expected non-nil PutLogEventsInput")
|
||||
}
|
||||
if argument.SequenceToken == nil {
|
||||
t.Fatal("Expected non-nil PutLogEventsInput.SequenceToken")
|
||||
}
|
||||
if *argument.SequenceToken != sequenceToken {
|
||||
t.Errorf("Expected PutLogEventsInput.SequenceToken to be %s, but was %s", sequenceToken, *argument.SequenceToken)
|
||||
}
|
||||
if len(argument.LogEvents) != 1 {
|
||||
t.Errorf("Expected LogEvents to contain 1 element, but contains %d", len(argument.LogEvents))
|
||||
}
|
||||
if argument.LogEvents[0] != events[0] {
|
||||
t.Error("Expected event to equal input")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublishBatchError(t *testing.T) {
|
||||
mockClient := newMockClient()
|
||||
stream := &logStream{
|
||||
client: mockClient,
|
||||
logGroupName: groupName,
|
||||
logStreamName: streamName,
|
||||
sequenceToken: aws.String(sequenceToken),
|
||||
}
|
||||
mockClient.putLogEventsResult <- &putLogEventsResult{
|
||||
errorResult: errors.New("Error!"),
|
||||
}
|
||||
|
||||
events := []*cloudwatchlogs.InputLogEvent{
|
||||
{
|
||||
Message: aws.String(logline),
|
||||
},
|
||||
}
|
||||
|
||||
stream.publishBatch(events)
|
||||
if stream.sequenceToken == nil {
|
||||
t.Fatal("Expected non-nil sequenceToken")
|
||||
}
|
||||
if *stream.sequenceToken != sequenceToken {
|
||||
t.Errorf("Expected sequenceToken to be %s, but was %s", sequenceToken, *stream.sequenceToken)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublishBatchInvalidSeqSuccess(t *testing.T) {
|
||||
mockClient := newMockClientBuffered(2)
|
||||
stream := &logStream{
|
||||
client: mockClient,
|
||||
logGroupName: groupName,
|
||||
logStreamName: streamName,
|
||||
sequenceToken: aws.String(sequenceToken),
|
||||
}
|
||||
mockClient.putLogEventsResult <- &putLogEventsResult{
|
||||
errorResult: awserr.New(invalidSequenceTokenCode, "use token token", nil),
|
||||
}
|
||||
mockClient.putLogEventsResult <- &putLogEventsResult{
|
||||
successResult: &cloudwatchlogs.PutLogEventsOutput{
|
||||
NextSequenceToken: aws.String(nextSequenceToken),
|
||||
},
|
||||
}
|
||||
|
||||
events := []*cloudwatchlogs.InputLogEvent{
|
||||
{
|
||||
Message: aws.String(logline),
|
||||
},
|
||||
}
|
||||
|
||||
stream.publishBatch(events)
|
||||
if stream.sequenceToken == nil {
|
||||
t.Fatal("Expected non-nil sequenceToken")
|
||||
}
|
||||
if *stream.sequenceToken != nextSequenceToken {
|
||||
t.Errorf("Expected sequenceToken to be %s, but was %s", nextSequenceToken, *stream.sequenceToken)
|
||||
}
|
||||
|
||||
argument := <-mockClient.putLogEventsArgument
|
||||
if argument == nil {
|
||||
t.Fatal("Expected non-nil PutLogEventsInput")
|
||||
}
|
||||
if argument.SequenceToken == nil {
|
||||
t.Fatal("Expected non-nil PutLogEventsInput.SequenceToken")
|
||||
}
|
||||
if *argument.SequenceToken != sequenceToken {
|
||||
t.Errorf("Expected PutLogEventsInput.SequenceToken to be %s, but was %s", sequenceToken, *argument.SequenceToken)
|
||||
}
|
||||
if len(argument.LogEvents) != 1 {
|
||||
t.Errorf("Expected LogEvents to contain 1 element, but contains %d", len(argument.LogEvents))
|
||||
}
|
||||
if argument.LogEvents[0] != events[0] {
|
||||
t.Error("Expected event to equal input")
|
||||
}
|
||||
|
||||
argument = <-mockClient.putLogEventsArgument
|
||||
if argument == nil {
|
||||
t.Fatal("Expected non-nil PutLogEventsInput")
|
||||
}
|
||||
if argument.SequenceToken == nil {
|
||||
t.Fatal("Expected non-nil PutLogEventsInput.SequenceToken")
|
||||
}
|
||||
if *argument.SequenceToken != "token" {
|
||||
t.Errorf("Expected PutLogEventsInput.SequenceToken to be %s, but was %s", "token", *argument.SequenceToken)
|
||||
}
|
||||
if len(argument.LogEvents) != 1 {
|
||||
t.Errorf("Expected LogEvents to contain 1 element, but contains %d", len(argument.LogEvents))
|
||||
}
|
||||
if argument.LogEvents[0] != events[0] {
|
||||
t.Error("Expected event to equal input")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublishBatchAlreadyAccepted(t *testing.T) {
|
||||
mockClient := newMockClient()
|
||||
stream := &logStream{
|
||||
client: mockClient,
|
||||
logGroupName: groupName,
|
||||
logStreamName: streamName,
|
||||
sequenceToken: aws.String(sequenceToken),
|
||||
}
|
||||
mockClient.putLogEventsResult <- &putLogEventsResult{
|
||||
errorResult: awserr.New(dataAlreadyAcceptedCode, "use token token", nil),
|
||||
}
|
||||
|
||||
events := []*cloudwatchlogs.InputLogEvent{
|
||||
{
|
||||
Message: aws.String(logline),
|
||||
},
|
||||
}
|
||||
|
||||
stream.publishBatch(events)
|
||||
if stream.sequenceToken == nil {
|
||||
t.Fatal("Expected non-nil sequenceToken")
|
||||
}
|
||||
if *stream.sequenceToken != "token" {
|
||||
t.Errorf("Expected sequenceToken to be %s, but was %s", "token", *stream.sequenceToken)
|
||||
}
|
||||
|
||||
argument := <-mockClient.putLogEventsArgument
|
||||
if argument == nil {
|
||||
t.Fatal("Expected non-nil PutLogEventsInput")
|
||||
}
|
||||
if argument.SequenceToken == nil {
|
||||
t.Fatal("Expected non-nil PutLogEventsInput.SequenceToken")
|
||||
}
|
||||
if *argument.SequenceToken != sequenceToken {
|
||||
t.Errorf("Expected PutLogEventsInput.SequenceToken to be %s, but was %s", sequenceToken, *argument.SequenceToken)
|
||||
}
|
||||
if len(argument.LogEvents) != 1 {
|
||||
t.Errorf("Expected LogEvents to contain 1 element, but contains %d", len(argument.LogEvents))
|
||||
}
|
||||
if argument.LogEvents[0] != events[0] {
|
||||
t.Error("Expected event to equal input")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectBatchSimple(t *testing.T) {
|
||||
mockClient := newMockClient()
|
||||
stream := &logStream{
|
||||
client: mockClient,
|
||||
logGroupName: groupName,
|
||||
logStreamName: streamName,
|
||||
sequenceToken: aws.String(sequenceToken),
|
||||
messages: make(chan *logger.Message),
|
||||
}
|
||||
mockClient.putLogEventsResult <- &putLogEventsResult{
|
||||
successResult: &cloudwatchlogs.PutLogEventsOutput{
|
||||
NextSequenceToken: aws.String(nextSequenceToken),
|
||||
},
|
||||
}
|
||||
ticks := make(chan time.Time)
|
||||
newTicker = func(_ time.Duration) *time.Ticker {
|
||||
return &time.Ticker{
|
||||
C: ticks,
|
||||
}
|
||||
}
|
||||
|
||||
go stream.collectBatch()
|
||||
|
||||
stream.Log(&logger.Message{
|
||||
Line: []byte(logline),
|
||||
Timestamp: time.Time{},
|
||||
})
|
||||
|
||||
ticks <- time.Time{}
|
||||
stream.Close()
|
||||
|
||||
argument := <-mockClient.putLogEventsArgument
|
||||
if argument == nil {
|
||||
t.Fatal("Expected non-nil PutLogEventsInput")
|
||||
}
|
||||
if len(argument.LogEvents) != 1 {
|
||||
t.Errorf("Expected LogEvents to contain 1 element, but contains %d", len(argument.LogEvents))
|
||||
}
|
||||
if *argument.LogEvents[0].Message != logline {
|
||||
t.Errorf("Expected message to be %s but was %s", logline, *argument.LogEvents[0].Message)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectBatchTicker(t *testing.T) {
|
||||
mockClient := newMockClient()
|
||||
stream := &logStream{
|
||||
client: mockClient,
|
||||
logGroupName: groupName,
|
||||
logStreamName: streamName,
|
||||
sequenceToken: aws.String(sequenceToken),
|
||||
messages: make(chan *logger.Message),
|
||||
}
|
||||
mockClient.putLogEventsResult <- &putLogEventsResult{
|
||||
successResult: &cloudwatchlogs.PutLogEventsOutput{
|
||||
NextSequenceToken: aws.String(nextSequenceToken),
|
||||
},
|
||||
}
|
||||
ticks := make(chan time.Time)
|
||||
newTicker = func(_ time.Duration) *time.Ticker {
|
||||
return &time.Ticker{
|
||||
C: ticks,
|
||||
}
|
||||
}
|
||||
|
||||
go stream.collectBatch()
|
||||
|
||||
stream.Log(&logger.Message{
|
||||
Line: []byte(logline + " 1"),
|
||||
Timestamp: time.Time{},
|
||||
})
|
||||
stream.Log(&logger.Message{
|
||||
Line: []byte(logline + " 2"),
|
||||
Timestamp: time.Time{},
|
||||
})
|
||||
|
||||
ticks <- time.Time{}
|
||||
|
||||
// Verify first batch
|
||||
argument := <-mockClient.putLogEventsArgument
|
||||
if argument == nil {
|
||||
t.Fatal("Expected non-nil PutLogEventsInput")
|
||||
}
|
||||
if len(argument.LogEvents) != 2 {
|
||||
t.Errorf("Expected LogEvents to contain 2 elements, but contains %d", len(argument.LogEvents))
|
||||
}
|
||||
if *argument.LogEvents[0].Message != logline+" 1" {
|
||||
t.Errorf("Expected message to be %s but was %s", logline+" 1", *argument.LogEvents[0].Message)
|
||||
}
|
||||
if *argument.LogEvents[1].Message != logline+" 2" {
|
||||
t.Errorf("Expected message to be %s but was %s", logline+" 2", *argument.LogEvents[0].Message)
|
||||
}
|
||||
|
||||
stream.Log(&logger.Message{
|
||||
Line: []byte(logline + " 3"),
|
||||
Timestamp: time.Time{},
|
||||
})
|
||||
|
||||
ticks <- time.Time{}
|
||||
argument = <-mockClient.putLogEventsArgument
|
||||
if argument == nil {
|
||||
t.Fatal("Expected non-nil PutLogEventsInput")
|
||||
}
|
||||
if len(argument.LogEvents) != 1 {
|
||||
t.Errorf("Expected LogEvents to contain 1 elements, but contains %d", len(argument.LogEvents))
|
||||
}
|
||||
if *argument.LogEvents[0].Message != logline+" 3" {
|
||||
t.Errorf("Expected message to be %s but was %s", logline+" 3", *argument.LogEvents[0].Message)
|
||||
}
|
||||
|
||||
stream.Close()
|
||||
|
||||
}
|
||||
|
||||
func TestCollectBatchClose(t *testing.T) {
|
||||
mockClient := newMockClient()
|
||||
stream := &logStream{
|
||||
client: mockClient,
|
||||
logGroupName: groupName,
|
||||
logStreamName: streamName,
|
||||
sequenceToken: aws.String(sequenceToken),
|
||||
messages: make(chan *logger.Message),
|
||||
}
|
||||
mockClient.putLogEventsResult <- &putLogEventsResult{
|
||||
successResult: &cloudwatchlogs.PutLogEventsOutput{
|
||||
NextSequenceToken: aws.String(nextSequenceToken),
|
||||
},
|
||||
}
|
||||
var ticks = make(chan time.Time)
|
||||
newTicker = func(_ time.Duration) *time.Ticker {
|
||||
return &time.Ticker{
|
||||
C: ticks,
|
||||
}
|
||||
}
|
||||
|
||||
go stream.collectBatch()
|
||||
|
||||
stream.Log(&logger.Message{
|
||||
Line: []byte(logline),
|
||||
Timestamp: time.Time{},
|
||||
})
|
||||
|
||||
// no ticks
|
||||
stream.Close()
|
||||
|
||||
argument := <-mockClient.putLogEventsArgument
|
||||
if argument == nil {
|
||||
t.Fatal("Expected non-nil PutLogEventsInput")
|
||||
}
|
||||
if len(argument.LogEvents) != 1 {
|
||||
t.Errorf("Expected LogEvents to contain 1 element, but contains %d", len(argument.LogEvents))
|
||||
}
|
||||
if *argument.LogEvents[0].Message != logline {
|
||||
t.Errorf("Expected message to be %s but was %s", logline, *argument.LogEvents[0].Message)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectBatchLineSplit(t *testing.T) {
|
||||
mockClient := newMockClient()
|
||||
stream := &logStream{
|
||||
client: mockClient,
|
||||
logGroupName: groupName,
|
||||
logStreamName: streamName,
|
||||
sequenceToken: aws.String(sequenceToken),
|
||||
messages: make(chan *logger.Message),
|
||||
}
|
||||
mockClient.putLogEventsResult <- &putLogEventsResult{
|
||||
successResult: &cloudwatchlogs.PutLogEventsOutput{
|
||||
NextSequenceToken: aws.String(nextSequenceToken),
|
||||
},
|
||||
}
|
||||
var ticks = make(chan time.Time)
|
||||
newTicker = func(_ time.Duration) *time.Ticker {
|
||||
return &time.Ticker{
|
||||
C: ticks,
|
||||
}
|
||||
}
|
||||
|
||||
go stream.collectBatch()
|
||||
|
||||
longline := strings.Repeat("A", maximumBytesPerEvent)
|
||||
stream.Log(&logger.Message{
|
||||
Line: []byte(longline + "B"),
|
||||
Timestamp: time.Time{},
|
||||
})
|
||||
|
||||
// no ticks
|
||||
stream.Close()
|
||||
|
||||
argument := <-mockClient.putLogEventsArgument
|
||||
if argument == nil {
|
||||
t.Fatal("Expected non-nil PutLogEventsInput")
|
||||
}
|
||||
if len(argument.LogEvents) != 2 {
|
||||
t.Errorf("Expected LogEvents to contain 2 elements, but contains %d", len(argument.LogEvents))
|
||||
}
|
||||
if *argument.LogEvents[0].Message != longline {
|
||||
t.Errorf("Expected message to be %s but was %s", longline, *argument.LogEvents[0].Message)
|
||||
}
|
||||
if *argument.LogEvents[1].Message != "B" {
|
||||
t.Errorf("Expected message to be %s but was %s", "B", *argument.LogEvents[1].Message)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectBatchMaxEvents(t *testing.T) {
|
||||
mockClient := newMockClientBuffered(1)
|
||||
stream := &logStream{
|
||||
client: mockClient,
|
||||
logGroupName: groupName,
|
||||
logStreamName: streamName,
|
||||
sequenceToken: aws.String(sequenceToken),
|
||||
messages: make(chan *logger.Message),
|
||||
}
|
||||
mockClient.putLogEventsResult <- &putLogEventsResult{
|
||||
successResult: &cloudwatchlogs.PutLogEventsOutput{
|
||||
NextSequenceToken: aws.String(nextSequenceToken),
|
||||
},
|
||||
}
|
||||
var ticks = make(chan time.Time)
|
||||
newTicker = func(_ time.Duration) *time.Ticker {
|
||||
return &time.Ticker{
|
||||
C: ticks,
|
||||
}
|
||||
}
|
||||
|
||||
go stream.collectBatch()
|
||||
|
||||
line := "A"
|
||||
for i := 0; i <= maximumLogEventsPerPut; i++ {
|
||||
stream.Log(&logger.Message{
|
||||
Line: []byte(line),
|
||||
Timestamp: time.Time{},
|
||||
})
|
||||
}
|
||||
|
||||
// no ticks
|
||||
stream.Close()
|
||||
|
||||
argument := <-mockClient.putLogEventsArgument
|
||||
if argument == nil {
|
||||
t.Fatal("Expected non-nil PutLogEventsInput")
|
||||
}
|
||||
if len(argument.LogEvents) != maximumLogEventsPerPut {
|
||||
t.Errorf("Expected LogEvents to contain %d elements, but contains %d", maximumLogEventsPerPut, len(argument.LogEvents))
|
||||
}
|
||||
|
||||
argument = <-mockClient.putLogEventsArgument
|
||||
if argument == nil {
|
||||
t.Fatal("Expected non-nil PutLogEventsInput")
|
||||
}
|
||||
if len(argument.LogEvents) != 1 {
|
||||
t.Errorf("Expected LogEvents to contain %d elements, but contains %d", 1, len(argument.LogEvents))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectBatchMaxTotalBytes(t *testing.T) {
|
||||
mockClient := newMockClientBuffered(1)
|
||||
stream := &logStream{
|
||||
client: mockClient,
|
||||
logGroupName: groupName,
|
||||
logStreamName: streamName,
|
||||
sequenceToken: aws.String(sequenceToken),
|
||||
messages: make(chan *logger.Message),
|
||||
}
|
||||
mockClient.putLogEventsResult <- &putLogEventsResult{
|
||||
successResult: &cloudwatchlogs.PutLogEventsOutput{
|
||||
NextSequenceToken: aws.String(nextSequenceToken),
|
||||
},
|
||||
}
|
||||
var ticks = make(chan time.Time)
|
||||
newTicker = func(_ time.Duration) *time.Ticker {
|
||||
return &time.Ticker{
|
||||
C: ticks,
|
||||
}
|
||||
}
|
||||
|
||||
go stream.collectBatch()
|
||||
|
||||
longline := strings.Repeat("A", maximumBytesPerPut)
|
||||
stream.Log(&logger.Message{
|
||||
Line: []byte(longline + "B"),
|
||||
Timestamp: time.Time{},
|
||||
})
|
||||
|
||||
// no ticks
|
||||
stream.Close()
|
||||
|
||||
argument := <-mockClient.putLogEventsArgument
|
||||
if argument == nil {
|
||||
t.Fatal("Expected non-nil PutLogEventsInput")
|
||||
}
|
||||
bytes := 0
|
||||
for _, event := range argument.LogEvents {
|
||||
bytes += len(*event.Message)
|
||||
}
|
||||
if bytes > maximumBytesPerPut {
|
||||
t.Errorf("Expected <= %d bytes but was %d", maximumBytesPerPut, bytes)
|
||||
}
|
||||
|
||||
argument = <-mockClient.putLogEventsArgument
|
||||
if len(argument.LogEvents) != 1 {
|
||||
t.Errorf("Expected LogEvents to contain 1 elements, but contains %d", len(argument.LogEvents))
|
||||
}
|
||||
message := *argument.LogEvents[0].Message
|
||||
if message[len(message)-1:] != "B" {
|
||||
t.Errorf("Expected message to be %s but was %s", "B", message[len(message)-1:])
|
||||
}
|
||||
}
|
||||
76
vendor/github.com/hyperhq/hypercli/daemon/logger/awslogs/cwlogsiface_mock_test.go
generated
vendored
Normal file
76
vendor/github.com/hyperhq/hypercli/daemon/logger/awslogs/cwlogsiface_mock_test.go
generated
vendored
Normal file
@@ -0,0 +1,76 @@
|
||||
package awslogs
|
||||
|
||||
import "github.com/aws/aws-sdk-go/service/cloudwatchlogs"
|
||||
|
||||
type mockcwlogsclient struct {
|
||||
createLogStreamArgument chan *cloudwatchlogs.CreateLogStreamInput
|
||||
createLogStreamResult chan *createLogStreamResult
|
||||
putLogEventsArgument chan *cloudwatchlogs.PutLogEventsInput
|
||||
putLogEventsResult chan *putLogEventsResult
|
||||
}
|
||||
|
||||
type createLogStreamResult struct {
|
||||
successResult *cloudwatchlogs.CreateLogStreamOutput
|
||||
errorResult error
|
||||
}
|
||||
|
||||
type putLogEventsResult struct {
|
||||
successResult *cloudwatchlogs.PutLogEventsOutput
|
||||
errorResult error
|
||||
}
|
||||
|
||||
func newMockClient() *mockcwlogsclient {
|
||||
return &mockcwlogsclient{
|
||||
createLogStreamArgument: make(chan *cloudwatchlogs.CreateLogStreamInput, 1),
|
||||
createLogStreamResult: make(chan *createLogStreamResult, 1),
|
||||
putLogEventsArgument: make(chan *cloudwatchlogs.PutLogEventsInput, 1),
|
||||
putLogEventsResult: make(chan *putLogEventsResult, 1),
|
||||
}
|
||||
}
|
||||
|
||||
func newMockClientBuffered(buflen int) *mockcwlogsclient {
|
||||
return &mockcwlogsclient{
|
||||
createLogStreamArgument: make(chan *cloudwatchlogs.CreateLogStreamInput, buflen),
|
||||
createLogStreamResult: make(chan *createLogStreamResult, buflen),
|
||||
putLogEventsArgument: make(chan *cloudwatchlogs.PutLogEventsInput, buflen),
|
||||
putLogEventsResult: make(chan *putLogEventsResult, buflen),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *mockcwlogsclient) CreateLogStream(input *cloudwatchlogs.CreateLogStreamInput) (*cloudwatchlogs.CreateLogStreamOutput, error) {
|
||||
m.createLogStreamArgument <- input
|
||||
output := <-m.createLogStreamResult
|
||||
return output.successResult, output.errorResult
|
||||
}
|
||||
|
||||
func (m *mockcwlogsclient) PutLogEvents(input *cloudwatchlogs.PutLogEventsInput) (*cloudwatchlogs.PutLogEventsOutput, error) {
|
||||
m.putLogEventsArgument <- input
|
||||
output := <-m.putLogEventsResult
|
||||
return output.successResult, output.errorResult
|
||||
}
|
||||
|
||||
type mockmetadataclient struct {
|
||||
regionResult chan *regionResult
|
||||
}
|
||||
|
||||
type regionResult struct {
|
||||
successResult string
|
||||
errorResult error
|
||||
}
|
||||
|
||||
func newMockMetadataClient() *mockmetadataclient {
|
||||
return &mockmetadataclient{
|
||||
regionResult: make(chan *regionResult, 1),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *mockmetadataclient) Region() (string, error) {
|
||||
output := <-m.regionResult
|
||||
return output.successResult, output.errorResult
|
||||
}
|
||||
|
||||
func test() {
|
||||
_ = &logStream{
|
||||
client: newMockClient(),
|
||||
}
|
||||
}
|
||||
112
vendor/github.com/hyperhq/hypercli/daemon/logger/context.go
generated
vendored
Normal file
112
vendor/github.com/hyperhq/hypercli/daemon/logger/context.go
generated
vendored
Normal file
@@ -0,0 +1,112 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Context provides enough information for a logging driver to do its function.
|
||||
type Context struct {
|
||||
Config map[string]string
|
||||
ContainerID string
|
||||
ContainerName string
|
||||
ContainerEntrypoint string
|
||||
ContainerArgs []string
|
||||
ContainerImageID string
|
||||
ContainerImageName string
|
||||
ContainerCreated time.Time
|
||||
ContainerEnv []string
|
||||
ContainerLabels map[string]string
|
||||
LogPath string
|
||||
}
|
||||
|
||||
// ExtraAttributes returns the user-defined extra attributes (labels,
|
||||
// environment variables) in key-value format. This can be used by log drivers
|
||||
// that support metadata to add more context to a log.
|
||||
func (ctx *Context) ExtraAttributes(keyMod func(string) string) map[string]string {
|
||||
extra := make(map[string]string)
|
||||
labels, ok := ctx.Config["labels"]
|
||||
if ok && len(labels) > 0 {
|
||||
for _, l := range strings.Split(labels, ",") {
|
||||
if v, ok := ctx.ContainerLabels[l]; ok {
|
||||
if keyMod != nil {
|
||||
l = keyMod(l)
|
||||
}
|
||||
extra[l] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
env, ok := ctx.Config["env"]
|
||||
if ok && len(env) > 0 {
|
||||
envMapping := make(map[string]string)
|
||||
for _, e := range ctx.ContainerEnv {
|
||||
if kv := strings.SplitN(e, "=", 2); len(kv) == 2 {
|
||||
envMapping[kv[0]] = kv[1]
|
||||
}
|
||||
}
|
||||
for _, l := range strings.Split(env, ",") {
|
||||
if v, ok := envMapping[l]; ok {
|
||||
if keyMod != nil {
|
||||
l = keyMod(l)
|
||||
}
|
||||
extra[l] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return extra
|
||||
}
|
||||
|
||||
// Hostname returns the hostname from the underlying OS.
|
||||
func (ctx *Context) Hostname() (string, error) {
|
||||
hostname, err := os.Hostname()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("logger: can not resolve hostname: %v", err)
|
||||
}
|
||||
return hostname, nil
|
||||
}
|
||||
|
||||
// Command returns the command that the container being logged was
|
||||
// started with. The Entrypoint is prepended to the container
|
||||
// arguments.
|
||||
func (ctx *Context) Command() string {
|
||||
terms := []string{ctx.ContainerEntrypoint}
|
||||
for _, arg := range ctx.ContainerArgs {
|
||||
terms = append(terms, arg)
|
||||
}
|
||||
command := strings.Join(terms, " ")
|
||||
return command
|
||||
}
|
||||
|
||||
// ID Returns the Container ID shortened to 12 characters.
|
||||
func (ctx *Context) ID() string {
|
||||
return ctx.ContainerID[:12]
|
||||
}
|
||||
|
||||
// FullID is an alias of ContainerID.
|
||||
func (ctx *Context) FullID() string {
|
||||
return ctx.ContainerID
|
||||
}
|
||||
|
||||
// Name returns the ContainerName without a preceding '/'.
|
||||
func (ctx *Context) Name() string {
|
||||
return ctx.ContainerName[1:]
|
||||
}
|
||||
|
||||
// ImageID returns the ContainerImageID shortened to 12 characters.
|
||||
func (ctx *Context) ImageID() string {
|
||||
return ctx.ContainerImageID[:12]
|
||||
}
|
||||
|
||||
// ImageFullID is an alias of ContainerImageID.
|
||||
func (ctx *Context) ImageFullID() string {
|
||||
return ctx.ContainerImageID
|
||||
}
|
||||
|
||||
// ImageName is an alias of ContainerImageName
|
||||
func (ctx *Context) ImageName() string {
|
||||
return ctx.ContainerImageName
|
||||
}
|
||||
86
vendor/github.com/hyperhq/hypercli/daemon/logger/copier.go
generated
vendored
Normal file
86
vendor/github.com/hyperhq/hypercli/daemon/logger/copier.go
generated
vendored
Normal file
@@ -0,0 +1,86 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"io"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
)
|
||||
|
||||
// Copier can copy logs from specified sources to Logger and attach
|
||||
// ContainerID and Timestamp.
|
||||
// Writes are concurrent, so you need implement some sync in your logger
|
||||
type Copier struct {
|
||||
// cid is the container id for which we are copying logs
|
||||
cid string
|
||||
// srcs is map of name -> reader pairs, for example "stdout", "stderr"
|
||||
srcs map[string]io.Reader
|
||||
dst Logger
|
||||
copyJobs sync.WaitGroup
|
||||
closed chan struct{}
|
||||
}
|
||||
|
||||
// NewCopier creates a new Copier
|
||||
func NewCopier(cid string, srcs map[string]io.Reader, dst Logger) *Copier {
|
||||
return &Copier{
|
||||
cid: cid,
|
||||
srcs: srcs,
|
||||
dst: dst,
|
||||
closed: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
// Run starts logs copying
|
||||
func (c *Copier) Run() {
|
||||
for src, w := range c.srcs {
|
||||
c.copyJobs.Add(1)
|
||||
go c.copySrc(src, w)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Copier) copySrc(name string, src io.Reader) {
|
||||
defer c.copyJobs.Done()
|
||||
reader := bufio.NewReader(src)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-c.closed:
|
||||
return
|
||||
default:
|
||||
line, err := reader.ReadBytes('\n')
|
||||
line = bytes.TrimSuffix(line, []byte{'\n'})
|
||||
|
||||
// ReadBytes can return full or partial output even when it failed.
|
||||
// e.g. it can return a full entry and EOF.
|
||||
if err == nil || len(line) > 0 {
|
||||
if logErr := c.dst.Log(&Message{ContainerID: c.cid, Line: line, Source: name, Timestamp: time.Now().UTC()}); logErr != nil {
|
||||
logrus.Errorf("Failed to log msg %q for logger %s: %s", line, c.dst.Name(), logErr)
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if err != io.EOF {
|
||||
logrus.Errorf("Error scanning log stream: %s", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Wait waits until all copying is done
|
||||
func (c *Copier) Wait() {
|
||||
c.copyJobs.Wait()
|
||||
}
|
||||
|
||||
// Close closes the copier
|
||||
func (c *Copier) Close() {
|
||||
select {
|
||||
case <-c.closed:
|
||||
default:
|
||||
close(c.closed)
|
||||
}
|
||||
}
|
||||
132
vendor/github.com/hyperhq/hypercli/daemon/logger/copier_test.go
generated
vendored
Normal file
132
vendor/github.com/hyperhq/hypercli/daemon/logger/copier_test.go
generated
vendored
Normal file
@@ -0,0 +1,132 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type TestLoggerJSON struct {
|
||||
*json.Encoder
|
||||
delay time.Duration
|
||||
}
|
||||
|
||||
func (l *TestLoggerJSON) Log(m *Message) error {
|
||||
if l.delay > 0 {
|
||||
time.Sleep(l.delay)
|
||||
}
|
||||
return l.Encode(m)
|
||||
}
|
||||
|
||||
func (l *TestLoggerJSON) Close() error { return nil }
|
||||
|
||||
func (l *TestLoggerJSON) Name() string { return "json" }
|
||||
|
||||
type TestLoggerText struct {
|
||||
*bytes.Buffer
|
||||
}
|
||||
|
||||
func (l *TestLoggerText) Log(m *Message) error {
|
||||
_, err := l.WriteString(m.ContainerID + " " + m.Source + " " + string(m.Line) + "\n")
|
||||
return err
|
||||
}
|
||||
|
||||
func (l *TestLoggerText) Close() error { return nil }
|
||||
|
||||
func (l *TestLoggerText) Name() string { return "text" }
|
||||
|
||||
func TestCopier(t *testing.T) {
|
||||
stdoutLine := "Line that thinks that it is log line from docker stdout"
|
||||
stderrLine := "Line that thinks that it is log line from docker stderr"
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
for i := 0; i < 30; i++ {
|
||||
if _, err := stdout.WriteString(stdoutLine + "\n"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := stderr.WriteString(stderrLine + "\n"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
var jsonBuf bytes.Buffer
|
||||
|
||||
jsonLog := &TestLoggerJSON{Encoder: json.NewEncoder(&jsonBuf)}
|
||||
|
||||
cid := "a7317399f3f857173c6179d44823594f8294678dea9999662e5c625b5a1c7657"
|
||||
c := NewCopier(cid,
|
||||
map[string]io.Reader{
|
||||
"stdout": &stdout,
|
||||
"stderr": &stderr,
|
||||
},
|
||||
jsonLog)
|
||||
c.Run()
|
||||
wait := make(chan struct{})
|
||||
go func() {
|
||||
c.Wait()
|
||||
close(wait)
|
||||
}()
|
||||
select {
|
||||
case <-time.After(1 * time.Second):
|
||||
t.Fatal("Copier failed to do its work in 1 second")
|
||||
case <-wait:
|
||||
}
|
||||
dec := json.NewDecoder(&jsonBuf)
|
||||
for {
|
||||
var msg Message
|
||||
if err := dec.Decode(&msg); err != nil {
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
t.Fatal(err)
|
||||
}
|
||||
if msg.Source != "stdout" && msg.Source != "stderr" {
|
||||
t.Fatalf("Wrong Source: %q, should be %q or %q", msg.Source, "stdout", "stderr")
|
||||
}
|
||||
if msg.ContainerID != cid {
|
||||
t.Fatalf("Wrong ContainerID: %q, expected %q", msg.ContainerID, cid)
|
||||
}
|
||||
if msg.Source == "stdout" {
|
||||
if string(msg.Line) != stdoutLine {
|
||||
t.Fatalf("Wrong Line: %q, expected %q", msg.Line, stdoutLine)
|
||||
}
|
||||
}
|
||||
if msg.Source == "stderr" {
|
||||
if string(msg.Line) != stderrLine {
|
||||
t.Fatalf("Wrong Line: %q, expected %q", msg.Line, stderrLine)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCopierSlow(t *testing.T) {
|
||||
stdoutLine := "Line that thinks that it is log line from docker stdout"
|
||||
var stdout bytes.Buffer
|
||||
for i := 0; i < 30; i++ {
|
||||
if _, err := stdout.WriteString(stdoutLine + "\n"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
var jsonBuf bytes.Buffer
|
||||
//encoder := &encodeCloser{Encoder: json.NewEncoder(&jsonBuf)}
|
||||
jsonLog := &TestLoggerJSON{Encoder: json.NewEncoder(&jsonBuf), delay: 100 * time.Millisecond}
|
||||
|
||||
cid := "a7317399f3f857173c6179d44823594f8294678dea9999662e5c625b5a1c7657"
|
||||
c := NewCopier(cid, map[string]io.Reader{"stdout": &stdout}, jsonLog)
|
||||
c.Run()
|
||||
wait := make(chan struct{})
|
||||
go func() {
|
||||
c.Wait()
|
||||
close(wait)
|
||||
}()
|
||||
<-time.After(150 * time.Millisecond)
|
||||
c.Close()
|
||||
select {
|
||||
case <-time.After(200 * time.Millisecond):
|
||||
t.Fatalf("failed to exit in time after the copier is closed")
|
||||
case <-wait:
|
||||
}
|
||||
}
|
||||
89
vendor/github.com/hyperhq/hypercli/daemon/logger/factory.go
generated
vendored
Normal file
89
vendor/github.com/hyperhq/hypercli/daemon/logger/factory.go
generated
vendored
Normal file
@@ -0,0 +1,89 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Creator builds a logging driver instance with given context.
|
||||
type Creator func(Context) (Logger, error)
|
||||
|
||||
// LogOptValidator checks the options specific to the underlying
|
||||
// logging implementation.
|
||||
type LogOptValidator func(cfg map[string]string) error
|
||||
|
||||
type logdriverFactory struct {
|
||||
registry map[string]Creator
|
||||
optValidator map[string]LogOptValidator
|
||||
m sync.Mutex
|
||||
}
|
||||
|
||||
func (lf *logdriverFactory) register(name string, c Creator) error {
|
||||
lf.m.Lock()
|
||||
defer lf.m.Unlock()
|
||||
|
||||
if _, ok := lf.registry[name]; ok {
|
||||
return fmt.Errorf("logger: log driver named '%s' is already registered", name)
|
||||
}
|
||||
lf.registry[name] = c
|
||||
return nil
|
||||
}
|
||||
|
||||
func (lf *logdriverFactory) registerLogOptValidator(name string, l LogOptValidator) error {
|
||||
lf.m.Lock()
|
||||
defer lf.m.Unlock()
|
||||
|
||||
if _, ok := lf.optValidator[name]; ok {
|
||||
return fmt.Errorf("logger: log validator named '%s' is already registered", name)
|
||||
}
|
||||
lf.optValidator[name] = l
|
||||
return nil
|
||||
}
|
||||
|
||||
func (lf *logdriverFactory) get(name string) (Creator, error) {
|
||||
lf.m.Lock()
|
||||
defer lf.m.Unlock()
|
||||
|
||||
c, ok := lf.registry[name]
|
||||
if !ok {
|
||||
return c, fmt.Errorf("logger: no log driver named '%s' is registered", name)
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (lf *logdriverFactory) getLogOptValidator(name string) LogOptValidator {
|
||||
lf.m.Lock()
|
||||
defer lf.m.Unlock()
|
||||
|
||||
c, _ := lf.optValidator[name]
|
||||
return c
|
||||
}
|
||||
|
||||
var factory = &logdriverFactory{registry: make(map[string]Creator), optValidator: make(map[string]LogOptValidator)} // global factory instance
|
||||
|
||||
// RegisterLogDriver registers the given logging driver builder with given logging
|
||||
// driver name.
|
||||
func RegisterLogDriver(name string, c Creator) error {
|
||||
return factory.register(name, c)
|
||||
}
|
||||
|
||||
// RegisterLogOptValidator registers the logging option validator with
|
||||
// the given logging driver name.
|
||||
func RegisterLogOptValidator(name string, l LogOptValidator) error {
|
||||
return factory.registerLogOptValidator(name, l)
|
||||
}
|
||||
|
||||
// GetLogDriver provides the logging driver builder for a logging driver name.
|
||||
func GetLogDriver(name string) (Creator, error) {
|
||||
return factory.get(name)
|
||||
}
|
||||
|
||||
// ValidateLogOpts checks the options for the given log driver. The
|
||||
// options supported are specific to the LogDriver implementation.
|
||||
func ValidateLogOpts(name string, cfg map[string]string) error {
|
||||
l := factory.getLogOptValidator(name)
|
||||
if l != nil {
|
||||
return l(cfg)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
160
vendor/github.com/hyperhq/hypercli/daemon/logger/fluentd/fluentd.go
generated
vendored
Normal file
160
vendor/github.com/hyperhq/hypercli/daemon/logger/fluentd/fluentd.go
generated
vendored
Normal file
@@ -0,0 +1,160 @@
|
||||
// Package fluentd provides the log driver for forwarding server logs
|
||||
// to fluentd endpoints.
|
||||
package fluentd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/hyperhq/hypercli/daemon/logger"
|
||||
"github.com/hyperhq/hypercli/daemon/logger/loggerutils"
|
||||
"github.com/fluent/fluent-logger-golang/fluent"
|
||||
)
|
||||
|
||||
type fluentd struct {
|
||||
tag string
|
||||
containerID string
|
||||
containerName string
|
||||
writer *fluent.Fluent
|
||||
extra map[string]string
|
||||
}
|
||||
|
||||
const (
|
||||
name = "fluentd"
|
||||
defaultHostName = "localhost"
|
||||
defaultPort = 24224
|
||||
defaultTagPrefix = "docker"
|
||||
defaultIgnoreConnectErrorOnStart = false // So that we do not break existing behaviour
|
||||
defaultBufferLimit = 1 * 1024 * 1024 // 1M buffer by default
|
||||
)
|
||||
|
||||
func init() {
|
||||
if err := logger.RegisterLogDriver(name, New); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
if err := logger.RegisterLogOptValidator(name, ValidateLogOpt); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// New creates a fluentd logger using the configuration passed in on
|
||||
// the context. Supported context configuration variables are
|
||||
// fluentd-address & fluentd-tag.
|
||||
func New(ctx logger.Context) (logger.Logger, error) {
|
||||
host, port, err := parseAddress(ctx.Config["fluentd-address"])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tag, err := loggerutils.ParseLogTag(ctx, "docker.{{.ID}}")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
failOnStartupError, err := loggerutils.ParseFailOnStartupErrorFlag(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bufferLimit, err := parseBufferLimit(ctx.Config["buffer-limit"])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
extra := ctx.ExtraAttributes(nil)
|
||||
logrus.Debugf("logging driver fluentd configured for container:%s, host:%s, port:%d, tag:%s, extra:%v.", ctx.ContainerID, host, port, tag, extra)
|
||||
// logger tries to reconnect 2**32 - 1 times
|
||||
// failed (and panic) after 204 years [ 1.5 ** (2**32 - 1) - 1 seconds]
|
||||
log, err := fluent.New(fluent.Config{FluentPort: port, FluentHost: host, RetryWait: 1000, MaxRetry: math.MaxInt32, BufferLimit: bufferLimit})
|
||||
if err != nil {
|
||||
if failOnStartupError {
|
||||
return nil, err
|
||||
}
|
||||
logrus.Warnf("fluentd cannot connect to configured endpoint. Ignoring as instructed. Error: %q", err)
|
||||
}
|
||||
return &fluentd{
|
||||
tag: tag,
|
||||
containerID: ctx.ContainerID,
|
||||
containerName: ctx.ContainerName,
|
||||
writer: log,
|
||||
extra: extra,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (f *fluentd) Log(msg *logger.Message) error {
|
||||
data := map[string]string{
|
||||
"container_id": f.containerID,
|
||||
"container_name": f.containerName,
|
||||
"source": msg.Source,
|
||||
"log": string(msg.Line),
|
||||
}
|
||||
for k, v := range f.extra {
|
||||
data[k] = v
|
||||
}
|
||||
// fluent-logger-golang buffers logs from failures and disconnections,
|
||||
// and these are transferred again automatically.
|
||||
return f.writer.PostWithTime(f.tag, msg.Timestamp, data)
|
||||
}
|
||||
|
||||
func (f *fluentd) Close() error {
|
||||
return f.writer.Close()
|
||||
}
|
||||
|
||||
func (f *fluentd) Name() string {
|
||||
return name
|
||||
}
|
||||
|
||||
// ValidateLogOpt looks for fluentd specific log options fluentd-address & fluentd-tag.
|
||||
func ValidateLogOpt(cfg map[string]string) error {
|
||||
for key := range cfg {
|
||||
switch key {
|
||||
case "fluentd-address":
|
||||
case "fluentd-tag":
|
||||
case "tag":
|
||||
case "labels":
|
||||
case "env":
|
||||
case "fail-on-startup-error":
|
||||
case "buffer-limit":
|
||||
default:
|
||||
return fmt.Errorf("unknown log opt '%s' for fluentd log driver", key)
|
||||
}
|
||||
}
|
||||
|
||||
if _, _, err := parseAddress(cfg["fluentd-address"]); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseAddress(address string) (string, int, error) {
|
||||
if address == "" {
|
||||
return defaultHostName, defaultPort, nil
|
||||
}
|
||||
|
||||
host, port, err := net.SplitHostPort(address)
|
||||
if err != nil {
|
||||
if !strings.Contains(err.Error(), "missing port in address") {
|
||||
return "", 0, fmt.Errorf("invalid fluentd-address %s: %s", address, err)
|
||||
}
|
||||
return host, defaultPort, nil
|
||||
}
|
||||
|
||||
portnum, err := strconv.Atoi(port)
|
||||
if err != nil {
|
||||
return "", 0, fmt.Errorf("invalid fluentd-address %s: %s", address, err)
|
||||
}
|
||||
return host, portnum, nil
|
||||
}
|
||||
|
||||
func parseBufferLimit(bufferLimit string) (int, error) {
|
||||
if bufferLimit == "" {
|
||||
return defaultBufferLimit, nil
|
||||
}
|
||||
limit, err := strconv.Atoi(bufferLimit)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("invalid buffer limit %s: %s", bufferLimit, err)
|
||||
}
|
||||
return limit, nil
|
||||
}
|
||||
172
vendor/github.com/hyperhq/hypercli/daemon/logger/gelf/gelf.go
generated
vendored
Normal file
172
vendor/github.com/hyperhq/hypercli/daemon/logger/gelf/gelf.go
generated
vendored
Normal file
@@ -0,0 +1,172 @@
|
||||
// +build linux
|
||||
|
||||
// Package gelf provides the log driver for forwarding server logs to
|
||||
// endpoints that support the Graylog Extended Log Format.
|
||||
package gelf
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/Graylog2/go-gelf/gelf"
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/hyperhq/hypercli/daemon/logger"
|
||||
"github.com/hyperhq/hypercli/daemon/logger/loggerutils"
|
||||
"github.com/hyperhq/hypercli/pkg/urlutil"
|
||||
)
|
||||
|
||||
const name = "gelf"
|
||||
|
||||
type gelfLogger struct {
|
||||
writer *gelf.Writer
|
||||
ctx logger.Context
|
||||
hostname string
|
||||
extra map[string]interface{}
|
||||
}
|
||||
|
||||
func init() {
|
||||
if err := logger.RegisterLogDriver(name, New); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
if err := logger.RegisterLogOptValidator(name, ValidateLogOpt); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// New creates a gelf logger using the configuration passed in on the
|
||||
// context. Supported context configuration variables are
|
||||
// gelf-address, & gelf-tag.
|
||||
func New(ctx logger.Context) (logger.Logger, error) {
|
||||
// parse gelf address
|
||||
address, err := parseAddress(ctx.Config["gelf-address"])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// collect extra data for GELF message
|
||||
hostname, err := ctx.Hostname()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("gelf: cannot access hostname to set source field")
|
||||
}
|
||||
|
||||
// remove trailing slash from container name
|
||||
containerName := bytes.TrimLeft([]byte(ctx.ContainerName), "/")
|
||||
|
||||
// parse log tag
|
||||
tag, err := loggerutils.ParseLogTag(ctx, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
extra := map[string]interface{}{
|
||||
"_container_id": ctx.ContainerID,
|
||||
"_container_name": string(containerName),
|
||||
"_image_id": ctx.ContainerImageID,
|
||||
"_image_name": ctx.ContainerImageName,
|
||||
"_command": ctx.Command(),
|
||||
"_tag": tag,
|
||||
"_created": ctx.ContainerCreated,
|
||||
}
|
||||
|
||||
extraAttrs := ctx.ExtraAttributes(func(key string) string {
|
||||
if key[0] == '_' {
|
||||
return key
|
||||
}
|
||||
return "_" + key
|
||||
})
|
||||
for k, v := range extraAttrs {
|
||||
extra[k] = v
|
||||
}
|
||||
|
||||
// create new gelfWriter
|
||||
gelfWriter, err := gelf.NewWriter(address)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("gelf: cannot connect to GELF endpoint: %s %v", address, err)
|
||||
}
|
||||
|
||||
return &gelfLogger{
|
||||
writer: gelfWriter,
|
||||
ctx: ctx,
|
||||
hostname: hostname,
|
||||
extra: extra,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *gelfLogger) Log(msg *logger.Message) error {
|
||||
level := gelf.LOG_INFO
|
||||
if msg.Source == "stderr" {
|
||||
level = gelf.LOG_ERR
|
||||
}
|
||||
|
||||
m := gelf.Message{
|
||||
Version: "1.1",
|
||||
Host: s.hostname,
|
||||
Short: string(msg.Line),
|
||||
TimeUnix: float64(msg.Timestamp.UnixNano()/int64(time.Millisecond)) / 1000.0,
|
||||
Level: level,
|
||||
Extra: s.extra,
|
||||
}
|
||||
|
||||
if err := s.writer.WriteMessage(&m); err != nil {
|
||||
return fmt.Errorf("gelf: cannot send GELF message: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *gelfLogger) Close() error {
|
||||
return s.writer.Close()
|
||||
}
|
||||
|
||||
func (s *gelfLogger) Name() string {
|
||||
return name
|
||||
}
|
||||
|
||||
// ValidateLogOpt looks for gelf specific log options gelf-address, &
|
||||
// gelf-tag.
|
||||
func ValidateLogOpt(cfg map[string]string) error {
|
||||
for key := range cfg {
|
||||
switch key {
|
||||
case "gelf-address":
|
||||
case "gelf-tag":
|
||||
case "tag":
|
||||
case "labels":
|
||||
case "env":
|
||||
default:
|
||||
return fmt.Errorf("unknown log opt '%s' for gelf log driver", key)
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := parseAddress(cfg["gelf-address"]); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseAddress(address string) (string, error) {
|
||||
if address == "" {
|
||||
return "", nil
|
||||
}
|
||||
if !urlutil.IsTransportURL(address) {
|
||||
return "", fmt.Errorf("gelf-address should be in form proto://address, got %v", address)
|
||||
}
|
||||
url, err := url.Parse(address)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// we support only udp
|
||||
if url.Scheme != "udp" {
|
||||
return "", fmt.Errorf("gelf: endpoint needs to be UDP")
|
||||
}
|
||||
|
||||
// get host and port
|
||||
if _, _, err = net.SplitHostPort(url.Host); err != nil {
|
||||
return "", fmt.Errorf("gelf: please provide gelf-address as udp://host:port")
|
||||
}
|
||||
|
||||
return url.Host, nil
|
||||
}
|
||||
3
vendor/github.com/hyperhq/hypercli/daemon/logger/gelf/gelf_unsupported.go
generated
vendored
Normal file
3
vendor/github.com/hyperhq/hypercli/daemon/logger/gelf/gelf_unsupported.go
generated
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
// +build !linux
|
||||
|
||||
package gelf
|
||||
95
vendor/github.com/hyperhq/hypercli/daemon/logger/journald/journald.go
generated
vendored
Normal file
95
vendor/github.com/hyperhq/hypercli/daemon/logger/journald/journald.go
generated
vendored
Normal file
@@ -0,0 +1,95 @@
|
||||
// +build linux
|
||||
|
||||
// Package journald provides the log driver for forwarding server logs
|
||||
// to endpoints that receive the systemd format.
|
||||
package journald
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/coreos/go-systemd/journal"
|
||||
"github.com/hyperhq/hypercli/daemon/logger"
|
||||
"github.com/hyperhq/hypercli/daemon/logger/loggerutils"
|
||||
)
|
||||
|
||||
const name = "journald"
|
||||
|
||||
type journald struct {
|
||||
vars map[string]string // additional variables and values to send to the journal along with the log message
|
||||
readers readerList
|
||||
}
|
||||
|
||||
type readerList struct {
|
||||
mu sync.Mutex
|
||||
readers map[*logger.LogWatcher]*logger.LogWatcher
|
||||
}
|
||||
|
||||
func init() {
|
||||
if err := logger.RegisterLogDriver(name, New); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
if err := logger.RegisterLogOptValidator(name, validateLogOpt); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// New creates a journald logger using the configuration passed in on
|
||||
// the context.
|
||||
func New(ctx logger.Context) (logger.Logger, error) {
|
||||
if !journal.Enabled() {
|
||||
return nil, fmt.Errorf("journald is not enabled on this host")
|
||||
}
|
||||
// Strip a leading slash so that people can search for
|
||||
// CONTAINER_NAME=foo rather than CONTAINER_NAME=/foo.
|
||||
name := ctx.ContainerName
|
||||
if name[0] == '/' {
|
||||
name = name[1:]
|
||||
}
|
||||
|
||||
// parse log tag
|
||||
tag, err := loggerutils.ParseLogTag(ctx, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
vars := map[string]string{
|
||||
"CONTAINER_ID": ctx.ContainerID[:12],
|
||||
"CONTAINER_ID_FULL": ctx.ContainerID,
|
||||
"CONTAINER_NAME": name,
|
||||
"CONTAINER_TAG": tag,
|
||||
}
|
||||
extraAttrs := ctx.ExtraAttributes(strings.ToTitle)
|
||||
for k, v := range extraAttrs {
|
||||
vars[k] = v
|
||||
}
|
||||
return &journald{vars: vars, readers: readerList{readers: make(map[*logger.LogWatcher]*logger.LogWatcher)}}, nil
|
||||
}
|
||||
|
||||
// We don't actually accept any options, but we have to supply a callback for
|
||||
// the factory to pass the (probably empty) configuration map to.
|
||||
func validateLogOpt(cfg map[string]string) error {
|
||||
for key := range cfg {
|
||||
switch key {
|
||||
case "labels":
|
||||
case "env":
|
||||
case "tag":
|
||||
default:
|
||||
return fmt.Errorf("unknown log opt '%s' for journald log driver", key)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *journald) Log(msg *logger.Message) error {
|
||||
if msg.Source == "stderr" {
|
||||
return journal.Send(string(msg.Line), journal.PriErr, s.vars)
|
||||
}
|
||||
return journal.Send(string(msg.Line), journal.PriInfo, s.vars)
|
||||
}
|
||||
|
||||
func (s *journald) Name() string {
|
||||
return name
|
||||
}
|
||||
6
vendor/github.com/hyperhq/hypercli/daemon/logger/journald/journald_unsupported.go
generated
vendored
Normal file
6
vendor/github.com/hyperhq/hypercli/daemon/logger/journald/journald_unsupported.go
generated
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
// +build !linux
|
||||
|
||||
package journald
|
||||
|
||||
type journald struct {
|
||||
}
|
||||
299
vendor/github.com/hyperhq/hypercli/daemon/logger/journald/read.go
generated
vendored
Normal file
299
vendor/github.com/hyperhq/hypercli/daemon/logger/journald/read.go
generated
vendored
Normal file
@@ -0,0 +1,299 @@
|
||||
// +build linux,cgo,!static_build,journald
|
||||
|
||||
package journald
|
||||
|
||||
// #cgo pkg-config: libsystemd-journal
|
||||
// #include <sys/types.h>
|
||||
// #include <sys/poll.h>
|
||||
// #include <systemd/sd-journal.h>
|
||||
// #include <errno.h>
|
||||
// #include <stdio.h>
|
||||
// #include <stdlib.h>
|
||||
// #include <string.h>
|
||||
// #include <time.h>
|
||||
// #include <unistd.h>
|
||||
//
|
||||
//static int get_message(sd_journal *j, const char **msg, size_t *length)
|
||||
//{
|
||||
// int rc;
|
||||
// *msg = NULL;
|
||||
// *length = 0;
|
||||
// rc = sd_journal_get_data(j, "MESSAGE", (const void **) msg, length);
|
||||
// if (rc == 0) {
|
||||
// if (*length > 8) {
|
||||
// (*msg) += 8;
|
||||
// *length -= 8;
|
||||
// } else {
|
||||
// *msg = NULL;
|
||||
// *length = 0;
|
||||
// rc = -ENOENT;
|
||||
// }
|
||||
// }
|
||||
// return rc;
|
||||
//}
|
||||
//static int get_priority(sd_journal *j, int *priority)
|
||||
//{
|
||||
// const void *data;
|
||||
// size_t i, length;
|
||||
// int rc;
|
||||
// *priority = -1;
|
||||
// rc = sd_journal_get_data(j, "PRIORITY", &data, &length);
|
||||
// if (rc == 0) {
|
||||
// if ((length > 9) && (strncmp(data, "PRIORITY=", 9) == 0)) {
|
||||
// *priority = 0;
|
||||
// for (i = 9; i < length; i++) {
|
||||
// *priority = *priority * 10 + ((const char *)data)[i] - '0';
|
||||
// }
|
||||
// if (length > 9) {
|
||||
// rc = 0;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// return rc;
|
||||
//}
|
||||
//static int wait_for_data_or_close(sd_journal *j, int pipefd)
|
||||
//{
|
||||
// struct pollfd fds[2];
|
||||
// uint64_t when = 0;
|
||||
// int timeout, jevents, i;
|
||||
// struct timespec ts;
|
||||
// uint64_t now;
|
||||
// do {
|
||||
// memset(&fds, 0, sizeof(fds));
|
||||
// fds[0].fd = pipefd;
|
||||
// fds[0].events = POLLHUP;
|
||||
// fds[1].fd = sd_journal_get_fd(j);
|
||||
// if (fds[1].fd < 0) {
|
||||
// return -1;
|
||||
// }
|
||||
// jevents = sd_journal_get_events(j);
|
||||
// if (jevents < 0) {
|
||||
// return -1;
|
||||
// }
|
||||
// fds[1].events = jevents;
|
||||
// sd_journal_get_timeout(j, &when);
|
||||
// if (when == -1) {
|
||||
// timeout = -1;
|
||||
// } else {
|
||||
// clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||
// now = (uint64_t) ts.tv_sec * 1000000 + ts.tv_nsec / 1000;
|
||||
// timeout = when > now ? (int) ((when - now + 999) / 1000) : 0;
|
||||
// }
|
||||
// i = poll(fds, 2, timeout);
|
||||
// if ((i == -1) && (errno != EINTR)) {
|
||||
// /* An unexpected error. */
|
||||
// return -1;
|
||||
// }
|
||||
// if (fds[0].revents & POLLHUP) {
|
||||
// /* The close notification pipe was closed. */
|
||||
// return 0;
|
||||
// }
|
||||
// if (sd_journal_process(j) == SD_JOURNAL_APPEND) {
|
||||
// /* Data, which we might care about, was appended. */
|
||||
// return 1;
|
||||
// }
|
||||
// } while ((fds[0].revents & POLLHUP) == 0);
|
||||
// return 0;
|
||||
//}
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/coreos/go-systemd/journal"
|
||||
"github.com/hyperhq/hypercli/daemon/logger"
|
||||
)
|
||||
|
||||
func (s *journald) Close() error {
|
||||
s.readers.mu.Lock()
|
||||
for reader := range s.readers.readers {
|
||||
reader.Close()
|
||||
}
|
||||
s.readers.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *journald) drainJournal(logWatcher *logger.LogWatcher, config logger.ReadConfig, j *C.sd_journal, oldCursor string) string {
|
||||
var msg, cursor *C.char
|
||||
var length C.size_t
|
||||
var stamp C.uint64_t
|
||||
var priority C.int
|
||||
|
||||
// Walk the journal from here forward until we run out of new entries.
|
||||
drain:
|
||||
for {
|
||||
// Try not to send a given entry twice.
|
||||
if oldCursor != "" {
|
||||
ccursor := C.CString(oldCursor)
|
||||
defer C.free(unsafe.Pointer(ccursor))
|
||||
for C.sd_journal_test_cursor(j, ccursor) > 0 {
|
||||
if C.sd_journal_next(j) <= 0 {
|
||||
break drain
|
||||
}
|
||||
}
|
||||
}
|
||||
// Read and send the logged message, if there is one to read.
|
||||
i := C.get_message(j, &msg, &length)
|
||||
if i != -C.ENOENT && i != -C.EADDRNOTAVAIL {
|
||||
// Read the entry's timestamp.
|
||||
if C.sd_journal_get_realtime_usec(j, &stamp) != 0 {
|
||||
break
|
||||
}
|
||||
// Set up the time and text of the entry.
|
||||
timestamp := time.Unix(int64(stamp)/1000000, (int64(stamp)%1000000)*1000)
|
||||
line := append(C.GoBytes(unsafe.Pointer(msg), C.int(length)), "\n"...)
|
||||
// Recover the stream name by mapping
|
||||
// from the journal priority back to
|
||||
// the stream that we would have
|
||||
// assigned that value.
|
||||
source := ""
|
||||
if C.get_priority(j, &priority) != 0 {
|
||||
source = ""
|
||||
} else if priority == C.int(journal.PriErr) {
|
||||
source = "stderr"
|
||||
} else if priority == C.int(journal.PriInfo) {
|
||||
source = "stdout"
|
||||
}
|
||||
// Send the log message.
|
||||
cid := s.vars["CONTAINER_ID_FULL"]
|
||||
logWatcher.Msg <- &logger.Message{ContainerID: cid, Line: line, Source: source, Timestamp: timestamp}
|
||||
}
|
||||
// If we're at the end of the journal, we're done (for now).
|
||||
if C.sd_journal_next(j) <= 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
retCursor := ""
|
||||
if C.sd_journal_get_cursor(j, &cursor) == 0 {
|
||||
retCursor = C.GoString(cursor)
|
||||
C.free(unsafe.Pointer(cursor))
|
||||
}
|
||||
return retCursor
|
||||
}
|
||||
|
||||
func (s *journald) followJournal(logWatcher *logger.LogWatcher, config logger.ReadConfig, j *C.sd_journal, pfd [2]C.int, cursor string) {
|
||||
go func() {
|
||||
// Keep copying journal data out until we're notified to stop.
|
||||
for C.wait_for_data_or_close(j, pfd[0]) == 1 {
|
||||
cursor = s.drainJournal(logWatcher, config, j, cursor)
|
||||
}
|
||||
// Clean up.
|
||||
C.close(pfd[0])
|
||||
s.readers.mu.Lock()
|
||||
delete(s.readers.readers, logWatcher)
|
||||
s.readers.mu.Unlock()
|
||||
}()
|
||||
s.readers.mu.Lock()
|
||||
s.readers.readers[logWatcher] = logWatcher
|
||||
s.readers.mu.Unlock()
|
||||
// Wait until we're told to stop.
|
||||
select {
|
||||
case <-logWatcher.WatchClose():
|
||||
// Notify the other goroutine that its work is done.
|
||||
C.close(pfd[1])
|
||||
}
|
||||
}
|
||||
|
||||
func (s *journald) readLogs(logWatcher *logger.LogWatcher, config logger.ReadConfig) {
|
||||
var j *C.sd_journal
|
||||
var cmatch *C.char
|
||||
var stamp C.uint64_t
|
||||
var sinceUnixMicro uint64
|
||||
var pipes [2]C.int
|
||||
cursor := ""
|
||||
|
||||
defer close(logWatcher.Msg)
|
||||
// Get a handle to the journal.
|
||||
rc := C.sd_journal_open(&j, C.int(0))
|
||||
if rc != 0 {
|
||||
logWatcher.Err <- fmt.Errorf("error opening journal")
|
||||
return
|
||||
}
|
||||
defer C.sd_journal_close(j)
|
||||
// Remove limits on the size of data items that we'll retrieve.
|
||||
rc = C.sd_journal_set_data_threshold(j, C.size_t(0))
|
||||
if rc != 0 {
|
||||
logWatcher.Err <- fmt.Errorf("error setting journal data threshold")
|
||||
return
|
||||
}
|
||||
// Add a match to have the library do the searching for us.
|
||||
cmatch = C.CString("CONTAINER_ID_FULL=" + s.vars["CONTAINER_ID_FULL"])
|
||||
defer C.free(unsafe.Pointer(cmatch))
|
||||
rc = C.sd_journal_add_match(j, unsafe.Pointer(cmatch), C.strlen(cmatch))
|
||||
if rc != 0 {
|
||||
logWatcher.Err <- fmt.Errorf("error setting journal match")
|
||||
return
|
||||
}
|
||||
// If we have a cutoff time, convert it to Unix time once.
|
||||
if !config.Since.IsZero() {
|
||||
nano := config.Since.UnixNano()
|
||||
sinceUnixMicro = uint64(nano / 1000)
|
||||
}
|
||||
if config.Tail > 0 {
|
||||
lines := config.Tail
|
||||
// Start at the end of the journal.
|
||||
if C.sd_journal_seek_tail(j) < 0 {
|
||||
logWatcher.Err <- fmt.Errorf("error seeking to end of journal")
|
||||
return
|
||||
}
|
||||
if C.sd_journal_previous(j) < 0 {
|
||||
logWatcher.Err <- fmt.Errorf("error backtracking to previous journal entry")
|
||||
return
|
||||
}
|
||||
// Walk backward.
|
||||
for lines > 0 {
|
||||
// Stop if the entry time is before our cutoff.
|
||||
// We'll need the entry time if it isn't, so go
|
||||
// ahead and parse it now.
|
||||
if C.sd_journal_get_realtime_usec(j, &stamp) != 0 {
|
||||
break
|
||||
} else {
|
||||
// Compare the timestamp on the entry
|
||||
// to our threshold value.
|
||||
if sinceUnixMicro != 0 && sinceUnixMicro > uint64(stamp) {
|
||||
break
|
||||
}
|
||||
}
|
||||
lines--
|
||||
// If we're at the start of the journal, or
|
||||
// don't need to back up past any more entries,
|
||||
// stop.
|
||||
if lines == 0 || C.sd_journal_previous(j) <= 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Start at the beginning of the journal.
|
||||
if C.sd_journal_seek_head(j) < 0 {
|
||||
logWatcher.Err <- fmt.Errorf("error seeking to start of journal")
|
||||
return
|
||||
}
|
||||
// If we have a cutoff date, fast-forward to it.
|
||||
if sinceUnixMicro != 0 && C.sd_journal_seek_realtime_usec(j, C.uint64_t(sinceUnixMicro)) != 0 {
|
||||
logWatcher.Err <- fmt.Errorf("error seeking to start time in journal")
|
||||
return
|
||||
}
|
||||
if C.sd_journal_next(j) < 0 {
|
||||
logWatcher.Err <- fmt.Errorf("error skipping to next journal entry")
|
||||
return
|
||||
}
|
||||
}
|
||||
cursor = s.drainJournal(logWatcher, config, j, "")
|
||||
if config.Follow {
|
||||
// Create a pipe that we can poll at the same time as the journald descriptor.
|
||||
if C.pipe(&pipes[0]) == C.int(-1) {
|
||||
logWatcher.Err <- fmt.Errorf("error opening journald close notification pipe")
|
||||
} else {
|
||||
s.followJournal(logWatcher, config, j, pipes, cursor)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *journald) ReadLogs(config logger.ReadConfig) *logger.LogWatcher {
|
||||
logWatcher := logger.NewLogWatcher()
|
||||
go s.readLogs(logWatcher, config)
|
||||
return logWatcher
|
||||
}
|
||||
7
vendor/github.com/hyperhq/hypercli/daemon/logger/journald/read_unsupported.go
generated
vendored
Normal file
7
vendor/github.com/hyperhq/hypercli/daemon/logger/journald/read_unsupported.go
generated
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
// +build !linux !cgo static_build !journald
|
||||
|
||||
package journald
|
||||
|
||||
func (s *journald) Close() error {
|
||||
return nil
|
||||
}
|
||||
147
vendor/github.com/hyperhq/hypercli/daemon/logger/jsonfilelog/jsonfilelog.go
generated
vendored
Normal file
147
vendor/github.com/hyperhq/hypercli/daemon/logger/jsonfilelog/jsonfilelog.go
generated
vendored
Normal file
@@ -0,0 +1,147 @@
|
||||
// Package jsonfilelog provides the default Logger implementation for
|
||||
// Docker logging. This logger logs to files on the host server in the
|
||||
// JSON format.
|
||||
package jsonfilelog
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/hyperhq/hypercli/daemon/logger"
|
||||
"github.com/hyperhq/hypercli/daemon/logger/loggerutils"
|
||||
"github.com/hyperhq/hypercli/pkg/jsonlog"
|
||||
"github.com/docker/go-units"
|
||||
)
|
||||
|
||||
// Name is the name of the file that the jsonlogger logs to.
|
||||
const Name = "json-file"
|
||||
|
||||
// JSONFileLogger is Logger implementation for default Docker logging.
|
||||
type JSONFileLogger struct {
|
||||
buf *bytes.Buffer
|
||||
writer *loggerutils.RotateFileWriter
|
||||
mu sync.Mutex
|
||||
ctx logger.Context
|
||||
readers map[*logger.LogWatcher]struct{} // stores the active log followers
|
||||
extra []byte // json-encoded extra attributes
|
||||
}
|
||||
|
||||
func init() {
|
||||
if err := logger.RegisterLogDriver(Name, New); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
if err := logger.RegisterLogOptValidator(Name, ValidateLogOpt); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// New creates new JSONFileLogger which writes to filename passed in
|
||||
// on given context.
|
||||
func New(ctx logger.Context) (logger.Logger, error) {
|
||||
var capval int64 = -1
|
||||
if capacity, ok := ctx.Config["max-size"]; ok {
|
||||
var err error
|
||||
capval, err = units.FromHumanSize(capacity)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
var maxFiles = 1
|
||||
if maxFileString, ok := ctx.Config["max-file"]; ok {
|
||||
var err error
|
||||
maxFiles, err = strconv.Atoi(maxFileString)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if maxFiles < 1 {
|
||||
return nil, fmt.Errorf("max-file cannot be less than 1")
|
||||
}
|
||||
}
|
||||
|
||||
writer, err := loggerutils.NewRotateFileWriter(ctx.LogPath, capval, maxFiles)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var extra []byte
|
||||
if attrs := ctx.ExtraAttributes(nil); len(attrs) > 0 {
|
||||
var err error
|
||||
extra, err = json.Marshal(attrs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return &JSONFileLogger{
|
||||
buf: bytes.NewBuffer(nil),
|
||||
writer: writer,
|
||||
readers: make(map[*logger.LogWatcher]struct{}),
|
||||
extra: extra,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Log converts logger.Message to jsonlog.JSONLog and serializes it to file.
|
||||
func (l *JSONFileLogger) Log(msg *logger.Message) error {
|
||||
timestamp, err := jsonlog.FastTimeMarshalJSON(msg.Timestamp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
err = (&jsonlog.JSONLogs{
|
||||
Log: append(msg.Line, '\n'),
|
||||
Stream: msg.Source,
|
||||
Created: timestamp,
|
||||
RawAttrs: l.extra,
|
||||
}).MarshalJSONBuf(l.buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
l.buf.WriteByte('\n')
|
||||
_, err = l.writer.Write(l.buf.Bytes())
|
||||
l.buf.Reset()
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// ValidateLogOpt looks for json specific log options max-file & max-size.
|
||||
func ValidateLogOpt(cfg map[string]string) error {
|
||||
for key := range cfg {
|
||||
switch key {
|
||||
case "max-file":
|
||||
case "max-size":
|
||||
case "labels":
|
||||
case "env":
|
||||
default:
|
||||
return fmt.Errorf("unknown log opt '%s' for json-file log driver", key)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// LogPath returns the location the given json logger logs to.
|
||||
func (l *JSONFileLogger) LogPath() string {
|
||||
return l.writer.LogPath()
|
||||
}
|
||||
|
||||
// Close closes underlying file and signals all readers to stop.
|
||||
func (l *JSONFileLogger) Close() error {
|
||||
l.mu.Lock()
|
||||
err := l.writer.Close()
|
||||
for r := range l.readers {
|
||||
r.Close()
|
||||
delete(l.readers, r)
|
||||
}
|
||||
l.mu.Unlock()
|
||||
return err
|
||||
}
|
||||
|
||||
// Name returns name of this logger.
|
||||
func (l *JSONFileLogger) Name() string {
|
||||
return Name
|
||||
}
|
||||
201
vendor/github.com/hyperhq/hypercli/daemon/logger/jsonfilelog/jsonfilelog_test.go
generated
vendored
Normal file
201
vendor/github.com/hyperhq/hypercli/daemon/logger/jsonfilelog/jsonfilelog_test.go
generated
vendored
Normal file
@@ -0,0 +1,201 @@
|
||||
package jsonfilelog
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/hyperhq/hypercli/daemon/logger"
|
||||
"github.com/hyperhq/hypercli/pkg/jsonlog"
|
||||
)
|
||||
|
||||
func TestJSONFileLogger(t *testing.T) {
|
||||
cid := "a7317399f3f857173c6179d44823594f8294678dea9999662e5c625b5a1c7657"
|
||||
tmp, err := ioutil.TempDir("", "docker-logger-")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tmp)
|
||||
filename := filepath.Join(tmp, "container.log")
|
||||
l, err := New(logger.Context{
|
||||
ContainerID: cid,
|
||||
LogPath: filename,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer l.Close()
|
||||
|
||||
if err := l.Log(&logger.Message{ContainerID: cid, Line: []byte("line1"), Source: "src1"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := l.Log(&logger.Message{ContainerID: cid, Line: []byte("line2"), Source: "src2"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := l.Log(&logger.Message{ContainerID: cid, Line: []byte("line3"), Source: "src3"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
res, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
expected := `{"log":"line1\n","stream":"src1","time":"0001-01-01T00:00:00Z"}
|
||||
{"log":"line2\n","stream":"src2","time":"0001-01-01T00:00:00Z"}
|
||||
{"log":"line3\n","stream":"src3","time":"0001-01-01T00:00:00Z"}
|
||||
`
|
||||
|
||||
if string(res) != expected {
|
||||
t.Fatalf("Wrong log content: %q, expected %q", res, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkJSONFileLogger(b *testing.B) {
|
||||
cid := "a7317399f3f857173c6179d44823594f8294678dea9999662e5c625b5a1c7657"
|
||||
tmp, err := ioutil.TempDir("", "docker-logger-")
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tmp)
|
||||
filename := filepath.Join(tmp, "container.log")
|
||||
l, err := New(logger.Context{
|
||||
ContainerID: cid,
|
||||
LogPath: filename,
|
||||
})
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
defer l.Close()
|
||||
|
||||
testLine := "Line that thinks that it is log line from docker\n"
|
||||
msg := &logger.Message{ContainerID: cid, Line: []byte(testLine), Source: "stderr", Timestamp: time.Now().UTC()}
|
||||
jsonlog, err := (&jsonlog.JSONLog{Log: string(msg.Line) + "\n", Stream: msg.Source, Created: msg.Timestamp}).MarshalJSON()
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
b.SetBytes(int64(len(jsonlog)+1) * 30)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
for j := 0; j < 30; j++ {
|
||||
if err := l.Log(msg); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestJSONFileLoggerWithOpts(t *testing.T) {
|
||||
cid := "a7317399f3f857173c6179d44823594f8294678dea9999662e5c625b5a1c7657"
|
||||
tmp, err := ioutil.TempDir("", "docker-logger-")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tmp)
|
||||
filename := filepath.Join(tmp, "container.log")
|
||||
config := map[string]string{"max-file": "2", "max-size": "1k"}
|
||||
l, err := New(logger.Context{
|
||||
ContainerID: cid,
|
||||
LogPath: filename,
|
||||
Config: config,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer l.Close()
|
||||
for i := 0; i < 20; i++ {
|
||||
if err := l.Log(&logger.Message{ContainerID: cid, Line: []byte("line" + strconv.Itoa(i)), Source: "src1"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
res, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
penUlt, err := ioutil.ReadFile(filename + ".1")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
expectedPenultimate := `{"log":"line0\n","stream":"src1","time":"0001-01-01T00:00:00Z"}
|
||||
{"log":"line1\n","stream":"src1","time":"0001-01-01T00:00:00Z"}
|
||||
{"log":"line2\n","stream":"src1","time":"0001-01-01T00:00:00Z"}
|
||||
{"log":"line3\n","stream":"src1","time":"0001-01-01T00:00:00Z"}
|
||||
{"log":"line4\n","stream":"src1","time":"0001-01-01T00:00:00Z"}
|
||||
{"log":"line5\n","stream":"src1","time":"0001-01-01T00:00:00Z"}
|
||||
{"log":"line6\n","stream":"src1","time":"0001-01-01T00:00:00Z"}
|
||||
{"log":"line7\n","stream":"src1","time":"0001-01-01T00:00:00Z"}
|
||||
{"log":"line8\n","stream":"src1","time":"0001-01-01T00:00:00Z"}
|
||||
{"log":"line9\n","stream":"src1","time":"0001-01-01T00:00:00Z"}
|
||||
{"log":"line10\n","stream":"src1","time":"0001-01-01T00:00:00Z"}
|
||||
{"log":"line11\n","stream":"src1","time":"0001-01-01T00:00:00Z"}
|
||||
{"log":"line12\n","stream":"src1","time":"0001-01-01T00:00:00Z"}
|
||||
{"log":"line13\n","stream":"src1","time":"0001-01-01T00:00:00Z"}
|
||||
{"log":"line14\n","stream":"src1","time":"0001-01-01T00:00:00Z"}
|
||||
{"log":"line15\n","stream":"src1","time":"0001-01-01T00:00:00Z"}
|
||||
`
|
||||
expected := `{"log":"line16\n","stream":"src1","time":"0001-01-01T00:00:00Z"}
|
||||
{"log":"line17\n","stream":"src1","time":"0001-01-01T00:00:00Z"}
|
||||
{"log":"line18\n","stream":"src1","time":"0001-01-01T00:00:00Z"}
|
||||
{"log":"line19\n","stream":"src1","time":"0001-01-01T00:00:00Z"}
|
||||
`
|
||||
|
||||
if string(res) != expected {
|
||||
t.Fatalf("Wrong log content: %q, expected %q", res, expected)
|
||||
}
|
||||
if string(penUlt) != expectedPenultimate {
|
||||
t.Fatalf("Wrong log content: %q, expected %q", penUlt, expectedPenultimate)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestJSONFileLoggerWithLabelsEnv(t *testing.T) {
|
||||
cid := "a7317399f3f857173c6179d44823594f8294678dea9999662e5c625b5a1c7657"
|
||||
tmp, err := ioutil.TempDir("", "docker-logger-")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tmp)
|
||||
filename := filepath.Join(tmp, "container.log")
|
||||
config := map[string]string{"labels": "rack,dc", "env": "environ,debug,ssl"}
|
||||
l, err := New(logger.Context{
|
||||
ContainerID: cid,
|
||||
LogPath: filename,
|
||||
Config: config,
|
||||
ContainerLabels: map[string]string{"rack": "101", "dc": "lhr"},
|
||||
ContainerEnv: []string{"environ=production", "debug=false", "port=10001", "ssl=true"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer l.Close()
|
||||
if err := l.Log(&logger.Message{ContainerID: cid, Line: []byte("line"), Source: "src1"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
res, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var jsonLog jsonlog.JSONLogs
|
||||
if err := json.Unmarshal(res, &jsonLog); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
extra := make(map[string]string)
|
||||
if err := json.Unmarshal(jsonLog.RawAttrs, &extra); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
expected := map[string]string{
|
||||
"rack": "101",
|
||||
"dc": "lhr",
|
||||
"environ": "production",
|
||||
"debug": "false",
|
||||
"ssl": "true",
|
||||
}
|
||||
if !reflect.DeepEqual(extra, expected) {
|
||||
t.Fatalf("Wrong log attrs: %q, expected %q", extra, expected)
|
||||
}
|
||||
}
|
||||
216
vendor/github.com/hyperhq/hypercli/daemon/logger/jsonfilelog/read.go
generated
vendored
Normal file
216
vendor/github.com/hyperhq/hypercli/daemon/logger/jsonfilelog/read.go
generated
vendored
Normal file
@@ -0,0 +1,216 @@
|
||||
package jsonfilelog
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/hyperhq/hypercli/daemon/logger"
|
||||
"github.com/hyperhq/hypercli/pkg/filenotify"
|
||||
"github.com/hyperhq/hypercli/pkg/ioutils"
|
||||
"github.com/hyperhq/hypercli/pkg/jsonlog"
|
||||
"github.com/hyperhq/hypercli/pkg/tailfile"
|
||||
)
|
||||
|
||||
const maxJSONDecodeRetry = 20000
|
||||
|
||||
func decodeLogLine(dec *json.Decoder, l *jsonlog.JSONLog) (*logger.Message, error) {
|
||||
l.Reset()
|
||||
if err := dec.Decode(l); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
msg := &logger.Message{
|
||||
Source: l.Stream,
|
||||
Timestamp: l.Created,
|
||||
Line: []byte(l.Log),
|
||||
}
|
||||
return msg, nil
|
||||
}
|
||||
|
||||
// ReadLogs implements the logger's LogReader interface for the logs
|
||||
// created by this driver.
|
||||
func (l *JSONFileLogger) ReadLogs(config logger.ReadConfig) *logger.LogWatcher {
|
||||
logWatcher := logger.NewLogWatcher()
|
||||
|
||||
go l.readLogs(logWatcher, config)
|
||||
return logWatcher
|
||||
}
|
||||
|
||||
func (l *JSONFileLogger) readLogs(logWatcher *logger.LogWatcher, config logger.ReadConfig) {
|
||||
defer close(logWatcher.Msg)
|
||||
|
||||
pth := l.writer.LogPath()
|
||||
var files []io.ReadSeeker
|
||||
for i := l.writer.MaxFiles(); i > 1; i-- {
|
||||
f, err := os.Open(fmt.Sprintf("%s.%d", pth, i-1))
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
logWatcher.Err <- err
|
||||
break
|
||||
}
|
||||
continue
|
||||
}
|
||||
defer f.Close()
|
||||
files = append(files, f)
|
||||
}
|
||||
|
||||
latestFile, err := os.Open(pth)
|
||||
if err != nil {
|
||||
logWatcher.Err <- err
|
||||
return
|
||||
}
|
||||
defer latestFile.Close()
|
||||
|
||||
files = append(files, latestFile)
|
||||
tailer := ioutils.MultiReadSeeker(files...)
|
||||
|
||||
if config.Tail != 0 {
|
||||
tailFile(tailer, logWatcher, config.Tail, config.Since)
|
||||
}
|
||||
|
||||
if !config.Follow {
|
||||
return
|
||||
}
|
||||
|
||||
if config.Tail >= 0 {
|
||||
latestFile.Seek(0, os.SEEK_END)
|
||||
}
|
||||
|
||||
l.mu.Lock()
|
||||
l.readers[logWatcher] = struct{}{}
|
||||
l.mu.Unlock()
|
||||
|
||||
notifyRotate := l.writer.NotifyRotate()
|
||||
followLogs(latestFile, logWatcher, notifyRotate, config.Since)
|
||||
|
||||
l.mu.Lock()
|
||||
delete(l.readers, logWatcher)
|
||||
l.mu.Unlock()
|
||||
|
||||
l.writer.NotifyRotateEvict(notifyRotate)
|
||||
}
|
||||
|
||||
func tailFile(f io.ReadSeeker, logWatcher *logger.LogWatcher, tail int, since time.Time) {
|
||||
var rdr io.Reader = f
|
||||
if tail > 0 {
|
||||
ls, err := tailfile.TailFile(f, tail)
|
||||
if err != nil {
|
||||
logWatcher.Err <- err
|
||||
return
|
||||
}
|
||||
rdr = bytes.NewBuffer(bytes.Join(ls, []byte("\n")))
|
||||
}
|
||||
dec := json.NewDecoder(rdr)
|
||||
l := &jsonlog.JSONLog{}
|
||||
for {
|
||||
msg, err := decodeLogLine(dec, l)
|
||||
if err != nil {
|
||||
if err != io.EOF {
|
||||
logWatcher.Err <- err
|
||||
}
|
||||
return
|
||||
}
|
||||
if !since.IsZero() && msg.Timestamp.Before(since) {
|
||||
continue
|
||||
}
|
||||
logWatcher.Msg <- msg
|
||||
}
|
||||
}
|
||||
|
||||
func followLogs(f *os.File, logWatcher *logger.LogWatcher, notifyRotate chan interface{}, since time.Time) {
|
||||
dec := json.NewDecoder(f)
|
||||
l := &jsonlog.JSONLog{}
|
||||
|
||||
fileWatcher, err := filenotify.New()
|
||||
if err != nil {
|
||||
logWatcher.Err <- err
|
||||
}
|
||||
defer fileWatcher.Close()
|
||||
|
||||
var retries int
|
||||
for {
|
||||
msg, err := decodeLogLine(dec, l)
|
||||
if err != nil {
|
||||
if err != io.EOF {
|
||||
// try again because this shouldn't happen
|
||||
if _, ok := err.(*json.SyntaxError); ok && retries <= maxJSONDecodeRetry {
|
||||
dec = json.NewDecoder(f)
|
||||
retries++
|
||||
continue
|
||||
}
|
||||
|
||||
// io.ErrUnexpectedEOF is returned from json.Decoder when there is
|
||||
// remaining data in the parser's buffer while an io.EOF occurs.
|
||||
// If the json logger writes a partial json log entry to the disk
|
||||
// while at the same time the decoder tries to decode it, the race condition happens.
|
||||
if err == io.ErrUnexpectedEOF && retries <= maxJSONDecodeRetry {
|
||||
reader := io.MultiReader(dec.Buffered(), f)
|
||||
dec = json.NewDecoder(reader)
|
||||
retries++
|
||||
continue
|
||||
}
|
||||
logWatcher.Err <- err
|
||||
return
|
||||
}
|
||||
|
||||
logrus.WithField("logger", "json-file").Debugf("waiting for events")
|
||||
if err := fileWatcher.Add(f.Name()); err != nil {
|
||||
logrus.WithField("logger", "json-file").Warn("falling back to file poller")
|
||||
fileWatcher.Close()
|
||||
fileWatcher = filenotify.NewPollingWatcher()
|
||||
if err := fileWatcher.Add(f.Name()); err != nil {
|
||||
logrus.Errorf("error watching log file for modifications: %v", err)
|
||||
logWatcher.Err <- err
|
||||
}
|
||||
}
|
||||
select {
|
||||
case <-fileWatcher.Events():
|
||||
dec = json.NewDecoder(f)
|
||||
fileWatcher.Remove(f.Name())
|
||||
continue
|
||||
case <-fileWatcher.Errors():
|
||||
fileWatcher.Remove(f.Name())
|
||||
logWatcher.Err <- err
|
||||
return
|
||||
case <-logWatcher.WatchClose():
|
||||
fileWatcher.Remove(f.Name())
|
||||
return
|
||||
case <-notifyRotate:
|
||||
f, err = os.Open(f.Name())
|
||||
if err != nil {
|
||||
logWatcher.Err <- err
|
||||
return
|
||||
}
|
||||
|
||||
dec = json.NewDecoder(f)
|
||||
fileWatcher.Remove(f.Name())
|
||||
fileWatcher.Add(f.Name())
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
retries = 0 // reset retries since we've succeeded
|
||||
if !since.IsZero() && msg.Timestamp.Before(since) {
|
||||
continue
|
||||
}
|
||||
select {
|
||||
case logWatcher.Msg <- msg:
|
||||
case <-logWatcher.WatchClose():
|
||||
logWatcher.Msg <- msg
|
||||
for {
|
||||
msg, err := decodeLogLine(dec, l)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if !since.IsZero() && msg.Timestamp.Before(since) {
|
||||
continue
|
||||
}
|
||||
logWatcher.Msg <- msg
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
87
vendor/github.com/hyperhq/hypercli/daemon/logger/logger.go
generated
vendored
Normal file
87
vendor/github.com/hyperhq/hypercli/daemon/logger/logger.go
generated
vendored
Normal file
@@ -0,0 +1,87 @@
|
||||
// Package logger defines interfaces that logger drivers implement to
|
||||
// log messages.
|
||||
//
|
||||
// The other half of a logger driver is the implementation of the
|
||||
// factory, which holds the contextual instance information that
|
||||
// allows multiple loggers of the same type to perform different
|
||||
// actions, such as logging to different locations.
|
||||
package logger
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/hyperhq/hypercli/pkg/jsonlog"
|
||||
)
|
||||
|
||||
// ErrReadLogsNotSupported is returned when the logger does not support reading logs.
|
||||
var ErrReadLogsNotSupported = errors.New("configured logging reader does not support reading")
|
||||
|
||||
const (
|
||||
// TimeFormat is the time format used for timestamps sent to log readers.
|
||||
TimeFormat = jsonlog.RFC3339NanoFixed
|
||||
logWatcherBufferSize = 4096
|
||||
)
|
||||
|
||||
// Message is datastructure that represents record from some container.
|
||||
type Message struct {
|
||||
ContainerID string
|
||||
Line []byte
|
||||
Source string
|
||||
Timestamp time.Time
|
||||
}
|
||||
|
||||
// Logger is the interface for docker logging drivers.
|
||||
type Logger interface {
|
||||
Log(*Message) error
|
||||
Name() string
|
||||
Close() error
|
||||
}
|
||||
|
||||
// ReadConfig is the configuration passed into ReadLogs.
|
||||
type ReadConfig struct {
|
||||
Since time.Time
|
||||
Tail int
|
||||
Follow bool
|
||||
}
|
||||
|
||||
// LogReader is the interface for reading log messages for loggers that support reading.
|
||||
type LogReader interface {
|
||||
// Read logs from underlying logging backend
|
||||
ReadLogs(ReadConfig) *LogWatcher
|
||||
}
|
||||
|
||||
// LogWatcher is used when consuming logs read from the LogReader interface.
|
||||
type LogWatcher struct {
|
||||
// For sending log messages to a reader.
|
||||
Msg chan *Message
|
||||
// For sending error messages that occur while while reading logs.
|
||||
Err chan error
|
||||
closeNotifier chan struct{}
|
||||
}
|
||||
|
||||
// NewLogWatcher returns a new LogWatcher.
|
||||
func NewLogWatcher() *LogWatcher {
|
||||
return &LogWatcher{
|
||||
Msg: make(chan *Message, logWatcherBufferSize),
|
||||
Err: make(chan error, 1),
|
||||
closeNotifier: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
// Close notifies the underlying log reader to stop.
|
||||
func (w *LogWatcher) Close() {
|
||||
// only close if not already closed
|
||||
select {
|
||||
case <-w.closeNotifier:
|
||||
default:
|
||||
close(w.closeNotifier)
|
||||
}
|
||||
}
|
||||
|
||||
// WatchClose returns a channel receiver that receives notification
|
||||
// when the watcher has been closed. This should only be called from
|
||||
// one goroutine.
|
||||
func (w *LogWatcher) WatchClose() <-chan struct{} {
|
||||
return w.closeNotifier
|
||||
}
|
||||
26
vendor/github.com/hyperhq/hypercli/daemon/logger/loggerutils/log_option_helpers.go
generated
vendored
Normal file
26
vendor/github.com/hyperhq/hypercli/daemon/logger/loggerutils/log_option_helpers.go
generated
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
package loggerutils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/hyperhq/hypercli/daemon/logger"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultFailOnStartupError = true // So that we do not break existing behaviour
|
||||
)
|
||||
|
||||
// ParseFailOnStartupErrorFlag parses a log driver flag that determines if
|
||||
// the driver should ignore possible connection errors during startup
|
||||
func ParseFailOnStartupErrorFlag(ctx logger.Context) (bool, error) {
|
||||
failOnStartupError := ctx.Config["fail-on-startup-error"]
|
||||
if failOnStartupError == "" {
|
||||
return defaultFailOnStartupError, nil
|
||||
}
|
||||
failOnStartupErrorFlag, err := strconv.ParseBool(failOnStartupError)
|
||||
if err != nil {
|
||||
return defaultFailOnStartupError, fmt.Errorf("invalid connect error flag %s: %s", failOnStartupError, err)
|
||||
}
|
||||
return failOnStartupErrorFlag, nil
|
||||
}
|
||||
51
vendor/github.com/hyperhq/hypercli/daemon/logger/loggerutils/log_option_helpers_test.go
generated
vendored
Normal file
51
vendor/github.com/hyperhq/hypercli/daemon/logger/loggerutils/log_option_helpers_test.go
generated
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
package loggerutils
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/hyperhq/hypercli/daemon/logger"
|
||||
)
|
||||
|
||||
func TestParseDefaultIgnoreFlag(t *testing.T) {
|
||||
ctx := buildContext(map[string]string{})
|
||||
flag, e := ParseFailOnStartupErrorFlag(ctx)
|
||||
assertFlag(t, e, flag, true)
|
||||
}
|
||||
|
||||
func TestParseIgnoreFlagWhenFalse(t *testing.T) {
|
||||
ctx := buildContext(map[string]string{"fail-on-startup-error": "false"})
|
||||
flag, e := ParseFailOnStartupErrorFlag(ctx)
|
||||
assertFlag(t, e, flag, false)
|
||||
}
|
||||
|
||||
func TestParseIgnoreFlagWhenTrue(t *testing.T) {
|
||||
ctx := buildContext(map[string]string{"fail-on-startup-error": "true"})
|
||||
flag, e := ParseFailOnStartupErrorFlag(ctx)
|
||||
assertFlag(t, e, flag, true)
|
||||
}
|
||||
|
||||
func TestParseIgnoreFlagWithError(t *testing.T) {
|
||||
ctx := buildContext(map[string]string{"fail-on-startup-error": "maybe :)"})
|
||||
flag, e := ParseFailOnStartupErrorFlag(ctx)
|
||||
if e == nil {
|
||||
t.Fatalf("Error should have happened")
|
||||
}
|
||||
assertFlag(t, nil, flag, true)
|
||||
}
|
||||
|
||||
// Helpers
|
||||
|
||||
func buildConfig(cfg map[string]string) logger.Context {
|
||||
return logger.Context{
|
||||
Config: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
func assertFlag(t *testing.T, e error, flag bool, expected bool) {
|
||||
if e != nil {
|
||||
t.Fatalf("Error parsing ignore connect error flag: %q", e)
|
||||
}
|
||||
if flag != expected {
|
||||
t.Fatalf("Wrong flag: %t, should be %t", flag, expected)
|
||||
}
|
||||
}
|
||||
46
vendor/github.com/hyperhq/hypercli/daemon/logger/loggerutils/log_tag.go
generated
vendored
Normal file
46
vendor/github.com/hyperhq/hypercli/daemon/logger/loggerutils/log_tag.go
generated
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
package loggerutils
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"text/template"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/hyperhq/hypercli/daemon/logger"
|
||||
)
|
||||
|
||||
// ParseLogTag generates a context aware tag for consistency across different
|
||||
// log drivers based on the context of the running container.
|
||||
func ParseLogTag(ctx logger.Context, defaultTemplate string) (string, error) {
|
||||
tagTemplate := lookupTagTemplate(ctx, defaultTemplate)
|
||||
|
||||
tmpl, err := template.New("log-tag").Parse(tagTemplate)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
buf := new(bytes.Buffer)
|
||||
if err := tmpl.Execute(buf, &ctx); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
func lookupTagTemplate(ctx logger.Context, defaultTemplate string) string {
|
||||
tagTemplate := ctx.Config["tag"]
|
||||
|
||||
deprecatedConfigs := []string{"syslog-tag", "gelf-tag", "fluentd-tag"}
|
||||
for i := 0; tagTemplate == "" && i < len(deprecatedConfigs); i++ {
|
||||
cfg := deprecatedConfigs[i]
|
||||
if ctx.Config[cfg] != "" {
|
||||
tagTemplate = ctx.Config[cfg]
|
||||
logrus.Warn(fmt.Sprintf("Using log tag from deprecated log-opt '%s'. Please use: --log-opt tag=\"%s\"", cfg, tagTemplate))
|
||||
}
|
||||
}
|
||||
|
||||
if tagTemplate == "" {
|
||||
tagTemplate = defaultTemplate
|
||||
}
|
||||
|
||||
return tagTemplate
|
||||
}
|
||||
58
vendor/github.com/hyperhq/hypercli/daemon/logger/loggerutils/log_tag_test.go
generated
vendored
Normal file
58
vendor/github.com/hyperhq/hypercli/daemon/logger/loggerutils/log_tag_test.go
generated
vendored
Normal file
@@ -0,0 +1,58 @@
|
||||
package loggerutils
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/hyperhq/hypercli/daemon/logger"
|
||||
)
|
||||
|
||||
func TestParseLogTagDefaultTag(t *testing.T) {
|
||||
ctx := buildContext(map[string]string{})
|
||||
tag, e := ParseLogTag(ctx, "{{.ID}}")
|
||||
assertTag(t, e, tag, ctx.ID())
|
||||
}
|
||||
|
||||
func TestParseLogTag(t *testing.T) {
|
||||
ctx := buildContext(map[string]string{"tag": "{{.ImageName}}/{{.Name}}/{{.ID}}"})
|
||||
tag, e := ParseLogTag(ctx, "{{.ID}}")
|
||||
assertTag(t, e, tag, "test-image/test-container/container-ab")
|
||||
}
|
||||
|
||||
func TestParseLogTagSyslogTag(t *testing.T) {
|
||||
ctx := buildContext(map[string]string{"syslog-tag": "{{.ImageName}}/{{.Name}}/{{.ID}}"})
|
||||
tag, e := ParseLogTag(ctx, "{{.ID}}")
|
||||
assertTag(t, e, tag, "test-image/test-container/container-ab")
|
||||
}
|
||||
|
||||
func TestParseLogTagGelfTag(t *testing.T) {
|
||||
ctx := buildContext(map[string]string{"gelf-tag": "{{.ImageName}}/{{.Name}}/{{.ID}}"})
|
||||
tag, e := ParseLogTag(ctx, "{{.ID}}")
|
||||
assertTag(t, e, tag, "test-image/test-container/container-ab")
|
||||
}
|
||||
|
||||
func TestParseLogTagFluentdTag(t *testing.T) {
|
||||
ctx := buildContext(map[string]string{"fluentd-tag": "{{.ImageName}}/{{.Name}}/{{.ID}}"})
|
||||
tag, e := ParseLogTag(ctx, "{{.ID}}")
|
||||
assertTag(t, e, tag, "test-image/test-container/container-ab")
|
||||
}
|
||||
|
||||
// Helpers
|
||||
|
||||
func buildContext(cfg map[string]string) logger.Context {
|
||||
return logger.Context{
|
||||
ContainerID: "container-abcdefghijklmnopqrstuvwxyz01234567890",
|
||||
ContainerName: "/test-container",
|
||||
ContainerImageID: "image-abcdefghijklmnopqrstuvwxyz01234567890",
|
||||
ContainerImageName: "test-image",
|
||||
Config: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
func assertTag(t *testing.T, e error, tag string, expected string) {
|
||||
if e != nil {
|
||||
t.Fatalf("Error generating tag: %q", e)
|
||||
}
|
||||
if tag != expected {
|
||||
t.Fatalf("Wrong tag: %q, should be %q", tag, expected)
|
||||
}
|
||||
}
|
||||
132
vendor/github.com/hyperhq/hypercli/daemon/logger/loggerutils/rotatefilewriter.go
generated
vendored
Normal file
132
vendor/github.com/hyperhq/hypercli/daemon/logger/loggerutils/rotatefilewriter.go
generated
vendored
Normal file
@@ -0,0 +1,132 @@
|
||||
package loggerutils
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
"github.com/hyperhq/hypercli/pkg/pubsub"
|
||||
)
|
||||
|
||||
// RotateFileWriter is Logger implementation for default Docker logging.
|
||||
type RotateFileWriter struct {
|
||||
f *os.File // store for closing
|
||||
mu sync.Mutex
|
||||
capacity int64 //maximum size of each file
|
||||
maxFiles int //maximum number of files
|
||||
notifyRotate *pubsub.Publisher
|
||||
}
|
||||
|
||||
//NewRotateFileWriter creates new RotateFileWriter
|
||||
func NewRotateFileWriter(logPath string, capacity int64, maxFiles int) (*RotateFileWriter, error) {
|
||||
log, err := os.OpenFile(logPath, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0640)
|
||||
if err != nil {
|
||||
return &RotateFileWriter{}, err
|
||||
}
|
||||
|
||||
return &RotateFileWriter{
|
||||
f: log,
|
||||
capacity: capacity,
|
||||
maxFiles: maxFiles,
|
||||
notifyRotate: pubsub.NewPublisher(0, 1),
|
||||
}, nil
|
||||
}
|
||||
|
||||
//WriteLog write log message to File
|
||||
func (w *RotateFileWriter) Write(message []byte) (int, error) {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
if err := w.checkCapacityAndRotate(); err != nil {
|
||||
return -1, err
|
||||
}
|
||||
|
||||
return w.f.Write(message)
|
||||
}
|
||||
|
||||
func (w *RotateFileWriter) checkCapacityAndRotate() error {
|
||||
if w.capacity == -1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
meta, err := w.f.Stat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if meta.Size() >= w.capacity {
|
||||
name := w.f.Name()
|
||||
if err := w.f.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := rotate(name, w.maxFiles); err != nil {
|
||||
return err
|
||||
}
|
||||
file, err := os.OpenFile(name, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 06400)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
w.f = file
|
||||
w.notifyRotate.Publish(struct{}{})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func rotate(name string, maxFiles int) error {
|
||||
if maxFiles < 2 {
|
||||
return nil
|
||||
}
|
||||
for i := maxFiles - 1; i > 1; i-- {
|
||||
toPath := name + "." + strconv.Itoa(i)
|
||||
fromPath := name + "." + strconv.Itoa(i-1)
|
||||
if err := backup(fromPath, toPath); err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := backup(name, name+".1"); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// backup renames a file from fromPath to toPath
|
||||
func backup(fromPath, toPath string) error {
|
||||
if _, err := os.Stat(fromPath); os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := os.Stat(toPath); !os.IsNotExist(err) {
|
||||
err := os.Remove(toPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return os.Rename(fromPath, toPath)
|
||||
}
|
||||
|
||||
// LogPath returns the location the given writer logs to.
|
||||
func (w *RotateFileWriter) LogPath() string {
|
||||
return w.f.Name()
|
||||
}
|
||||
|
||||
// MaxFiles return maximum number of files
|
||||
func (w *RotateFileWriter) MaxFiles() int {
|
||||
return w.maxFiles
|
||||
}
|
||||
|
||||
//NotifyRotate returns the new subscriber
|
||||
func (w *RotateFileWriter) NotifyRotate() chan interface{} {
|
||||
return w.notifyRotate.Subscribe()
|
||||
}
|
||||
|
||||
//NotifyRotateEvict removes the specified subscriber from receiving any more messages.
|
||||
func (w *RotateFileWriter) NotifyRotateEvict(sub chan interface{}) {
|
||||
w.notifyRotate.Evict(sub)
|
||||
}
|
||||
|
||||
// Close closes underlying file and signals all readers to stop.
|
||||
func (w *RotateFileWriter) Close() error {
|
||||
return w.f.Close()
|
||||
}
|
||||
268
vendor/github.com/hyperhq/hypercli/daemon/logger/splunk/splunk.go
generated
vendored
Normal file
268
vendor/github.com/hyperhq/hypercli/daemon/logger/splunk/splunk.go
generated
vendored
Normal file
@@ -0,0 +1,268 @@
|
||||
// Package splunk provides the log driver for forwarding server logs to
|
||||
// Splunk HTTP Event Collector endpoint.
|
||||
package splunk
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/hyperhq/hypercli/daemon/logger"
|
||||
"github.com/hyperhq/hypercli/daemon/logger/loggerutils"
|
||||
"github.com/hyperhq/hypercli/pkg/urlutil"
|
||||
)
|
||||
|
||||
const (
|
||||
driverName = "splunk"
|
||||
splunkURLKey = "splunk-url"
|
||||
splunkTokenKey = "splunk-token"
|
||||
splunkSourceKey = "splunk-source"
|
||||
splunkSourceTypeKey = "splunk-sourcetype"
|
||||
splunkIndexKey = "splunk-index"
|
||||
splunkCAPathKey = "splunk-capath"
|
||||
splunkCANameKey = "splunk-caname"
|
||||
splunkInsecureSkipVerifyKey = "splunk-insecureskipverify"
|
||||
envKey = "env"
|
||||
labelsKey = "labels"
|
||||
tagKey = "tag"
|
||||
)
|
||||
|
||||
type splunkLogger struct {
|
||||
client *http.Client
|
||||
transport *http.Transport
|
||||
|
||||
url string
|
||||
auth string
|
||||
nullMessage *splunkMessage
|
||||
}
|
||||
|
||||
type splunkMessage struct {
|
||||
Event splunkMessageEvent `json:"event"`
|
||||
Time string `json:"time"`
|
||||
Host string `json:"host"`
|
||||
Source string `json:"source,omitempty"`
|
||||
SourceType string `json:"sourcetype,omitempty"`
|
||||
Index string `json:"index,omitempty"`
|
||||
}
|
||||
|
||||
type splunkMessageEvent struct {
|
||||
Line string `json:"line"`
|
||||
Source string `json:"source"`
|
||||
Tag string `json:"tag,omitempty"`
|
||||
Attrs map[string]string `json:"attrs,omitempty"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
if err := logger.RegisterLogDriver(driverName, New); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
if err := logger.RegisterLogOptValidator(driverName, ValidateLogOpt); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// New creates splunk logger driver using configuration passed in context
|
||||
func New(ctx logger.Context) (logger.Logger, error) {
|
||||
hostname, err := ctx.Hostname()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: cannot access hostname to set source field", driverName)
|
||||
}
|
||||
|
||||
// Parse and validate Splunk URL
|
||||
splunkURL, err := parseURL(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Splunk Token is required parameter
|
||||
splunkToken, ok := ctx.Config[splunkTokenKey]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%s: %s is expected", driverName, splunkTokenKey)
|
||||
}
|
||||
|
||||
tlsConfig := &tls.Config{}
|
||||
|
||||
// Splunk is using autogenerated certificates by default,
|
||||
// allow users to trust them with skipping verification
|
||||
if insecureSkipVerifyStr, ok := ctx.Config[splunkInsecureSkipVerifyKey]; ok {
|
||||
insecureSkipVerify, err := strconv.ParseBool(insecureSkipVerifyStr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tlsConfig.InsecureSkipVerify = insecureSkipVerify
|
||||
}
|
||||
|
||||
// If path to the root certificate is provided - load it
|
||||
if caPath, ok := ctx.Config[splunkCAPathKey]; ok {
|
||||
caCert, err := ioutil.ReadFile(caPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
caPool := x509.NewCertPool()
|
||||
caPool.AppendCertsFromPEM(caCert)
|
||||
tlsConfig.RootCAs = caPool
|
||||
}
|
||||
|
||||
if caName, ok := ctx.Config[splunkCANameKey]; ok {
|
||||
tlsConfig.ServerName = caName
|
||||
}
|
||||
|
||||
transport := &http.Transport{
|
||||
TLSClientConfig: tlsConfig,
|
||||
}
|
||||
client := &http.Client{
|
||||
Transport: transport,
|
||||
}
|
||||
|
||||
var nullMessage = &splunkMessage{
|
||||
Host: hostname,
|
||||
}
|
||||
|
||||
// Optional parameters for messages
|
||||
nullMessage.Source = ctx.Config[splunkSourceKey]
|
||||
nullMessage.SourceType = ctx.Config[splunkSourceTypeKey]
|
||||
nullMessage.Index = ctx.Config[splunkIndexKey]
|
||||
|
||||
tag, err := loggerutils.ParseLogTag(ctx, "{{.ID}}")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
nullMessage.Event.Tag = tag
|
||||
nullMessage.Event.Attrs = ctx.ExtraAttributes(nil)
|
||||
|
||||
logger := &splunkLogger{
|
||||
client: client,
|
||||
transport: transport,
|
||||
url: splunkURL.String(),
|
||||
auth: "Splunk " + splunkToken,
|
||||
nullMessage: nullMessage,
|
||||
}
|
||||
|
||||
err = verifySplunkConnection(logger)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return logger, nil
|
||||
}
|
||||
|
||||
func (l *splunkLogger) Log(msg *logger.Message) error {
|
||||
// Construct message as a copy of nullMessage
|
||||
message := *l.nullMessage
|
||||
message.Time = fmt.Sprintf("%f", float64(msg.Timestamp.UnixNano())/1000000000)
|
||||
message.Event.Line = string(msg.Line)
|
||||
message.Event.Source = msg.Source
|
||||
|
||||
jsonEvent, err := json.Marshal(&message)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req, err := http.NewRequest("POST", l.url, bytes.NewBuffer(jsonEvent))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Authorization", l.auth)
|
||||
res, err := l.client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if res.Body != nil {
|
||||
defer res.Body.Close()
|
||||
}
|
||||
if res.StatusCode != http.StatusOK {
|
||||
var body []byte
|
||||
body, err = ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return fmt.Errorf("%s: failed to send event - %s - %s", driverName, res.Status, body)
|
||||
}
|
||||
io.Copy(ioutil.Discard, res.Body)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *splunkLogger) Close() error {
|
||||
l.transport.CloseIdleConnections()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *splunkLogger) Name() string {
|
||||
return driverName
|
||||
}
|
||||
|
||||
// ValidateLogOpt looks for all supported by splunk driver options
|
||||
func ValidateLogOpt(cfg map[string]string) error {
|
||||
for key := range cfg {
|
||||
switch key {
|
||||
case splunkURLKey:
|
||||
case splunkTokenKey:
|
||||
case splunkSourceKey:
|
||||
case splunkSourceTypeKey:
|
||||
case splunkIndexKey:
|
||||
case splunkCAPathKey:
|
||||
case splunkCANameKey:
|
||||
case splunkInsecureSkipVerifyKey:
|
||||
case envKey:
|
||||
case labelsKey:
|
||||
case tagKey:
|
||||
default:
|
||||
return fmt.Errorf("unknown log opt '%s' for %s log driver", key, driverName)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseURL(ctx logger.Context) (*url.URL, error) {
|
||||
splunkURLStr, ok := ctx.Config[splunkURLKey]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%s: %s is expected", driverName, splunkURLKey)
|
||||
}
|
||||
|
||||
splunkURL, err := url.Parse(splunkURLStr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: failed to parse %s as url value in %s", driverName, splunkURLStr, splunkURLKey)
|
||||
}
|
||||
|
||||
if !urlutil.IsURL(splunkURLStr) ||
|
||||
!splunkURL.IsAbs() ||
|
||||
(splunkURL.Path != "" && splunkURL.Path != "/") ||
|
||||
splunkURL.RawQuery != "" ||
|
||||
splunkURL.Fragment != "" {
|
||||
return nil, fmt.Errorf("%s: expected format schema://dns_name_or_ip:port for %s", driverName, splunkURLKey)
|
||||
}
|
||||
|
||||
splunkURL.Path = "/services/collector/event/1.0"
|
||||
|
||||
return splunkURL, nil
|
||||
}
|
||||
|
||||
func verifySplunkConnection(l *splunkLogger) error {
|
||||
req, err := http.NewRequest("OPTIONS", l.url, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
res, err := l.client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if res.Body != nil {
|
||||
defer res.Body.Close()
|
||||
}
|
||||
if res.StatusCode != http.StatusOK {
|
||||
var body []byte
|
||||
body, err = ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return fmt.Errorf("%s: failed to verify connection - %s - %s", driverName, res.Status, body)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
209
vendor/github.com/hyperhq/hypercli/daemon/logger/syslog/syslog.go
generated
vendored
Normal file
209
vendor/github.com/hyperhq/hypercli/daemon/logger/syslog/syslog.go
generated
vendored
Normal file
@@ -0,0 +1,209 @@
|
||||
// +build linux
|
||||
|
||||
// Package syslog provides the logdriver for forwarding server logs to syslog endpoints.
|
||||
package syslog
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
syslog "github.com/RackSec/srslog"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/hyperhq/hypercli/daemon/logger"
|
||||
"github.com/hyperhq/hypercli/daemon/logger/loggerutils"
|
||||
"github.com/hyperhq/hypercli/pkg/urlutil"
|
||||
"github.com/docker/go-connections/tlsconfig"
|
||||
)
|
||||
|
||||
const (
|
||||
name = "syslog"
|
||||
secureProto = "tcp+tls"
|
||||
)
|
||||
|
||||
var facilities = map[string]syslog.Priority{
|
||||
"kern": syslog.LOG_KERN,
|
||||
"user": syslog.LOG_USER,
|
||||
"mail": syslog.LOG_MAIL,
|
||||
"daemon": syslog.LOG_DAEMON,
|
||||
"auth": syslog.LOG_AUTH,
|
||||
"syslog": syslog.LOG_SYSLOG,
|
||||
"lpr": syslog.LOG_LPR,
|
||||
"news": syslog.LOG_NEWS,
|
||||
"uucp": syslog.LOG_UUCP,
|
||||
"cron": syslog.LOG_CRON,
|
||||
"authpriv": syslog.LOG_AUTHPRIV,
|
||||
"ftp": syslog.LOG_FTP,
|
||||
"local0": syslog.LOG_LOCAL0,
|
||||
"local1": syslog.LOG_LOCAL1,
|
||||
"local2": syslog.LOG_LOCAL2,
|
||||
"local3": syslog.LOG_LOCAL3,
|
||||
"local4": syslog.LOG_LOCAL4,
|
||||
"local5": syslog.LOG_LOCAL5,
|
||||
"local6": syslog.LOG_LOCAL6,
|
||||
"local7": syslog.LOG_LOCAL7,
|
||||
}
|
||||
|
||||
type syslogger struct {
|
||||
writer *syslog.Writer
|
||||
}
|
||||
|
||||
func init() {
|
||||
if err := logger.RegisterLogDriver(name, New); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
if err := logger.RegisterLogOptValidator(name, ValidateLogOpt); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// New creates a syslog logger using the configuration passed in on
|
||||
// the context. Supported context configuration variables are
|
||||
// syslog-address, syslog-facility, & syslog-tag.
|
||||
func New(ctx logger.Context) (logger.Logger, error) {
|
||||
tag, err := loggerutils.ParseLogTag(ctx, "{{.ID}}")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
proto, address, err := parseAddress(ctx.Config["syslog-address"])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
facility, err := parseFacility(ctx.Config["syslog-facility"])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
logTag := path.Base(os.Args[0]) + "/" + tag
|
||||
|
||||
var log *syslog.Writer
|
||||
if proto == secureProto {
|
||||
tlsConfig, tlsErr := parseTLSConfig(ctx.Config)
|
||||
if tlsErr != nil {
|
||||
return nil, tlsErr
|
||||
}
|
||||
log, err = syslog.DialWithTLSConfig(proto, address, facility, logTag, tlsConfig)
|
||||
} else {
|
||||
log, err = syslog.Dial(proto, address, facility, logTag)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &syslogger{
|
||||
writer: log,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *syslogger) Log(msg *logger.Message) error {
|
||||
if msg.Source == "stderr" {
|
||||
return s.writer.Err(string(msg.Line))
|
||||
}
|
||||
return s.writer.Info(string(msg.Line))
|
||||
}
|
||||
|
||||
func (s *syslogger) Close() error {
|
||||
return s.writer.Close()
|
||||
}
|
||||
|
||||
func (s *syslogger) Name() string {
|
||||
return name
|
||||
}
|
||||
|
||||
func parseAddress(address string) (string, string, error) {
|
||||
if address == "" {
|
||||
return "", "", nil
|
||||
}
|
||||
if !urlutil.IsTransportURL(address) {
|
||||
return "", "", fmt.Errorf("syslog-address should be in form proto://address, got %v", address)
|
||||
}
|
||||
url, err := url.Parse(address)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
// unix socket validation
|
||||
if url.Scheme == "unix" {
|
||||
if _, err := os.Stat(url.Path); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
return url.Scheme, url.Path, nil
|
||||
}
|
||||
|
||||
// here we process tcp|udp
|
||||
host := url.Host
|
||||
if _, _, err := net.SplitHostPort(host); err != nil {
|
||||
if !strings.Contains(err.Error(), "missing port in address") {
|
||||
return "", "", err
|
||||
}
|
||||
host = host + ":514"
|
||||
}
|
||||
|
||||
return url.Scheme, host, nil
|
||||
}
|
||||
|
||||
// ValidateLogOpt looks for syslog specific log options
|
||||
// syslog-address, syslog-facility, & syslog-tag.
|
||||
func ValidateLogOpt(cfg map[string]string) error {
|
||||
for key := range cfg {
|
||||
switch key {
|
||||
case "syslog-address":
|
||||
case "syslog-facility":
|
||||
case "syslog-tag":
|
||||
case "syslog-tls-ca-cert":
|
||||
case "syslog-tls-cert":
|
||||
case "syslog-tls-key":
|
||||
case "syslog-tls-skip-verify":
|
||||
case "tag":
|
||||
default:
|
||||
return fmt.Errorf("unknown log opt '%s' for syslog log driver", key)
|
||||
}
|
||||
}
|
||||
if _, _, err := parseAddress(cfg["syslog-address"]); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := parseFacility(cfg["syslog-facility"]); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseFacility(facility string) (syslog.Priority, error) {
|
||||
if facility == "" {
|
||||
return syslog.LOG_DAEMON, nil
|
||||
}
|
||||
|
||||
if syslogFacility, valid := facilities[facility]; valid {
|
||||
return syslogFacility, nil
|
||||
}
|
||||
|
||||
fInt, err := strconv.Atoi(facility)
|
||||
if err == nil && 0 <= fInt && fInt <= 23 {
|
||||
return syslog.Priority(fInt << 3), nil
|
||||
}
|
||||
|
||||
return syslog.Priority(0), errors.New("invalid syslog facility")
|
||||
}
|
||||
|
||||
func parseTLSConfig(cfg map[string]string) (*tls.Config, error) {
|
||||
_, skipVerify := cfg["syslog-tls-skip-verify"]
|
||||
|
||||
opts := tlsconfig.Options{
|
||||
CAFile: cfg["syslog-tls-ca-cert"],
|
||||
CertFile: cfg["syslog-tls-cert"],
|
||||
KeyFile: cfg["syslog-tls-key"],
|
||||
InsecureSkipVerify: skipVerify,
|
||||
}
|
||||
|
||||
return tlsconfig.Client(opts)
|
||||
}
|
||||
3
vendor/github.com/hyperhq/hypercli/daemon/logger/syslog/syslog_unsupported.go
generated
vendored
Normal file
3
vendor/github.com/hyperhq/hypercli/daemon/logger/syslog/syslog_unsupported.go
generated
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
// +build !linux
|
||||
|
||||
package syslog
|
||||
Reference in New Issue
Block a user