/* 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" "regexp" "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 ActionType *string Target *string Started int64 Recordable bool } var ( doNotRecordRegex = regexp.MustCompile(`(?i)^get|^init$`) // the default gets we don't care about writeMutex sync.Mutex // DB is public so other packages can do selects on it DB *sql.DB // Tables is public so other packages can range over it 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), type text check(type in ('direct', 'phase')) null, target text null, success tinyint(1) default 0, started bigint, elapsed bigint, stopped bigint)` // the prepared statement used for inserts // TODO (aschiefe): determine if we need to batch inserts insert = `INSERT INTO table(subcomponent, user, type, target, 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 will 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 { err = createTables() if err != nil { log.Fatal(err) } } } // createTables is only used when there is no database to write the correct structure for the records func createTables() error { for _, table := range Tables { stmt, err := DB.Prepare(strings.ReplaceAll(tableCreate, "table", table)) if err != nil { return err } _, err = stmt.Exec() if err != nil { return err } log.Tracef("%s table created.", table) } return nil } // NewTransaction establishes the transaction which will record func NewTransaction(user *string, request configs.WsMessage) *Transaction { return &Transaction{ Table: request.Component, SubComponent: request.SubComponent, ActionType: request.ActionType, Target: request.Target, Started: time.Now().UnixNano() / 1000000, User: user, Recordable: isRecordable(request), } } // Complete will put an entry into the statistics database for the transaction func (transaction *Transaction) Complete(errorMessageNotPresent bool) { if transaction.User != nil && transaction.Recordable { 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 errorMessageNotPresent { success = 1 } writeMutex.Lock() defer writeMutex.Unlock() result, err := stmt.Exec(transaction.SubComponent, transaction.User, transaction.ActionType, transaction.Target, 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 isRecordable(request configs.WsMessage) bool { recordable := true // don't record auth attempts if request.Component == configs.Auth { recordable = false } // don't record default get data events if doNotRecordRegex.MatchString(string(request.SubComponent)) { recordable = false } // don't request actions taken against multiple targets, the individual action will be recorded if request.Targets != nil { recordable = false } return recordable }