From 10e27d296217b913407c0875c0f9fccca31c7a42 Mon Sep 17 00:00:00 2001 From: Mahmoud Emad Date: Wed, 25 Dec 2024 13:31:03 +0200 Subject: [PATCH] feat: add base module - Add base module with context, session, and configurator. - Implement session management and configuration loading/saving. - Introduce error handling and logging mechanisms. - Include template files for Lua scripts. --- lib/core/base/base.v | 25 +++ lib/core/base/baseconfig.v | 164 +++++++++++++++++++ lib/core/base/configurator.v | 121 ++++++++++++++ lib/core/base/context.v | 187 ++++++++++++++++++++++ lib/core/base/context_session.v | 89 ++++++++++ lib/core/base/factory.v | 6 + lib/core/base/factory_context.v | 103 ++++++++++++ lib/core/base/readme.md | 39 +++++ lib/core/base/session.v | 98 ++++++++++++ lib/core/base/session_error.v | 42 +++++ lib/core/base/session_logger.v | 61 +++++++ lib/core/base/templates/load.sh | 4 + lib/core/base/templates/logger_add.lua | 51 ++++++ lib/core/base/templates/logger_del.lua | 22 +++ lib/core/base/templates/logger_example.sh | 18 +++ 15 files changed, 1030 insertions(+) create mode 100644 lib/core/base/base.v create mode 100644 lib/core/base/baseconfig.v create mode 100644 lib/core/base/configurator.v create mode 100644 lib/core/base/context.v create mode 100644 lib/core/base/context_session.v create mode 100644 lib/core/base/factory.v create mode 100644 lib/core/base/factory_context.v create mode 100644 lib/core/base/readme.md create mode 100644 lib/core/base/session.v create mode 100644 lib/core/base/session_error.v create mode 100644 lib/core/base/session_logger.v create mode 100644 lib/core/base/templates/load.sh create mode 100644 lib/core/base/templates/logger_add.lua create mode 100644 lib/core/base/templates/logger_del.lua create mode 100755 lib/core/base/templates/logger_example.sh diff --git a/lib/core/base/base.v b/lib/core/base/base.v new file mode 100644 index 00000000..3c17de3d --- /dev/null +++ b/lib/core/base/base.v @@ -0,0 +1,25 @@ +module base + +@[heap] +pub struct Base { + configtype string @[required] +mut: + instance string + session_ ?&Session +} + +pub fn (mut self Base) session() !&Session { + mut mysession := self.session_ or { + // mut c := context()! + // mut r := c.redis()! + panic('sdsdsd') + // incrkey := 'sessions:base:latest:${self.type_name}:${self.instance}' + // latestid:=r.incr(incrkey)! + // name:="${self.type_name}_${self.instance}_${latestid}" + // mut s:=c.session_new(name:name)! + // self.session_ = &s + // &s + } + + return mysession +} diff --git a/lib/core/base/baseconfig.v b/lib/core/base/baseconfig.v new file mode 100644 index 00000000..22484ab9 --- /dev/null +++ b/lib/core/base/baseconfig.v @@ -0,0 +1,164 @@ +module base + +import json +// import freeflowuniverse.crystallib.ui.console + +// is an object which has a configurator, session and config object which is unique for the model +// T is the Config Object + +pub struct BaseConfig[T] { +mut: + configurator_ ?Configurator[T] @[skip; str: skip] + config_ ?&T + session_ ?&Session @[skip; str: skip] + configtype string +pub mut: + instance string +} + +pub fn (mut self BaseConfig[T]) session() !&Session { + mut mysession := self.session_ or { + mut c := context()! + mut r := c.redis()! + incrkey := 'sessions:base:latest:${self.configtype}:${self.instance}' + latestid := r.incr(incrkey)! + name := '${self.configtype}_${self.instance}_${latestid}' + mut s := c.session_new(name: name)! + self.session_ = &s + &s + } + + return mysession +} + +// management class of the configs of this obj +pub fn (mut self BaseConfig[T]) configurator() !&Configurator[T] { + if self.configurator_ == none { + mut c := configurator_new[T]( + instance: self.instance + )! + self.configurator_ = c + } + return &(self.configurator_ or { return error('configurator not initialized') }) +} + +// will overwrite the config +pub fn (mut self BaseConfig[T]) config_set(myconfig T) ! { + self.config_ = &myconfig + self.config_save()! +} + +pub fn (mut self BaseConfig[T]) config_new() !&T { + config := self.config_ or { + mut configurator := self.configurator()! + mut c := configurator.new()! + self.config_ = &c + &c + } + + self.config_save()! + return config +} + +pub fn (mut self BaseConfig[T]) config() !&T { + mut config := self.config_ or { return error('config was not initialized yet') } + + return config +} + +pub fn (mut self BaseConfig[T]) config_get() !&T { + mut mycontext := context()! + mut config := self.config_ or { + mut configurator := self.configurator()! + if !(configurator.exists()!) { + mut mycfg := self.config_new()! + return mycfg + } + + mut db := mycontext.db_config_get()! + if !db.exists(key: configurator.config_key())! { + return error("can't find configuration with name: ${configurator.config_key()} in context:'${mycontext.config.name}'") + } + data := db.get(key: configurator.config_key())! + + mut c := json.decode(T, data)! + $for field in T.fields { + field_attrs := attrs_get(field.attrs) + if 'secret' in field_attrs { + // QUESTION: is it ok if we only support encryption for string fields + $if field.typ is string { + v := c.$(field.name) + c.$(field.name) = mycontext.secret_decrypt(v)! + // console.print_debug('FIELD DECRYPTED: ${field} ${field.name}') + } + } + } + self.config_ = &c + &c + } + + return config +} + +pub fn (mut self BaseConfig[T]) config_save() ! { + mut config2 := *self.config()! // dereference so we don't modify the original + mut mycontext := context()! + // //walk over the properties see where they need to be encrypted, if yes encrypt + $for field in T.fields { + field_attrs := attrs_get(field.attrs) + if 'secret' in field_attrs { + // QUESTION: is it ok if we only support encryption for string fields + $if field.typ is string { + v := config2.$(field.name) + config2.$(field.name) = mycontext.secret_encrypt(v)! + } + // console.print_debug('FIELD ENCRYPTED: ${field.name}') + } + } + mut configurator := self.configurator()! + configurator.set(config2)! +} + +pub fn (mut self BaseConfig[T]) config_delete() ! { + mut configurator := self.configurator()! + configurator.delete()! + self.config_ = none +} + +pub enum Action { + set + get + new + delete +} + +// init our class with the base session_args +pub fn (mut self BaseConfig[T]) init(configtype string, instance string, action Action, myconfig T) ! { + self.instance = instance + self.configtype = configtype + if action == .get { + self.config_get()! + } else if action == .new { + self.config_new()! + } else if action == .delete { + self.config_delete()! + } else if action == .set { + self.config_set(myconfig)! + } else { + panic('bug') + } +} + +// will return {'name': 'teststruct', 'params': ''} +fn attrs_get(attrs []string) map[string]string { + mut out := map[string]string{} + for i in attrs { + if i.contains('=') { + kv := i.split('=') + out[kv[0].trim_space().to_lower()] = kv[1].trim_space().to_lower() + } else { + out[i.trim_space().to_lower()] = '' + } + } + return out +} diff --git a/lib/core/base/configurator.v b/lib/core/base/configurator.v new file mode 100644 index 00000000..cad63a51 --- /dev/null +++ b/lib/core/base/configurator.v @@ -0,0 +1,121 @@ +module base + +import json +import freeflowuniverse.crystallib.ui.console + +@[heap] +pub struct Configurator[T] { +pub mut: + // context &Context @[skip; str: skip] + instance string + description string + configured bool + configtype string // e.g. sshclient +} + +@[params] +pub struct ConfiguratorArgs { +pub mut: + // context &Context // optional context for the configurator + instance string @[required] +} + +// name is e.g. mailclient (the type of configuration setting) +// instance is the instance of the config e.g. kds +// the context defines the context in which we operate, is optional will get the default one if not set +pub fn configurator_new[T](args ConfiguratorArgs) !Configurator[T] { + return Configurator[T]{ + // context: args.context + configtype: T.name.to_lower() + instance: args.instance + } +} + +fn (mut self Configurator[T]) config_key() string { + return '${self.configtype}_config_${self.instance}' +} + +// set the full configuration as one object to dbconfig +pub fn (mut self Configurator[T]) set(args T) ! { + mut mycontext := context()! + mut db := mycontext.db_config_get()! + data := json.encode_pretty(args) + db.set(key: self.config_key(), value: data)! +} + +pub fn (mut self Configurator[T]) exists() !bool { + mut mycontext := context()! + mut db := mycontext.db_config_get()! + return db.exists(key: self.config_key()) +} + +pub fn (mut self Configurator[T]) new() !T { + return T{ + instance: self.instance + description: self.description + } +} + +pub fn (mut self Configurator[T]) get() !T { + mut mycontext := context()! + mut db := mycontext.db_config_get()! + if !db.exists(key: self.config_key())! { + return error("can't find configuration with name: ${self.config_key()} in context:'${mycontext.config.name}'") + } + data := db.get(key: self.config_key())! + return json.decode(T, data)! +} + +pub fn (mut self Configurator[T]) delete() ! { + mut mycontext := context()! + mut db := mycontext.db_config_get()! + db.delete(key: self.config_key())! +} + +pub fn (mut self Configurator[T]) getset(args T) !T { + mut mycontext := context()! + mut db := mycontext.db_config_get()! + if db.exists(key: self.config_key())! { + return self.get()! + } + self.set(args)! + return self.get()! +} + +@[params] +pub struct PrintArgs { +pub mut: + name string +} + +pub fn (mut self Configurator[T]) list() ![]string { + panic('implement') +} + +pub fn (mut self Configurator[T]) configprint(args PrintArgs) ! { + mut mycontext := context()! + mut db := mycontext.db_config_get()! + if args.name.len > 0 { + if db.exists(key: self.config_key())! { + data := db.get(key: self.config_key())! + c := json.decode(T, data)! + console.print_debug('${c}') + console.print_debug('') + } else { + return error("Can't find connection with name: ${args.name}") + } + } else { + panic('implement') + // for item in list()! { + // // console.print_debug(" ==== $item") + // configprint(name: item)! + // } + } +} + +// init our class with the base session_args +// pub fn (mut self Configurator[T]) init(session_args_ SessionNewArgs) ! { +// self.session_=session_args.session or { +// session_new(session_args)! +// } +// } diff --git a/lib/core/base/context.v b/lib/core/base/context.v new file mode 100644 index 00000000..0490d277 --- /dev/null +++ b/lib/core/base/context.v @@ -0,0 +1,187 @@ +module base + +import freeflowuniverse.crystallib.data.paramsparser +import freeflowuniverse.crystallib.clients.redisclient +import freeflowuniverse.crystallib.data.dbfs +// import freeflowuniverse.crystallib.crypt.secp256k1 +import freeflowuniverse.crystallib.crypt.aes_symmetric +import freeflowuniverse.crystallib.ui +import freeflowuniverse.crystallib.ui.console +import freeflowuniverse.crystallib.core.pathlib +import freeflowuniverse.crystallib.core.texttools +import freeflowuniverse.crystallib.core.rootpath +import json +import os +import crypto.md5 + +@[heap] +pub struct Context { +mut: + // priv_key_ ?&secp256k1.Secp256k1 @[skip; str: skip] + params_ ?¶msparser.Params + dbcollection_ ?&dbfs.DBCollection @[skip; str: skip] + redis_ ?&redisclient.Redis @[skip; str: skip] +pub mut: + // snippets map[string]string + config ContextConfig +} + +@[params] +pub struct ContextConfig { +pub mut: + id u32 @[required] + name string = 'default' + params string + coderoot string + interactive bool + secret string // is hashed secret + priv_key string // encrypted version + db_path string // path to dbcollection + encrypt bool +} + +// return the gistructure as is being used in context +pub fn (mut self Context) params() !¶msparser.Params { + mut p := self.params_ or { + mut p := paramsparser.new(self.config.params)! + self.params_ = &p + &p + } + + return p +} + +pub fn (self Context) id() string { + return self.config.id.str() +} + +pub fn (self Context) name() string { + return self.config.name +} + +pub fn (self Context) guid() string { + return '${self.id()}:${self.name()}' +} + +pub fn (mut self Context) redis() !&redisclient.Redis { + mut r2 := self.redis_ or { + mut r := redisclient.core_get()! + if self.config.id > 0 { + // make sure we are on the right db + r.selectdb(int(self.config.id))! + } + self.redis_ = &r + &r + } + + return r2 +} + +pub fn (mut self Context) save() ! { + jsonargs := json.encode_pretty(self.config) + mut r := self.redis()! + // console.print_debug("save") + // console.print_debug(jsonargs) + r.set('context:config', jsonargs)! +} + +// get context from out of redis +pub fn (mut self Context) load() ! { + mut r := self.redis()! + d := r.get('context:config')! + // console.print_debug("load") + // console.print_debug(d) + if d.len > 0 { + self.config = json.decode(ContextConfig, d)! + } +} + +fn (mut self Context) cfg_redis_exists() !bool { + mut r := self.redis()! + return r.exists('context:config')! +} + +// return db collection +pub fn (mut self Context) dbcollection() !&dbfs.DBCollection { + mut dbc2 := self.dbcollection_ or { + if self.config.db_path.len == 0 { + self.config.db_path = '${os.home_dir()}/hero/db/${self.config.id}' + } + mut dbc := dbfs.get( + contextid: self.config.id + dbpath: self.config.db_path + secret: self.config.secret + )! + self.dbcollection_ = &dbc + &dbc + } + + return dbc2 +} + +pub fn (mut self Context) db_get(dbname string) !dbfs.DB { + mut dbc := self.dbcollection()! + return dbc.db_get_create(name: dbname, withkeys: true)! +} + +// always return the config db which is the same for all apps in context +pub fn (mut self Context) db_config_get() !dbfs.DB { + mut dbc := self.dbcollection()! + return dbc.db_get_create(name: 'config', withkeys: true)! +} + +pub fn (mut self Context) hero_config_set(cat string, name string, content_ string) ! { + mut content := texttools.dedent(content_) + content = rootpath.shell_expansion(content) + path := '${os.home_dir()}/hero/context/${self.config.name}/${cat}__${name}.yaml' + mut config_file := pathlib.get_file(path: path)! + config_file.write(content)! +} + +pub fn (mut self Context) hero_config_exists(cat string, name string) bool { + path := '${os.home_dir()}/hero/context/${self.config.name}/${cat}__${name}.yaml' + return os.exists(path) +} + +pub fn (mut self Context) hero_config_get(cat string, name string) !string { + path := '${os.home_dir()}/hero/context/${self.config.name}/${cat}__${name}.yaml' + mut config_file := pathlib.get_file(path: path, create: false)! + return config_file.read()! +} + +pub fn (mut self Context) secret_encrypt(txt string) !string { + return aes_symmetric.encrypt_str(txt, self.secret_get()!) +} + +pub fn (mut self Context) secret_decrypt(txt string) !string { + return aes_symmetric.decrypt_str(txt, self.secret_get()!) +} + +pub fn (mut self Context) secret_get() !string { + mut secret := self.config.secret + if secret == '' { + self.secret_configure()! + secret = self.config.secret + self.save()! + } + if secret == '' { + return error("can't get secret") + } + return secret +} + +// show a UI in console to configure the secret +pub fn (mut self Context) secret_configure() ! { + mut myui := ui.new()! + console.clear() + secret_ := myui.ask_question(question: 'Please enter your hero secret string:')! + self.secret_set(secret_)! +} + +// unhashed secret +pub fn (mut self Context) secret_set(secret_ string) ! { + secret := secret_.trim_space() + secret2 := md5.hexhash(secret) + self.config.secret = secret2 + self.save()! +} diff --git a/lib/core/base/context_session.v b/lib/core/base/context_session.v new file mode 100644 index 00000000..eec17062 --- /dev/null +++ b/lib/core/base/context_session.v @@ -0,0 +1,89 @@ +module base + +import freeflowuniverse.crystallib.data.ourtime +import freeflowuniverse.crystallib.data.paramsparser +import json + +@[params] +pub struct SessionConfig { +pub mut: + name string // unique name for session (id), there can be more than 1 session per context + start string // can be e.g. +1h + description string + params string +} + +// get a session object based on the name / +// params: +// ``` +// name string +// ``` +pub fn (mut context Context) session_new(args_ SessionConfig) !Session { + mut args := args_ + if args.name == '' { + args.name = ourtime.now().key() + } + + if args.start == '' { + t := ourtime.new(args.start)! + args.start = t.str() + } + + mut r := context.redis()! + + rkey := 'sessions:config:${args.name}' + + config_json := json.encode(args) + + r.set(rkey, config_json)! + + rkey_latest := 'sessions:config:latest' + r.set(rkey_latest, args.name)! + + return context.session_get(name: args.name)! +} + +@[params] +pub struct ContextSessionGetArgs { +pub mut: + name string +} + +pub fn (mut context Context) session_get(args_ ContextSessionGetArgs) !Session { + mut args := args_ + mut r := context.redis()! + + if args.name == '' { + rkey_latest := 'sessions:config:latest' + args.name = r.get(rkey_latest)! + } + rkey := 'sessions:config:${args.name}' + mut datajson := r.get(rkey)! + if datajson == '' { + if args.name == '' { + return context.session_new()! + } else { + return error("can't find session with name ${args.name}") + } + } + config := json.decode(SessionConfig, datajson)! + t := ourtime.new(config.start)! + mut s := Session{ + name: args.name + start: t + context: &context + params: paramsparser.new(config.params)! + config: config + } + return s +} + +pub fn (mut context Context) session_latest() !Session { + mut r := context.redis()! + rkey_latest := 'sessions:config:latest' + latestname := r.get(rkey_latest)! + if latestname == '' { + return context.session_new()! + } + return context.session_get(name: latestname)! +} diff --git a/lib/core/base/factory.v b/lib/core/base/factory.v new file mode 100644 index 00000000..4077be65 --- /dev/null +++ b/lib/core/base/factory.v @@ -0,0 +1,6 @@ +module base + +__global ( + contexts map[u32]&Context + context_current u32 +) diff --git a/lib/core/base/factory_context.v b/lib/core/base/factory_context.v new file mode 100644 index 00000000..856297e3 --- /dev/null +++ b/lib/core/base/factory_context.v @@ -0,0 +1,103 @@ +module base + +import freeflowuniverse.crystallib.data.paramsparser +import freeflowuniverse.crystallib.ui +import freeflowuniverse.crystallib.ui.console +import crypto.md5 + +@[params] +pub struct ContextConfigArgs { +pub mut: + id u32 + name string = 'default' + params string + coderoot string + interactive bool + secret string + encrypt bool + priv_key_hex string // hex representation of private key +} + +// configure a context object +// params: . +// ``` +// id u32 //if not set then redis will get a new context id +// name string = 'default' +// params string +// coderoot string +// interactive bool +// secret string +// priv_key_hex string //hex representation of private key +// ``` +pub fn context_new(args_ ContextConfigArgs) !&Context { + mut args := ContextConfig{ + id: args_.id + name: args_.name + params: args_.params + coderoot: args_.coderoot + interactive: args_.interactive + secret: args_.secret + encrypt: args_.encrypt + } + + if args.encrypt && args.secret == '' && args.interactive { + mut myui := ui.new()! + console.clear() + args.secret = myui.ask_question(question: 'Please enter your hero secret string:')! + } + + if args.encrypt && args.secret.len > 0 { + args.secret = md5.hexhash(args.secret) + } + + mut c := Context{ + config: args + } + + // if args_.priv_key_hex.len > 0 { + // c.privkey_set(args_.priv_key_hex)! + // } + + // c.save()! + + if args.params.len > 0 { + mut p := paramsparser.new('')! + c.params_ = &p + } + + c.save()! + contexts[args.id] = &c + + return contexts[args.id] or { panic('bug') } +} + +pub fn context_get(id u32) !&Context { + context_current = id + + if id in contexts { + return contexts[id] or { panic('bug') } + } + + mut mycontext := Context{ + config: ContextConfig{ + id: id + } + } + + if mycontext.cfg_redis_exists()! { + mycontext.load()! + return &mycontext + } + + mut mycontext2 := context_new(id: id)! + return mycontext2 +} + +pub fn context_select(id u32) !&Context { + context_current = id + return context()! +} + +pub fn context() !&Context { + return context_get(context_current)! +} diff --git a/lib/core/base/readme.md b/lib/core/base/readme.md new file mode 100644 index 00000000..344d87db --- /dev/null +++ b/lib/core/base/readme.md @@ -0,0 +1,39 @@ +## context & sessions + +Everything we do in hero lives in a context, each context has a unique name. + +Redis is used to manage the contexts and the sessions. + +- redis db 0 + - `context:current` curent id of the context, is also the DB if redis if redis is used +- redis db X, x is nr of context + - `context:name` name of this context + - `context:secret` secret as is used in context (is md5 of original config secret) + - `context:privkey` secp256k1 privkey as is used in context (encrypted by secret) + - `context:params` params for a context, can have metadata + - `context:lastid` last id for our db + - `session:$id` the location of session + - `session:$id:params` params for the session, can have metadata + +Session id is $contextid:$sessionid (e.g. 10:111) + +**The SessionNewArgs:** + +- context_name string = 'default' +- session_name string //default will be an incremental nr if not filled in +- interactive bool = true //can ask questions, default on true +- coderoot string //this will define where all code is checked out + +```v +import freeflowuniverse.crystallib.core.base + +mut session:=context_new( + coderoot:'/tmp/code' + interactive:true +)! + +mut session:=session_new(context:'default',session:'mysession1')! +mut session:=session_new()! //returns default context & incremental new session + +``` + diff --git a/lib/core/base/session.v b/lib/core/base/session.v new file mode 100644 index 00000000..9a199db4 --- /dev/null +++ b/lib/core/base/session.v @@ -0,0 +1,98 @@ +module base + +import freeflowuniverse.crystallib.data.ourtime +// import freeflowuniverse.crystallib.core.texttools +import freeflowuniverse.crystallib.data.paramsparser +import freeflowuniverse.crystallib.data.dbfs +import json +// import freeflowuniverse.crystallib.core.pathlib +// import freeflowuniverse.crystallib.develop.gittools +// import freeflowuniverse.crystallib.ui.console + +@[heap] +pub struct Session { +pub mut: + name string // unique id for session (session id), can be more than one per context + interactive bool = true + params paramsparser.Params + start ourtime.OurTime + end ourtime.OurTime + context &Context @[skip; str: skip] + config SessionConfig + env map[string]string +} + +///////// LOAD & SAVE + +// fn (mut self Session) key() string { +// return 'hero:sessions:${self.guid()}' +// } + +// get db of the session, is unique per session +pub fn (mut self Session) db_get() !dbfs.DB { + return self.context.db_get('session_${self.name}')! +} + +// get the db of the config, is unique per context +pub fn (mut self Session) db_config_get() !dbfs.DB { + return self.context.db_get('config')! +} + +// load the params from redis +pub fn (mut self Session) load() ! { + mut r := self.context.redis()! + rkey := 'sessions:config:${self.name}' + mut datajson := r.get(rkey)! + if datajson == '' { + return error("can't find session with name ${self.name}") + } + self.config = json.decode(SessionConfig, datajson)! + self.params = paramsparser.new(self.config.params)! +} + +// save the params to redis +pub fn (mut self Session) save() ! { + self.check()! + rkey := 'sessions:config:${self.name}' + mut r := self.context.redis()! + self.config.params = self.params.str() + config_json := json.encode(self.config) + r.set(rkey, config_json)! +} + +// Set an environment variable +pub fn (mut self Session) env_set(key string, value string) ! { + self.env[key] = value + self.save()! +} + +// Get an environment variable +pub fn (mut self Session) env_get(key string) !string { + return self.env[key] or { return error("can't find env in session ${self.name}") } +} + +// Delete an environment variable +pub fn (mut self Session) env_delete(key string) { + self.env.delete(key) +} + +////////// REPRESENTATION + +pub fn (self Session) check() ! { + if self.name.len < 3 { + return error('name should be at least 3 char') + } +} + +pub fn (self Session) guid() string { + return '${self.context.guid()}:${self.name}' +} + +fn (self Session) str2() string { + mut out := 'session:${self.guid()}' + out += ' start:\'${self.start}\'' + if !self.end.empty() { + out += ' end:\'${self.end}\'' + } + return out +} diff --git a/lib/core/base/session_error.v b/lib/core/base/session_error.v new file mode 100644 index 00000000..70dbbf23 --- /dev/null +++ b/lib/core/base/session_error.v @@ -0,0 +1,42 @@ +module base + +import freeflowuniverse.crystallib.data.ourtime +import freeflowuniverse.crystallib.core.texttools + +pub struct ErrorArgs { +pub mut: + cat string + error string + errortype ErrorType +} + +pub struct ErrorItem { +pub mut: + time ourtime.OurTime + cat string + error string + errortype ErrorType + session string // the unique name for the session +} + +pub enum ErrorType { + uknown + value +} + +pub fn (mut session Session) error(args_ ErrorArgs) !ErrorItem { + mut args := args_ + args.cat = texttools.name_fix(args.cat) + + mut l := ErrorItem{ + cat: args.cat + error: args.error + errortype: args.errortype + time: ourtime.now() + session: session.name + } + + // TODO: get string output and put to redis + + return l +} diff --git a/lib/core/base/session_logger.v b/lib/core/base/session_logger.v new file mode 100644 index 00000000..8f7846cf --- /dev/null +++ b/lib/core/base/session_logger.v @@ -0,0 +1,61 @@ +module base + +import freeflowuniverse.crystallib.data.ourtime +import freeflowuniverse.crystallib.core.texttools + +@[heap] +pub struct Logger { +pub mut: + session string +} + +pub struct LogItem { +pub mut: + time ourtime.OurTime + cat string + log string + logtype LogType + session string +} + +pub enum LogType { + stdout + error +} + +pub fn (session Session) logger_new() !Logger { + // mut l:=log.Log{} + // l.set_full_logpath('./info.log') + // l.log_to_console_too() + return Logger{} +} + +@[params] +pub struct LogArgs { +pub mut: + cat string + log string @[required] + logtype LogType +} + +// cat & log are the arguments . +// category can be any well chosen category e.g. vm +pub fn (mut session Session) log(args_ LogArgs) !LogItem { + mut args := args_ + args.cat = texttools.name_fix(args.cat) + + mut l := LogItem{ + cat: args.cat + log: args.log + time: ourtime.now() + // session: session.guid() + } + + // TODO: get string output and put to redis + + return l +} + +pub fn (li LogItem) str() string { + return '${li.session}' +} diff --git a/lib/core/base/templates/load.sh b/lib/core/base/templates/load.sh new file mode 100644 index 00000000..ed25e865 --- /dev/null +++ b/lib/core/base/templates/load.sh @@ -0,0 +1,4 @@ +#redis-cli SCRIPT LOAD "$(cat logger.lua)" +export LOGGER_ADD=$(redis-cli SCRIPT LOAD "$(cat logger_add.lua)") +export LOGGER_DEL=$(redis-cli SCRIPT LOAD "$(cat logger_del.lua)") +export STATS_ADD=$(redis-cli SCRIPT LOAD "$(cat stats_add.lua)") diff --git a/lib/core/base/templates/logger_add.lua b/lib/core/base/templates/logger_add.lua new file mode 100644 index 00000000..f1cdb450 --- /dev/null +++ b/lib/core/base/templates/logger_add.lua @@ -0,0 +1,51 @@ + +local function normalize(str) + return string.gsub(string.lower(str), "%s+", "_") +end + +local src = normalize(ARGV[1]) +local category = normalize(ARGV[2]) +local message = ARGV[3] +local logHashKey = "logs:" .. src +local lastIdKey = "logs:" .. src .. ":lastid" + +-- redis.log(redis.LOG_NOTICE, "...") + +-- Increment the log ID using Redis INCR command +local logId = redis.call('INCR', lastIdKey) + +-- Get the current epoch time +local epoch = redis.call('TIME')[1] + +-- Prepare the log entry with a unique ID, epoch time, and message +local logEntry = category .. ":" .. epoch .. ":" .. message + +-- Add the new log entry to the hash set +redis.call('HSET', logHashKey, logId, logEntry) + +-- Optionally manage the size of the hash to keep the latest 2000 entries only +local hlen = redis.call('HLEN', logHashKey) +if hlen > 5000 then + -- Find the smallest logId + local smallestId = logId + local cursor = "0" + repeat + local scanResult = redis.call('HSCAN', logHashKey, cursor, "COUNT", 5) + cursor = scanResult[1] + local entries = scanResult[2] + for i = 1, #entries, 2 do + local currentId = tonumber(entries[i]) + if currentId < smallestId then + smallestId = currentId + end + end + until cursor == "0" + -- redis.log(redis.LOG_NOTICE, "smallest id: " .. smallestId) + + -- Remove the oldest entries + for i = smallestId, smallestId + 500 do + redis.call('HDEL', logHashKey, i) + end +end + +return logEntry diff --git a/lib/core/base/templates/logger_del.lua b/lib/core/base/templates/logger_del.lua new file mode 100644 index 00000000..10253d6c --- /dev/null +++ b/lib/core/base/templates/logger_del.lua @@ -0,0 +1,22 @@ +-- Function to normalize strings (convert to lower case and replace spaces with underscores) +local function normalize(str) + return string.gsub(string.lower(str), "%s+", "_") +end + +local src = ARGV[1] and normalize(ARGV[1]) or nil + +if src then + -- Delete logs for specified source and category + local logHashKey = "logs:" .. src + local lastIdKey = logHashKey .. ":lastid" + redis.call('DEL', logHashKey) + redis.call('DEL', lastIdKey) +else + -- Delete all logs for all sources and categories + local keys = redis.call('KEYS', "logs:*") + for i, key in ipairs(keys) do + redis.call('DEL', key) + end +end + +return "Logs deleted" diff --git a/lib/core/base/templates/logger_example.sh b/lib/core/base/templates/logger_example.sh new file mode 100755 index 00000000..5a399d98 --- /dev/null +++ b/lib/core/base/templates/logger_example.sh @@ -0,0 +1,18 @@ +#!/bin/bash +set -x +cd "$(dirname "$0")" +source load.sh + +# for i in $(seq 1 1000) +# do +# redis-cli EVALSHA $LOGHASH 0 "AAA" "CAT1" "Example log message" +# redis-cli EVALSHA $LOGHASH 0 "AAA" "CAT2" "Example log message" +# done + +redis-cli EVALSHA $LOGGER_DEL 0 + +for i in $(seq 1 200) +do + redis-cli EVALSHA $LOGGER_ADD 0 "BBB" "CAT2" "Example log message" +done +