github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/core/auditlog/auditlog.go (about)

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