airshipui/pkg/statistics/recorder.go
Schiefelbein, Andrew b4583b1db5 Add sqlite for statistics / auditing for each transaction
This allows for a built in audit database for user actions
We can also see how often commands are run, how long they take
as well as who and when they're run

TODO: use sqlcipher to encrypt at rest & password protect the db

Change-Id: Ic7c8927bcfdd46ede3fe6a5aca4f57892ca3f3d4
2020-09-30 13:20:42 -05:00

153 lines
4.1 KiB
Go
Executable File

/*
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package statistics
import (
"database/sql"
"os"
"strings"
"sync"
"time"
_ "github.com/mattn/go-sqlite3" // this is required for the sqlite driver
"opendev.org/airship/airshipui/pkg/configs"
"opendev.org/airship/airshipui/pkg/log"
)
// Transaction will record the details of the CTL transaction and record them to the DB
type Transaction struct {
Table configs.WsComponentType
SubComponent configs.WsSubComponentType
User *string
Started int64
}
var (
writeMutex sync.Mutex
db *sql.DB
tables = []string{"baremetal", "cluster", "config", "document", "image", "phase", "secret"}
)
const (
// the table structure used for the records
tableCreate = `CREATE TABLE IF NOT EXISTS table (
subcomponent varchar(64) null,
user varchar(64) null,
success tinyint(1) default 0,
started timestamp,
elapsed timestamp,
stopped timestamp)`
// the prepared statement used for inserts
// TODO (aschiefe): determine if we need to batch inserts
insert = "INSERT INTO table(subcomponent, user, success, started, elapsed, stopped) values(?,?,?,?,?,?)"
)
// Init will create the database if it doesn't exist or open the existing database
func Init() {
intitTables := false
// TODO (aschiefe): pull the db location out to the confing
if _, err := os.Stat("./sqlite/statistics.db"); os.IsNotExist(err) {
intitTables = true
}
// need to define error so that the program well set the global db variable
var err error
// TODO (aschiefe): encrypt & password protect the database
// TODO (aschiefe): pull the db location out to the confing
db, err = sql.Open("sqlite3", "./sqlite/statistics.db")
if err != nil {
log.Fatal(err)
}
if intitTables {
createTables()
}
}
// createTables is only used when there is no database to write the correct structure for the records
func createTables() {
for index := range tables {
stmt, err := db.Prepare(strings.ReplaceAll(tableCreate, "table", tables[index]))
if err != nil {
log.Fatal(err)
}
_, err = stmt.Exec()
if err != nil {
log.Fatal(err)
}
log.Tracef("%s table created.", tables[index])
}
}
// NewTransaction establishes the transaction which will record
func NewTransaction(request configs.WsMessage, user *string) *Transaction {
return &Transaction{
Table: request.Component,
SubComponent: request.SubComponent,
Started: time.Now().UnixNano() / 1000000,
User: user,
}
}
// Complete will put an entry into the statistics database for the transaction
func (transaction *Transaction) Complete(errorMessagePresent bool) {
if transaction.User != nil && transaction.isRecordable() {
stmt, err := db.Prepare(strings.ReplaceAll(insert, "table", string(transaction.Table)))
if err != nil {
log.Error(err)
return
}
started := transaction.Started
stopped := time.Now().UnixNano() / 1000000
success := 0
if errorMessagePresent {
success = 1
}
writeMutex.Lock()
defer writeMutex.Unlock()
result, err := stmt.Exec(transaction.SubComponent, transaction.User, success, started, (stopped - started), stopped)
if err != nil {
log.Error(err)
return
}
rows, err := result.RowsAffected()
if err != nil {
log.Error(err)
return
}
log.Tracef("%d rows inserted into %s.", rows, transaction.Table)
}
}
// isRecordable will shuffle through the transaction and determine if we should write it to the database
func (transaction *Transaction) isRecordable() bool {
recordable := true
if transaction.Table == configs.Auth {
recordable = false
}
switch transaction.SubComponent {
case configs.GetTarget:
recordable = false
case configs.GetPhaseTree:
recordable = false
}
return recordable
}