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.
This commit is contained in:
Mahmoud Emad
2024-12-25 13:31:03 +02:00
parent 71373ed2fc
commit 10e27d2962
15 changed files with 1030 additions and 0 deletions

25
lib/core/base/base.v Normal file
View File

@@ -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
}

164
lib/core/base/baseconfig.v Normal file
View File

@@ -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
}

View File

@@ -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)!
// }
// }

187
lib/core/base/context.v Normal file
View File

@@ -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_ ?&paramsparser.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() !&paramsparser.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()!
}

View File

@@ -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)!
}

6
lib/core/base/factory.v Normal file
View File

@@ -0,0 +1,6 @@
module base
__global (
contexts map[u32]&Context
context_current u32
)

View File

@@ -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)!
}

39
lib/core/base/readme.md Normal file
View File

@@ -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
```

98
lib/core/base/session.v Normal file
View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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}'
}

View File

@@ -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)")

View File

@@ -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

View File

@@ -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"

View File

@@ -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