Skip to content

Commit aa26d39

Browse files
Add logfwd.Record.
1 parent 8778cee commit aa26d39

File tree

2 files changed

+289
-0
lines changed

2 files changed

+289
-0
lines changed

logfwd/record.go

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
// Copyright 2016 Canonical Ltd.
2+
// Licensed under the AGPLv3, see LICENCE file for details.
3+
4+
package logfwd
5+
6+
import (
7+
"fmt"
8+
"strconv"
9+
"strings"
10+
"time"
11+
12+
"github.com/juju/errors"
13+
"github.com/juju/loggo"
14+
)
15+
16+
// Record holds all the information for a single log record.
17+
type Record struct {
18+
// Origin describes what created the record.
19+
Origin Origin
20+
21+
// Timestamp is when the record was created.
22+
Timestamp time.Time
23+
24+
// Level is the basic logging level of the record.
25+
Level loggo.Level
26+
27+
// Location describes where the record was created.
28+
Location SourceLocation
29+
30+
// Message is the record's body. It may be empty.
31+
Message string
32+
}
33+
34+
// Validate ensures that the record is correct.
35+
func (rec Record) Validate() error {
36+
if err := rec.Origin.Validate(); err != nil {
37+
return errors.Annotate(err, "invalid Origin")
38+
}
39+
40+
if rec.Timestamp.IsZero() {
41+
return errors.NewNotValid(nil, "empty Timestamp")
42+
}
43+
44+
// rec.Level may be anything, so we don't check it.
45+
46+
if err := rec.Location.Validate(); err != nil {
47+
return errors.Annotate(err, "invalid Location")
48+
}
49+
50+
// rec.Message may be anything, so we don't check it.
51+
52+
return nil
53+
}
54+
55+
// SourceLocation holds all the information about the source code that
56+
// caused the record to be created.
57+
type SourceLocation struct {
58+
// Module is the source "module" (e.g. package) where the record
59+
// originated. This is optional.
60+
Module string
61+
62+
// Filename is the path to the source file. This is required only
63+
// if Line is greater than 0.
64+
Filename string
65+
66+
// Line is the line number in the source. It is optional. A negative
67+
// value means "not set". So does 0 if Filename is not set. If Line
68+
// is greater than 0 then Filename must be set.
69+
Line int
70+
}
71+
72+
// ParseLocation converts the given info into a SourceLocation. The
73+
// caller is responsible for validating the result.
74+
func ParseLocation(module, location string) (SourceLocation, error) {
75+
loc, err := parseLocation(module, location)
76+
if err != nil {
77+
return loc, errors.Annotate(err, "failed to parse location")
78+
}
79+
return loc, nil
80+
}
81+
82+
func parseLocation(module, location string) (SourceLocation, error) {
83+
loc := SourceLocation{
84+
Module: module,
85+
Filename: location,
86+
}
87+
if location != "" {
88+
loc.Line = -1
89+
pos := strings.LastIndex(location, ":")
90+
if pos >= 0 {
91+
loc.Filename = location[:pos]
92+
lineno, err := strconv.Atoi(location[pos+1:])
93+
if err != nil {
94+
return SourceLocation{}, errors.Trace(err)
95+
}
96+
loc.Line = lineno
97+
}
98+
}
99+
return loc, nil
100+
}
101+
102+
// String returns a string representation of the location.
103+
func (loc SourceLocation) String() string {
104+
if loc.Line < 0 {
105+
return loc.Filename
106+
}
107+
if loc.Line == 0 && loc.Filename == "" {
108+
return ""
109+
}
110+
return fmt.Sprintf("%s:%d", loc.Filename, loc.Line)
111+
}
112+
113+
// Validate ensures that the location is correct.
114+
func (loc SourceLocation) Validate() error {
115+
// Module may be anything, so there's nothing to check.
116+
117+
// Filename may be set with no line number set, but not the other
118+
// way around.
119+
if loc.Line > 0 && loc.Filename == "" {
120+
return errors.NewNotValid(nil, "Line set but Filename empty")
121+
}
122+
123+
return nil
124+
}

logfwd/record_test.go

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
// Copyright 2016 Canonical Ltd.
2+
// Licensed under the AGPLv3, see LICENCE file for details.
3+
4+
package logfwd_test
5+
6+
import (
7+
"time"
8+
9+
"github.com/juju/errors"
10+
"github.com/juju/loggo"
11+
"github.com/juju/testing"
12+
jc "github.com/juju/testing/checkers"
13+
gc "gopkg.in/check.v1"
14+
15+
"github.com/juju/juju/logfwd"
16+
)
17+
18+
type RecordSuite struct {
19+
testing.IsolationSuite
20+
}
21+
22+
var _ = gc.Suite(&RecordSuite{})
23+
24+
func (s *RecordSuite) TestValidateValid(c *gc.C) {
25+
rec := validRecord
26+
27+
err := rec.Validate()
28+
29+
c.Check(err, jc.ErrorIsNil)
30+
}
31+
32+
func (s *RecordSuite) TestValidateZero(c *gc.C) {
33+
var rec logfwd.Record
34+
35+
err := rec.Validate()
36+
37+
c.Check(err, jc.Satisfies, errors.IsNotValid)
38+
}
39+
40+
func (s *RecordSuite) TestValidateBadOrigin(c *gc.C) {
41+
rec := validRecord
42+
rec.Origin.Name = "..."
43+
44+
err := rec.Validate()
45+
46+
c.Check(err, jc.Satisfies, errors.IsNotValid)
47+
c.Check(err, gc.ErrorMatches, `invalid Origin: invalid Name "...": bad user`)
48+
}
49+
50+
func (s *RecordSuite) TestValidateEmptyTimestamp(c *gc.C) {
51+
rec := validRecord
52+
rec.Timestamp = time.Time{}
53+
54+
err := rec.Validate()
55+
56+
c.Check(err, jc.Satisfies, errors.IsNotValid)
57+
c.Check(err, gc.ErrorMatches, `empty Timestamp`)
58+
}
59+
60+
func (s *RecordSuite) TestValidateBadLocation(c *gc.C) {
61+
rec := validRecord
62+
rec.Location.Filename = ""
63+
64+
err := rec.Validate()
65+
66+
c.Check(err, jc.Satisfies, errors.IsNotValid)
67+
c.Check(err, gc.ErrorMatches, `invalid Location: Line set but Filename empty`)
68+
}
69+
70+
type LocationSuite struct {
71+
testing.IsolationSuite
72+
}
73+
74+
var _ = gc.Suite(&LocationSuite{})
75+
76+
func (s *LocationSuite) TestParseLocationTooLegitToQuit(c *gc.C) {
77+
expected := validLocation
78+
79+
loc, err := logfwd.ParseLocation(expected.Module, expected.String())
80+
c.Assert(err, jc.ErrorIsNil)
81+
82+
c.Check(loc, jc.DeepEquals, expected)
83+
}
84+
85+
func (s *LocationSuite) TestParseLocationMissingFilename(c *gc.C) {
86+
expected := validLocation
87+
expected.Filename = ""
88+
89+
loc, err := logfwd.ParseLocation(expected.Module, ":42")
90+
c.Assert(err, jc.ErrorIsNil)
91+
92+
c.Check(loc, jc.DeepEquals, expected)
93+
}
94+
95+
func (s *LocationSuite) TestParseLocationBogusFilename(c *gc.C) {
96+
expected := validLocation
97+
expected.Filename = "..."
98+
99+
loc, err := logfwd.ParseLocation(expected.Module, "...:42")
100+
c.Assert(err, jc.ErrorIsNil)
101+
102+
c.Check(loc, jc.DeepEquals, expected)
103+
}
104+
105+
func (s *LocationSuite) TestParseLocationFilenameOnly(c *gc.C) {
106+
expected := validLocation
107+
expected.Line = -1
108+
109+
loc, err := logfwd.ParseLocation(expected.Module, expected.Filename)
110+
c.Assert(err, jc.ErrorIsNil)
111+
112+
c.Check(loc, jc.DeepEquals, expected)
113+
}
114+
115+
func (s *LocationSuite) TestParseLocationMissingLine(c *gc.C) {
116+
_, err := logfwd.ParseLocation(validLocation.Module, "spam.go:")
117+
118+
c.Check(err, gc.ErrorMatches, `failed to parse location: strconv.ParseInt: parsing "": invalid syntax`)
119+
}
120+
121+
func (s *LocationSuite) TestParseLocationBogusLine(c *gc.C) {
122+
_, err := logfwd.ParseLocation(validLocation.Module, "spam.go:xxx")
123+
124+
c.Check(err, gc.ErrorMatches, `failed to parse location: strconv.ParseInt: parsing "xxx": invalid syntax`)
125+
}
126+
127+
func (s *LocationSuite) TestValidateValid(c *gc.C) {
128+
loc := validLocation
129+
130+
err := loc.Validate()
131+
132+
c.Check(err, jc.ErrorIsNil)
133+
}
134+
135+
func (s *LocationSuite) TestValidateEmpty(c *gc.C) {
136+
var loc logfwd.SourceLocation
137+
138+
err := loc.Validate()
139+
140+
c.Check(err, jc.ErrorIsNil)
141+
}
142+
143+
func (s *LocationSuite) TestValidateBadLine(c *gc.C) {
144+
loc := validLocation
145+
loc.Filename = ""
146+
147+
err := loc.Validate()
148+
149+
c.Check(err, jc.Satisfies, errors.IsNotValid)
150+
c.Check(err, gc.ErrorMatches, `Line set but Filename empty`)
151+
}
152+
153+
var validLocation = logfwd.SourceLocation{
154+
Module: "spam",
155+
Filename: "eggs.go",
156+
Line: 42,
157+
}
158+
159+
var validRecord = logfwd.Record{
160+
Origin: validOrigin,
161+
Timestamp: time.Now(),
162+
Level: loggo.ERROR,
163+
Location: validLocation,
164+
Message: "uh-oh",
165+
}

0 commit comments

Comments
 (0)