diff --git a/.gitignore b/.gitignore index c89044b..52ed524 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ bin/ config.json chromagies -log.txt \ No newline at end of file +log.txt +__debug* \ No newline at end of file diff --git a/main.go b/main.go index 8aec6c4..7d7f2b6 100644 --- a/main.go +++ b/main.go @@ -37,7 +37,7 @@ func main() { var conf config.Server = (config.GetConfig()).Server - http.ListenAndServe(fmt.Sprintf("%s:%s", conf.IP, conf.Port), router) + http.ListenAndServe(fmt.Sprintf("%s:%d", conf.IP, conf.Port), router) stopServer() } @@ -71,7 +71,7 @@ func initConfig() { } if err != nil { - log.Fatal("Cannot read/write config file !") + log.Fatalln("Cannot read/write config file !") } } diff --git a/src/api/api.go b/src/api/api.go index 3c9b6a9..fb4bc35 100644 --- a/src/api/api.go +++ b/src/api/api.go @@ -1,22 +1,32 @@ package api import ( + "chromagies/src/database" + "chromagies/src/profiler" "fmt" "net/http" + "time" "github.com/gorilla/mux" ) +const ( + profiler_API_PROCESS_REQUEST string = "API Process Request" +) + var apiRootPath string = "/api/v1" func Init(router *mux.Router) { + profiler.Register(profiler_API_PROCESS_REQUEST, 10000, "µs") + router.HandleFunc(apiRootPath, apiRoot).Methods("GET") routerAuth := router.PathPrefix(apiRootPath + "/auth").Subrouter() routerPage := router.PathPrefix(apiRootPath + "/page").Subrouter() routerUser := router.PathPrefix(apiRootPath + "/user").Subrouter() routerTag := router.PathPrefix(apiRootPath + "/tag").Subrouter() + routerDebug := router.PathPrefix(apiRootPath + "/debug").Subrouter() routerAuth.HandleFunc("", apiAuth) routerAuth.HandleFunc("/login", apiAuthLogin) @@ -32,67 +42,118 @@ func Init(router *mux.Router) { routerTag.HandleFunc("", apiTag) routerTag.HandleFunc("/{name}", apiTagName) + + routerDebug.HandleFunc("/profiler", apiDebugProfiler) + routerDebug.HandleFunc("/profiler/{name}", apiDebugProfilerName) } // API ROOT func apiRoot(w http.ResponseWriter, r *http.Request) { + tStart := time.Now() fmt.Fprintf(w, "API ROOT") + profiler.Add(profiler_API_PROCESS_REQUEST, time.Duration(time.Since(tStart).Microseconds())) } // API Auth func apiAuth(w http.ResponseWriter, r *http.Request) { + tStart := time.Now() fmt.Fprintf(w, "API Auth") + profiler.Add(profiler_API_PROCESS_REQUEST, time.Duration(time.Since(tStart).Microseconds())) } func apiAuthLogin(w http.ResponseWriter, r *http.Request) { + tStart := time.Now() fmt.Fprintf(w, "API Auth Login") + database.Ping() + profiler.Add(profiler_API_PROCESS_REQUEST, time.Duration(time.Since(tStart).Microseconds())) } func apiAuthLogout(w http.ResponseWriter, r *http.Request) { + tStart := time.Now() fmt.Fprintf(w, "API Auth Logout") + profiler.Add(profiler_API_PROCESS_REQUEST, time.Duration(time.Since(tStart).Microseconds())) } // API Page func apiPage(w http.ResponseWriter, r *http.Request) { + tStart := time.Now() fmt.Fprintf(w, "API Page") + profiler.Add(profiler_API_PROCESS_REQUEST, time.Duration(time.Since(tStart).Microseconds())) } func apiPageFolder(w http.ResponseWriter, r *http.Request) { + tStart := time.Now() vars := mux.Vars(r) fmt.Fprintf(w, "API Page Folder(%s)", vars["folder"]) + profiler.Add(profiler_API_PROCESS_REQUEST, time.Duration(time.Since(tStart).Microseconds())) } func apiPageFolderPage(w http.ResponseWriter, r *http.Request) { + tStart := time.Now() vars := mux.Vars(r) fmt.Fprintf(w, "API Page Folder(%s) Page(%s)", vars["folder"], vars["page"]) + profiler.Add(profiler_API_PROCESS_REQUEST, time.Duration(time.Since(tStart).Microseconds())) } func apiPageFolderPageContent(w http.ResponseWriter, r *http.Request) { + tStart := time.Now() vars := mux.Vars(r) fmt.Fprintf(w, "API Page Folder(%s) Page(%s) Content", vars["folder"], vars["page"]) + profiler.Add(profiler_API_PROCESS_REQUEST, time.Duration(time.Since(tStart).Microseconds())) } // API User func apiUser(w http.ResponseWriter, r *http.Request) { + tStart := time.Now() fmt.Fprintf(w, "API User") + profiler.Add(profiler_API_PROCESS_REQUEST, time.Duration(time.Since(tStart).Microseconds())) } func apiUserName(w http.ResponseWriter, r *http.Request) { + tStart := time.Now() vars := mux.Vars(r) fmt.Fprintf(w, "API User Name(%s)", vars["name"]) + profiler.Add(profiler_API_PROCESS_REQUEST, time.Duration(time.Since(tStart).Microseconds())) } // API Tag func apiTag(w http.ResponseWriter, r *http.Request) { + tStart := time.Now() fmt.Fprintf(w, "API Tag") + profiler.Add(profiler_API_PROCESS_REQUEST, time.Duration(time.Since(tStart).Microseconds())) } func apiTagName(w http.ResponseWriter, r *http.Request) { + tStart := time.Now() vars := mux.Vars(r) fmt.Fprintf(w, "API Tag Name(%s)", vars["name"]) + profiler.Add(profiler_API_PROCESS_REQUEST, time.Duration(time.Since(tStart).Microseconds())) +} + +// API Tag + +func apiDebugProfiler(w http.ResponseWriter, r *http.Request) { + tStart := time.Now() + + var entries []string + profiler.GetAll(&entries) + + for i := range entries { + var entry string = entries[i] + fmt.Fprintf(w, "API Debug Profiler(%s)\n\n%s\n\n", entry, string(profiler.Get(entry).ToString())) + } + + profiler.Add(profiler_API_PROCESS_REQUEST, time.Duration(time.Since(tStart).Microseconds())) +} + +func apiDebugProfilerName(w http.ResponseWriter, r *http.Request) { + tStart := time.Now() + vars := mux.Vars(r) + fmt.Fprintf(w, "API Debug Profiler(%s)\n%s", vars["name"], string(profiler.Get(vars["name"]).ToString())) + profiler.Add(profiler_API_PROCESS_REQUEST, time.Duration(time.Since(tStart).Microseconds())) } diff --git a/src/config/config.go b/src/config/config.go index b9025ed..92ff2ad 100644 --- a/src/config/config.go +++ b/src/config/config.go @@ -10,10 +10,12 @@ const CONFIG_FILE_NAME string = "./config.json" const CONFIG_FILE_DEFAULT_CONF string = `{ "Database": { "Host": "localhost", - "Port": "3306", + "Port": 3306, "Database": "Chromagies", "User": "chromagies_user", - "Password": "12345678" + "Password": "12345678", + "RetriesOnError": 5, + "TimeBetweenRetriesMs": 100 }, "Logger": { @@ -21,16 +23,18 @@ const CONFIG_FILE_DEFAULT_CONF string = `{ }, "Server": { "IP": "0.0.0.0", - "Port": "12345" + "Port": 12345 } }` type Database struct { - Host string - Port string - Database string - User string - Password string + Host string + Port uint16 + Database string + User string + Password string + RetriesOnError uint16 + TimeBetweenRetriesMs uint16 } type Logger struct { @@ -39,7 +43,7 @@ type Logger struct { type Server struct { IP string - Port string + Port uint16 } type Config struct { diff --git a/src/database/database.go b/src/database/database.go index 057daaa..2600da9 100644 --- a/src/database/database.go +++ b/src/database/database.go @@ -2,53 +2,156 @@ package database import ( "chromagies/src/config" + "chromagies/src/profiler" "database/sql" "fmt" "log" + "time" _ "github.com/go-sql-driver/mysql" ) +const ( + profiler_DATABASE_CONNECT string = "Database Connect" + profiler_DATABASE_QUERY string = "Database Query" + profiler_DATABASE_PING string = "Database Ping" +) + var db *sql.DB var dbConfig config.Database -var dbPath string +var isInit bool func Init() { - var err error + if isInit { + return + } + dbConfig = config.GetConfig().Database - dbPath = fmt.Sprintf("%s:%s@(%s:%s)/%s?parseTime=true", dbConfig.User, dbConfig.Password, dbConfig.Host, dbConfig.Port, dbConfig.Database) - db, err = sql.Open("mysql", dbPath) - if err != nil { - log.Println("Cannot connect to database !") - log.Fatal(err) - } + profiler.Register(profiler_DATABASE_CONNECT, 20, "µs") + profiler.Register(profiler_DATABASE_QUERY, 4000, "µs") + profiler.Register(profiler_DATABASE_PING, 20, "µs") - err = db.Ping() - if err != nil { - log.Println("Cannot ping the database !") - log.Fatal(err) - } + isInit = true + + Connect() var query string = `SHOW GRANTS;` var rows *sql.Rows = executeQuery(query) - defer rows.Close() + if rows != nil { + defer rows.Close() + for rows.Next() { + var res string + rows.Scan(&res) + fmt.Println(res) + } + } - for rows.Next() { - var res string - rows.Scan(&res) - fmt.Println(res) +} + +func Terminate() { + isInit = false + if db != nil { + db.Close() + db = nil + } +} + +func Connect() { + if !isInit { + return + } + + if db != nil { + db.Close() + } + + var err error + var dbPath string = fmt.Sprintf("%s:%s@(%s:%d)/%s?parseTime=true", dbConfig.User, dbConfig.Password, dbConfig.Host, dbConfig.Port, dbConfig.Database) + log.Printf("Connecting to database %s at %s:%d as %s\n", dbConfig.Database, dbConfig.Host, dbConfig.Port, dbConfig.User) + + tStart := time.Now() + for i := range dbConfig.RetriesOnError { + db, err = sql.Open("mysql", dbPath) + if err == nil { + break + } + log.Printf("Error while connecting the database [%d/%d] : %s\n", i+1, dbConfig.RetriesOnError, err) + time.Sleep((time.Duration)(dbConfig.TimeBetweenRetriesMs) * time.Millisecond) + } + profiler.Add(profiler_DATABASE_CONNECT, time.Duration(time.Since(tStart).Microseconds())) + + if err != nil { + log.Fatalln("Cannot connect to database !") + } + + Ping() +} + +func Disconnect() { + if !isInit { + return + } + + if db == nil { + return + } + + db.Close() +} + +func Ping() { + if !isInit { + return + } + + if db == nil { + return + } + + var err error + tStart := time.Now() + for i := range dbConfig.RetriesOnError { + err = db.Ping() + if err == nil { + break + } + log.Printf("Error while pinging the database [%d/%d] : %s\n", i+1, dbConfig.RetriesOnError, err) + time.Sleep((time.Duration)(dbConfig.TimeBetweenRetriesMs) * time.Millisecond) + } + profiler.Add(profiler_DATABASE_PING, time.Duration(time.Since(tStart).Microseconds())) + + if err != nil { + log.Fatalln("Cannot ping the database !") } } func executeQuery(query string) *sql.Rows { + if !isInit { + return nil + } + + if db == nil { + return nil + } + + Ping() + var err error var rows *sql.Rows + tStart := time.Now() + for i := range dbConfig.RetriesOnError { + rows, err = db.Query(query) + if err == nil { + break + } + log.Printf("Error while querying the database [%d/%d] : %s\n", i+1, dbConfig.RetriesOnError, err) + time.Sleep((time.Duration)(dbConfig.TimeBetweenRetriesMs) * time.Millisecond) + } + profiler.Add(profiler_DATABASE_QUERY, time.Duration(time.Since(tStart).Microseconds())) - rows, err = db.Query(query) if err != nil { - log.Println("Error while query DB :") - log.Fatal(err) + log.Fatalln("Cannot query the database !") } return rows diff --git a/src/profiler/profiler.go b/src/profiler/profiler.go new file mode 100644 index 0000000..a60dac2 --- /dev/null +++ b/src/profiler/profiler.go @@ -0,0 +1,139 @@ +package profiler + +import ( + "fmt" + "log" + "maps" + "math" + "slices" + "sync" + "time" +) + +const PROFILER_DEFAULT_MAX_COUNT uint32 = 1000 +const PROFILER_DEFAULT_UNIT string = "" + +type Data struct { + count uint32 + mean float64 + min time.Duration + max time.Duration + lastValue time.Duration + unit string +} + +type Profile struct { + sync.RWMutex + unit string + count uint32 + maxCount uint32 + index uint32 + sum time.Duration + mean float64 + values []time.Duration + lastValue time.Duration +} + +var profiles map[string]*Profile = make(map[string]*Profile) + +func Register(entry string, maxValues uint32, unit string) { + var profile *Profile + var exist bool + profile, exist = profiles[entry] + + if exist && (profile != nil) { + return + } + + profile = new(Profile) + profile.maxCount = maxValues + profile.values = make([]time.Duration, profile.maxCount) + profile.unit = unit + profiles[entry] = profile +} + +func Add(entry string, duration time.Duration) { + var profile *Profile + var exist bool + profile, exist = profiles[entry] + + if !exist || (profile == nil) { + profile = new(Profile) + profile.maxCount = PROFILER_DEFAULT_MAX_COUNT + profile.values = make([]time.Duration, profile.maxCount) + profile.unit = PROFILER_DEFAULT_UNIT + profiles[entry] = profile + } + + profile.Lock() + + var lastValue time.Duration = profile.values[profile.index] + profile.values[profile.index] = duration + profile.index++ + if profile.index >= profile.maxCount { + profile.index = 0 + } + + if profile.count < profile.maxCount { + profile.count++ + } + + profile.sum += duration - lastValue + profile.mean = (((float64)(profile.sum)) / (float64)(time.Duration(profile.count))) + profile.lastValue = duration + + profile.Unlock() +} + +func GetAll(entries *[]string) { + *entries = slices.Collect(maps.Keys(profiles)) +} + +func Get(entry string) Data { + var profile *Profile + var exist bool + profile, exist = profiles[entry] + + if !exist || (profile == nil) { + profile = new(Profile) + profiles[entry] = profile + } + + var data Data + + profile.RLock() + + data.mean = profile.mean + data.min = math.MaxInt64 + data.max = 0 + data.count = profile.count + data.unit = profile.unit + data.lastValue = profile.lastValue + for i := range profile.count { + value := profile.values[i] + if value < data.min { + data.min = value + } + if value > data.max { + data.max = value + } + } + + profile.RUnlock() + + return data +} + +func (data Data) ToString() string { + return fmt.Sprintf(` + - Last value : %d %s + - Mean : %f %s + - Min : %d %s + - Max : %d %s + - Number of values : %d + `, data.lastValue, data.unit, data.mean, data.unit, data.min, data.unit, data.max, data.unit, data.count) +} + +func Log(entry string) { + log.Println(Get(entry).ToString()) +}