-
Notifications
You must be signed in to change notification settings - Fork 502
/
auditlog.go
247 lines (218 loc) · 7.36 KB
/
auditlog.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
// Copyright 2017 Canonical Ltd.
// Licensed under the AGPLv3, see LICENCE file for details.
package auditlog
import (
"encoding/hex"
"encoding/json"
"fmt"
"io"
"math/rand"
"path/filepath"
"time"
"github.com/juju/clock"
"github.com/juju/errors"
"github.com/juju/lumberjack/v2"
"github.com/juju/juju/core/paths"
internallogger "github.com/juju/juju/internal/logger"
)
var logger = internallogger.GetLogger("core.auditlog")
// Conversation represents a high-level juju command from the juju
// client (or other client). There'll be one Conversation per API
// connection from the client, with zero or more associated
// Request/ResponseErrors pairs.
type Conversation struct {
Who string `json:"who"` // username@idm
What string `json:"what"` // "juju deploy ./foo/bar"
When string `json:"when"` // ISO 8601 to second precision
ModelName string `json:"model-name"` // full representation "user/name"
ModelUUID string `json:"model-uuid"`
ConversationID string `json:"conversation-id"` // uint64 in hex
ConnectionID string `json:"connection-id"` // uint64 in hex (using %X to match the value in log files)
}
// ConversationArgs is the information needed to create a method recorder.
type ConversationArgs struct {
Who string
What string
ModelName string
ModelUUID string
ConnectionID uint64
}
// Request represents a call to an API facade made as part of
// a specific conversation.
type Request struct {
ConversationID string `json:"conversation-id"`
ConnectionID string `json:"connection-id"`
RequestID uint64 `json:"request-id"`
When string `json:"when"`
Facade string `json:"facade"`
Method string `json:"method"`
Version int `json:"version"`
Args string `json:"args,omitempty"`
}
// RequestArgs is the information about an API call that we want to
// record.
type RequestArgs struct {
Facade string
Method string
Version int
Args string
RequestID uint64
}
// ResponseErrors captures any errors coming back from the API in
// response to a request.
type ResponseErrors struct {
ConversationID string `json:"conversation-id"`
ConnectionID string `json:"connection-id"`
RequestID uint64 `json:"request-id"`
When string `json:"when"`
Errors []*Error `json:"errors"`
}
// ResponseErrorsArgs has errors from an API response to record in the
// audit log.
type ResponseErrorsArgs struct {
RequestID uint64
Errors []*Error
}
// Error holds the details of an error sent back from the API.
type Error struct {
Message string `json:"message"`
Code string `json:"code"`
}
// Record is the top-level entry type in an audit log, which serves as
// a type discriminator. Only one of Conversation/Request/Errors should be set.
type Record struct {
Conversation *Conversation `json:"conversation,omitempty"`
Request *Request `json:"request,omitempty"`
Errors *ResponseErrors `json:"errors,omitempty"`
}
// AuditLog represents something that can store calls, requests and
// responses somewhere.
type AuditLog interface {
AddConversation(c Conversation) error
AddRequest(r Request) error
AddResponse(r ResponseErrors) error
Close() error
}
// Recorder records method calls for a specific API connection.
type Recorder struct {
log AuditLog
clock clock.Clock
connectionID string
callID string
}
// NewRecorder creates a Recorder for the connection described (and
// stores details of the initial call in the log).
func NewRecorder(log AuditLog, clock clock.Clock, c ConversationArgs) (*Recorder, error) {
callID := newConversationID()
connectionID := idString(c.ConnectionID)
err := log.AddConversation(Conversation{
ConversationID: callID,
ConnectionID: connectionID,
Who: c.Who,
What: c.What,
When: clock.Now().Format(time.RFC3339),
ModelName: c.ModelName,
ModelUUID: c.ModelUUID,
})
if err != nil {
return nil, errors.Trace(err)
}
return &Recorder{
log: log,
clock: clock,
callID: callID,
connectionID: connectionID,
}, nil
}
// AddRequest records a method call to the API.
func (r *Recorder) AddRequest(m RequestArgs) error {
return errors.Trace(r.log.AddRequest(Request{
ConversationID: r.callID,
ConnectionID: r.connectionID,
RequestID: m.RequestID,
When: r.clock.Now().Format(time.RFC3339),
Facade: m.Facade,
Method: m.Method,
Version: m.Version,
Args: m.Args,
}))
}
// AddResponse records the result of a method call to the API.
func (r *Recorder) AddResponse(m ResponseErrorsArgs) error {
return errors.Trace(r.log.AddResponse(ResponseErrors{
ConversationID: r.callID,
ConnectionID: r.connectionID,
RequestID: m.RequestID,
When: r.clock.Now().Format(time.RFC3339),
Errors: m.Errors,
}))
}
// newConversationID generates a random 64bit integer as hex - this
// will be used to link the requests and responses with the command
// the user issued. We don't use the API server's connection ID here
// because that starts from 0 and increments, so it resets when the
// API server is restarted. The conversation ID needs to be unique
// across restarts, otherwise we'd attribute requests to the wrong
// conversation.
func newConversationID() string {
buf := make([]byte, 8)
rand.Read(buf) // Can't fail
return hex.EncodeToString(buf)
}
type auditLogFile struct {
fileLogger io.WriteCloser
}
// NewLogFile returns an audit entry sink which writes to an audit.log
// file in the specified directory. maxSize is the maximum size (in
// megabytes) of the log file before it gets rotated. maxBackups is
// the maximum number of old compressed log files to keep (or 0 to
// keep all of them).
func NewLogFile(logDir string, maxSize, maxBackups int) AuditLog {
logPath := filepath.Join(logDir, "audit.log")
if err := paths.PrimeLogFile(logPath); err != nil {
// This isn't a fatal error so log and continue if priming
// fails.
logger.Errorf("Unable to prime %s (proceeding anyway): %v", logPath, err)
}
ljLogger := &lumberjack.Logger{
Filename: logPath,
MaxSize: maxSize,
MaxBackups: maxBackups,
Compress: true,
}
logger.Debugf("created rotating log file %q with max size %d MB and max backups %d",
ljLogger.Filename, ljLogger.MaxSize, ljLogger.MaxBackups)
return &auditLogFile{
fileLogger: ljLogger,
}
}
// AddConversation implements AuditLog.
func (a *auditLogFile) AddConversation(c Conversation) error {
return errors.Trace(a.addRecord(Record{Conversation: &c}))
}
// AddRequest implements AuditLog.
func (a *auditLogFile) AddRequest(m Request) error {
return errors.Trace(a.addRecord(Record{Request: &m}))
}
// AddResponse implements AuditLog.
func (a *auditLogFile) AddResponse(m ResponseErrors) error {
return errors.Trace(a.addRecord(Record{Errors: &m}))
}
// Close implements AuditLog.
func (a *auditLogFile) Close() error {
return errors.Trace(a.fileLogger.Close())
}
func (a *auditLogFile) addRecord(r Record) error {
bytes, err := json.Marshal(r)
if err != nil {
return errors.Trace(err)
}
// Add a linebreak to bytes rather than doing two calls to write
// just in case lumberjack rolls the file between them.
bytes = append(bytes, byte('\n'))
_, err = a.fileLogger.Write(bytes)
return errors.Trace(err)
}
func idString(id uint64) string {
return fmt.Sprintf("%X", id)
}