diff --git a/.gitignore b/.gitignore index fbef8db..0786743 100644 --- a/.gitignore +++ b/.gitignore @@ -33,7 +33,7 @@ go.work.sum # env file .env - +./examples/logs/*.cpo # Editor/IDE # .idea/ # .vscode/ diff --git a/config.cpo b/config.cpo new file mode 100644 index 0000000..30120ba --- /dev/null +++ b/config.cpo @@ -0,0 +1,3 @@ +CDC_ADAPTER=cpo +CDC_FILE_PATH=examples/logs/sawit.cpo +LOG_PATH=examples/logs/sawit.log \ No newline at end of file diff --git a/data.log b/data.log new file mode 100644 index 0000000..e69de29 diff --git a/docs/Bulog.md b/docs/Bulog.md new file mode 100644 index 0000000..6bc63de --- /dev/null +++ b/docs/Bulog.md @@ -0,0 +1,16 @@ +**Bulog** is module to create Log easier and pretty with JSON format. With this format Log can be analyzed more and easy to create aggregation log. + +## Log Level +| Method | Level | Description| +|---------|------|-------------| +| Kabarin | Info | create log as info | +| Awas | Warning | create log as Warning | +| Kacau | Error | create log as Error | +| Dolog | Debug | create log as debug | + +Output example: +``` +{"level":"Info","message":"Give info to user","usage":80,"timestamp":"2026-01-09T11:37:23.392Z"} +{"level":"Warning","message":"CPU usage has been exceeded","usage":90,"timestamp":"2026-01-09T11:37:23.398Z"} +{"level":"Error","message":"No disk space available","usage":100,"timestamp":"2026-01-09T11:37:23.398Z"} +``` \ No newline at end of file diff --git a/docs/DB Event.md b/docs/DB Event.md new file mode 100644 index 0000000..467ad24 --- /dev/null +++ b/docs/DB Event.md @@ -0,0 +1,32 @@ + +**DB Event** is a feature for `sawitDB` to provide Event Drivent capabilities especially regarding Change Data Capture. This feature is listening for event below: +| Event Name | Description| +|---------|-------------------| +| OnTableCreated | Triggered when new table is crated | +| OnTableDropped | Triggered when a table is dropped | +| OnTableSelected | Triggered when select record from a table | +| OnTableInserted | Triggered when insert record into a table | +| OnTableUpdated | Triggered when a table updataed rows | +| OnTableDeleted | Triggered when rows deleted from a table | + +## Enabled CDC (Change Data Capture) +SawitDB is supported for CDC, to enabled CDC you need follow these step +1. Determined **CDC Adapter**, currently we still only support **CPO** adapter. +2. **CDC Adapter** captures change on SawitDB and write as AQL to file `.cpo`, all changed will be recorded on this file. Below how to config CDC and Adapter on `config.cpo` for server side + +``` +CDC_FILE_PATH=./examples/logs/sawit.cpo +CDC_ADAPTER=cpo +``` +Next will be supported for Kafka +## Disabled CDC +If you don't want use CDC you just need let `CDC_ADAPTER` empty on `config.cpo` +## Custome Event Handler +By default EventHandler provide by `./internal/event/dbevent_handler.go` but you can use custom event handler as below: +``` +const event = require('./dbeventHandlerExample') +const db = new SawitDB(dbPath,{dbevent:new event()}); +``` +Full example can view on `./examples/dbevent/main.go`. +To see output CDC on `./examples/logs/sawit.cpo` + diff --git a/docs/config cpo.md b/docs/config cpo.md new file mode 100644 index 0000000..1621852 --- /dev/null +++ b/docs/config cpo.md @@ -0,0 +1,11 @@ +**SawitDB** used file `.cpo` as config file, eg: `config.cpo` to use it similar to `.env` file, here you can store all configuration variable for entire database. + +## Config +To load configuration it is provided in `internal/config/config.go` below is an example: +``` + if err := config.LoadEnv("config.cpo"); err != nil { + log.Fatal(err) + } +``` +## CPO vs ENV +you can use `.env` in service level such as to config the service like port, endpoint, IP etc. `.cpo` should be used for confi in database engine level like how to enabled/disabled feature, set path file storage, security, etc. `.cpo` similar to `.cfg` in Mysql \ No newline at end of file diff --git a/example.sawit b/example.sawit new file mode 100644 index 0000000..2f20261 Binary files /dev/null and b/example.sawit differ diff --git a/examples/create_local/main.go b/examples/create_local/main.go index 569dc84..69e3551 100644 --- a/examples/create_local/main.go +++ b/examples/create_local/main.go @@ -4,21 +4,14 @@ import ( "encoding/json" "fmt" "log" - "os" - "path/filepath" + + "github.com/WowoEngine/SawitDB-Go/internal/app" "github.com/WowoEngine/SawitDB-Go/internal/engine" ) func main() { - dbPath := "example.sawit" - if _, err := os.Stat(dbPath); err == nil { - os.Remove(dbPath) - } - - // Create absolute path to avoid directory confusion - absPath, _ := filepath.Abs(dbPath) - db, err := engine.NewSawitDB(absPath) + db, err := app.Bootstrap() if err != nil { log.Fatalf("Failed to create DB: %v", err) } diff --git a/examples/dbevent/main.go b/examples/dbevent/main.go new file mode 100644 index 0000000..1919538 --- /dev/null +++ b/examples/dbevent/main.go @@ -0,0 +1,31 @@ +package main + +import ( + "fmt" + + "github.com/WowoEngine/SawitDB-Go/internal/app" + "github.com/WowoEngine/SawitDB-Go/internal/engine" +) + +func main() { + db, _ := app.Bootstrap() + + query(db, "LAHAN sawit") + + query(db, "TANAM KE sawit (id, bibit, umur) BIBIT (101, 'Dura', 2)") + query(db, "TANAM KE sawit (id, bibit, umur) BIBIT (102, 'Tenera', 5)") + query(db, "TANAM KE sawit (id, bibit, umur) BIBIT (103, 'Pisifera', 1)") + query(db, "PANEN * DARI sawit DIMANA id='103'") + query(db, "PUPUK sawit DENGAN bibit='Dura' DIMANA id='103'") + query(db, "GUSUR DARI sawit DIMANA id='103'") + query(db, "BAKAR LAHAN sawit") +} + +func query(db *engine.SawitDB, q string) { + res, err := db.Query(q, nil) + if err != nil { + fmt.Printf("Query error '%s': %v\n", q, err) + } else { + fmt.Printf("Query '%s': %v\n", q, res) + } +} diff --git a/examples/logs/sawit.cpo b/examples/logs/sawit.cpo new file mode 100644 index 0000000..bf8de10 --- /dev/null +++ b/examples/logs/sawit.cpo @@ -0,0 +1,16 @@ +LAHAN sawit +TANAM KE sawit (id, bibit, umur) BIBIT (101, 'Dura', 2) +TANAM KE sawit (id, bibit, umur) BIBIT (102, 'Tenera', 5) +TANAM KE sawit (id, bibit, umur) BIBIT (103, 'Pisifera', 1) +PANEN * DARI sawit DIMANA id='103' +PUPUK sawit DENGAN bibit='Dura' DIMANA id='103' +GUSUR DARI sawit DIMANA id='103' +BAKAR LAHAN sawit +LAHAN sawit +TANAM KE sawit (id, bibit, umur) BIBIT (101, 'Dura', 2) +TANAM KE sawit (id, bibit, umur) BIBIT (102, 'Tenera', 5) +TANAM KE sawit (id, bibit, umur) BIBIT (103, 'Pisifera', 1) +PANEN * DARI sawit DIMANA id='103' +PUPUK sawit DENGAN bibit='Dura' DIMANA id='103' +GUSUR DARI sawit DIMANA id='103' +BAKAR LAHAN sawit \ No newline at end of file diff --git a/examples/logs/sawit.log b/examples/logs/sawit.log new file mode 100644 index 0000000..e69de29 diff --git a/internal/app/bootstrap.go b/internal/app/bootstrap.go new file mode 100644 index 0000000..0ae40fd --- /dev/null +++ b/internal/app/bootstrap.go @@ -0,0 +1,39 @@ +package app + +import ( + "log" + "os" + "path/filepath" + + "github.com/WowoEngine/SawitDB-Go/internal/config" + "github.com/WowoEngine/SawitDB-Go/internal/engine" + "github.com/WowoEngine/SawitDB-Go/internal/entity" + "github.com/WowoEngine/SawitDB-Go/internal/event" + "github.com/WowoEngine/SawitDB-Go/internal/utils" +) + +func Bootstrap() (*engine.SawitDB, error) { + if err := config.LoadEnv("config.cpo"); err != nil { + log.Fatal(err) + } + + cfg := config.NewConfig() + + bulog, _ := utils.NewBulog(cfg.LogPath()) + dbevent, _ := event.NewDBEventHandler(cfg.CdcFilePath(), bulog) + dbPath := "example.sawit" + if _, err := os.Stat(dbPath); err == nil { + os.Remove(dbPath) + } + + absPath, _ := filepath.Abs(dbPath) + + db, err := engine.NewSawitDB(absPath, dbevent, &entity.EngineOption{ + IsEvent: cfg.CdcAdapter() != "", + }) + if err != nil { + log.Fatalf("Failed to create DB: %v", err) + } + + return db, nil +} diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..e2993a9 --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,66 @@ +package config + +import ( + "bufio" + "os" + "strings" + + "github.com/WowoEngine/SawitDB-Go/internal/constant" +) + +type Config struct { + cdcAdapter string + cdcFilePath string + logPath string +} + +func NewConfig() *Config { + return &Config{ + cdcAdapter: os.Getenv(constant.CDC_ADAPTER), + cdcFilePath: os.Getenv(constant.CDC_PATH), + logPath: os.Getenv(constant.LOG_PATH), + } +} + +func (c *Config) CdcAdapter() string { + return c.cdcAdapter +} +func (c *Config) CdcFilePath() string { + return c.cdcFilePath +} +func (c *Config) LogPath() string { + return c.logPath +} + +// LoadEnv membaca file .env dan memasukkan ke environment variable +func LoadEnv(path string) error { + file, err := os.Open(path) + if err != nil { + return err + } + defer file.Close() + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + + // skip comment & empty line + if line == "" || strings.HasPrefix(line, "#") { + continue + } + + parts := strings.SplitN(line, "=", 2) + if len(parts) != 2 { + continue + } + + key := strings.TrimSpace(parts[0]) + value := strings.TrimSpace(parts[1]) + + value = strings.Trim(value, `"'`) + + os.Setenv(key, value) + } + + return scanner.Err() +} diff --git a/internal/constant/constant.go b/internal/constant/constant.go new file mode 100644 index 0000000..b2a7186 --- /dev/null +++ b/internal/constant/constant.go @@ -0,0 +1,19 @@ +package constant + +const ( + CDC_ADAPTER string = "CDC_ADAPTER" + CDC_PATH string = "CDC_FILE_PATH" +) + +const ( + LOG_PATH string = "LOG_PATH" +) + +type LogLevel string + +const ( + LOG_INFO LogLevel = "INFO" + LOG_WARN LogLevel = "WARN" + LOG_ERROR LogLevel = "ERROR" + LOG_DEBUG LogLevel = "DEBUG" +) diff --git a/internal/engine/dbevent.go b/internal/engine/dbevent.go new file mode 100644 index 0000000..4a2b567 --- /dev/null +++ b/internal/engine/dbevent.go @@ -0,0 +1,10 @@ +package engine + +type DBEvent interface { + OnTableCreated(name string, index int, aql string) + OnTableDropped(name string, index int, aql string) + OnTableSelected(name string, data []map[string]interface{}, aql string) + OnTableInserted(name string, data map[string]interface{}, aql string) + OnTableDeleted(name string, data []map[string]interface{}, aql string) + OnTableUpdated(name string, data []map[string]interface{}, aql string) +} diff --git a/internal/engine/engine.go b/internal/engine/engine.go index 479c1b1..a4cb785 100644 --- a/internal/engine/engine.go +++ b/internal/engine/engine.go @@ -7,20 +7,25 @@ import ( "fmt" "math" "regexp" + "sort" + "strings" + + "github.com/WowoEngine/SawitDB-Go/internal/entity" "github.com/WowoEngine/SawitDB-Go/internal/index" "github.com/WowoEngine/SawitDB-Go/internal/parser" "github.com/WowoEngine/SawitDB-Go/internal/storage" - "sort" - "strings" ) type SawitDB struct { Pager *storage.Pager Indexes map[string]*index.BTreeIndex Parser *parser.QueryParser + query string + event DBEvent + isEvent bool } -func NewSawitDB(filePath string) (*SawitDB, error) { +func NewSawitDB(filePath string, event DBEvent, options *entity.EngineOption) (*SawitDB, error) { pager, err := storage.NewPager(filePath) if err != nil { return nil, err @@ -29,6 +34,8 @@ func NewSawitDB(filePath string) (*SawitDB, error) { Pager: pager, Indexes: make(map[string]*index.BTreeIndex), Parser: parser.NewQueryParser(), + event: event, + isEvent: options.IsEvent, }, nil } @@ -38,6 +45,7 @@ func (db *SawitDB) Close() error { func (db *SawitDB) Query(queryString string, params map[string]interface{}) (interface{}, error) { cmd := db.Parser.Parse(queryString, params) + db.query = queryString if cmd.Type == "EMPTY" { return "", nil @@ -60,9 +68,9 @@ func (db *SawitDB) Query(queryString string, params map[string]interface{}) (int case "SHOW_INDEXES": return db.showIndexes(cmd.Table) case "INSERT": - return db.insert(cmd.Table, cmd.Data) + return db.insert(cmd.Table, cmd.Data, true) case "SELECT": - rows, err := db._select(cmd.Table, cmd.Criteria, cmd.Sort, cmd.Limit, cmd.Offset) + rows, err := db._select(cmd.Table, cmd.Criteria, cmd.Sort, cmd.Limit, cmd.Offset, true) if err != nil { return nil, err } @@ -92,7 +100,7 @@ func (db *SawitDB) Query(queryString string, params map[string]interface{}) (int return projected, nil case "DELETE": - return db.delete(cmd.Table, cmd.Criteria) + return db.delete(cmd.Table, cmd.Criteria, true) case "UPDATE": return db.update(cmd.Table, cmd.Updates, cmd.Criteria) case "DROP_TABLE": @@ -204,6 +212,7 @@ func (db *SawitDB) createTable(name string) (string, error) { return "", err } + db.event.OnTableCreated(name, int(numTables+1), db.query) return fmt.Sprintf("Kebun '%s' telah dibuka.", name), nil } @@ -239,7 +248,7 @@ func (db *SawitDB) dropTable(name string) (string, error) { if err != nil { return "", err } - + db.event.OnTableDropped(name, int(numTables+1), db.query) return fmt.Sprintf("Kebun '%s' telah dibakar (Drop).", name), nil } @@ -260,7 +269,11 @@ func (db *SawitDB) updateTableLastPage(name string, newLastPageId uint32) error return db.Pager.WritePage(0, p0) } -func (db *SawitDB) insert(table string, data map[string]interface{}) (string, error) { +/** +* this function is called recrusively +* so I add sendEvent as param in order to inform the function should trigger event or not + */ +func (db *SawitDB) insert(table string, data map[string]interface{}, sendEvent bool) (string, error) { if len(data) == 0 { return "", errors.New("Data kosong") } @@ -323,7 +336,9 @@ func (db *SawitDB) insert(table string, data map[string]interface{}) (string, er // Indexes db.updateIndexes(table, data) - + if sendEvent { + db.event.OnTableInserted(table, data, db.query) + } return "Bibit tertanam.", nil } @@ -470,7 +485,11 @@ func toFloat(i interface{}) (float64, bool) { } } -func (db *SawitDB) _select(table string, criteria *parser.Criteria, sortOpt *parser.Sort, limit, offset *int) ([]map[string]interface{}, error) { +/** +* this function is called recrusively +* so I add sendEvent as param in order to inform the function should trigger event or not + */ +func (db *SawitDB) _select(table string, criteria *parser.Criteria, sortOpt *parser.Sort, limit, offset *int, sendEvent bool) ([]map[string]interface{}, error) { entry, err := db.findTableEntry(table) if err != nil { return nil, err @@ -552,6 +571,9 @@ func (db *SawitDB) _select(table string, criteria *parser.Criteria, sortOpt *par if endIndex > len(results) { endIndex = len(results) } + if sendEvent { + db.event.OnTableSelected(table, results[startIndex:endIndex], db.query) + } return results[startIndex:endIndex], nil } @@ -587,7 +609,11 @@ func (db *SawitDB) scanTable(entry *TableEntry, criteria *parser.Criteria) ([]ma return results, nil } -func (db *SawitDB) delete(table string, criteria *parser.Criteria) (string, error) { +/** +* this function is called recrusively +* so I add sendEvent as param in order to inform the function should trigger event or not + */ +func (db *SawitDB) delete(table string, criteria *parser.Criteria, sendEvent bool) (string, error) { entry, err := db.findTableEntry(table) if err != nil { return "", err @@ -595,6 +621,11 @@ func (db *SawitDB) delete(table string, criteria *parser.Criteria) (string, erro if entry == nil { return "", fmt.Errorf("Kebun '%s' tidak ditemukan.", table) } + var dataDeleted []map[string]interface{} + if sendEvent { + recordDeleted, _ := db._select(table, criteria, nil, nil, nil, false) + dataDeleted = recordDeleted + } currentPageId := entry.StartPage deletedCount := 0 @@ -656,12 +687,14 @@ func (db *SawitDB) delete(table string, criteria *parser.Criteria) (string, erro } currentPageId = binary.LittleEndian.Uint32(pData[0:]) } - + if sendEvent { + db.event.OnTableDeleted(table, dataDeleted, db.query) + } return fmt.Sprintf("Berhasil menggusur %d bibit.", deletedCount), nil } func (db *SawitDB) update(table string, updates map[string]interface{}, criteria *parser.Criteria) (string, error) { - records, err := db._select(table, criteria, nil, nil, nil) + records, err := db._select(table, criteria, nil, nil, nil, false) // dont send event if err != nil { return "", err } @@ -670,16 +703,17 @@ func (db *SawitDB) update(table string, updates map[string]interface{}, criteria } // Inefficient: Delete then Insert - db.delete(table, criteria) + db.delete(table, criteria, false) // dont sent event count := 0 for _, rec := range records { for k, v := range updates { rec[k] = v } - db.insert(table, rec) + db.insert(table, rec, false) // don't send event count++ } + db.event.OnTableUpdated(table, records, db.query) return fmt.Sprintf("Berhasil memupuk %d bibit.", count), nil } @@ -702,7 +736,7 @@ func (db *SawitDB) createIndex(table string, field string) (string, error) { index.KeyField = field // Build - records, _ := db._select(table, nil, nil, nil, nil) // All + records, _ := db._select(table, nil, nil, nil, nil, false) // All for _, rec := range records { if val, ok := rec[field]; ok { index.Insert(val, rec) @@ -734,7 +768,7 @@ func (db *SawitDB) showIndexes(table string) (interface{}, error) { } func (db *SawitDB) aggregate(table string, fn string, field string, criteria *parser.Criteria, groupBy string) (interface{}, error) { - records, err := db._select(table, criteria, nil, nil, nil) + records, err := db._select(table, criteria, nil, nil, nil, false) if err != nil { return nil, err } diff --git a/internal/entity/engine_option.go b/internal/entity/engine_option.go new file mode 100644 index 0000000..399aa19 --- /dev/null +++ b/internal/entity/engine_option.go @@ -0,0 +1,5 @@ +package entity + +type EngineOption struct { + IsEvent bool +} diff --git a/internal/event/dbevent_handler.go b/internal/event/dbevent_handler.go new file mode 100644 index 0000000..7e0ef93 --- /dev/null +++ b/internal/event/dbevent_handler.go @@ -0,0 +1,87 @@ +package event + +import ( + "os" + "sync" + + "github.com/WowoEngine/SawitDB-Go/internal/utils" +) + +type DBEventHandler struct { + log *utils.Bulog + file *os.File + mu sync.Mutex +} + +func NewDBEventHandler(path string, bulog *utils.Bulog) (*DBEventHandler, error) { + if path != "" { + file, err := os.OpenFile( + path, + os.O_APPEND|os.O_CREATE|os.O_WRONLY, + 0644, + ) + if err != nil { + return nil, err + } + + return &DBEventHandler{ + log: bulog, + file: file, + }, err + } + + return &DBEventHandler{ + log: bulog, + file: nil, + }, nil +} + +func (w *DBEventHandler) write(aql string) { + w.mu.Lock() + defer w.mu.Unlock() + + line := aql + "\n" + + w.file.WriteString(line) +} + +func (w *DBEventHandler) Close() error { + return w.file.Close() +} +func (e *DBEventHandler) OnTableCreated(name string, index int, aql string) { + e.log.Kabarin(map[string]interface{}{"OnTableCreated": name, "index": index, "aql": aql}) + if e.file != nil { + e.write(aql) + } + +} +func (e *DBEventHandler) OnTableDropped(name string, index int, aql string) { + e.log.Kabarin(map[string]interface{}{"OnTableDropped": name, "index": index, "aql": aql}) + if e.file != nil { + e.write(aql) + } +} +func (e *DBEventHandler) OnTableSelected(name string, data []map[string]interface{}, aql string) { + e.log.Kabarin(map[string]interface{}{"OnTableSelected": name, "data": data, "aql": aql}) + if e.file != nil { + e.write(aql) + } +} +func (e *DBEventHandler) OnTableInserted(name string, data map[string]interface{}, aql string) { + e.log.Kabarin(map[string]interface{}{"OnTableInserted": name, "data": data, "aql": aql}) + if e.file != nil { + e.write(aql) + } +} +func (e *DBEventHandler) OnTableDeleted(name string, data []map[string]interface{}, aql string) { + e.log.Kabarin(map[string]interface{}{"OnTableDeleted": name, "data": data, "aql": aql}) + if e.file != nil { + e.write(aql) + } +} +func (e *DBEventHandler) OnTableUpdated(name string, data []map[string]interface{}, aql string) { + e.log.Kabarin(map[string]interface{}{"OnTableUpdated": name, "data": data, "aql": aql}) + if e.file != nil { + e.write(aql) + } +} diff --git a/internal/server/server.go b/internal/server/server.go index b8cd514..1cd9be5 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -13,6 +13,7 @@ import ( "time" "github.com/WowoEngine/SawitDB-Go/internal/engine" + "github.com/WowoEngine/SawitDB-Go/internal/entity" ) type Config struct { @@ -23,6 +24,8 @@ type Config struct { QueryTimeout time.Duration LogLevel string Auth map[string]string + Event engine.DBEvent + Options entity.EngineOption } type SawitServer struct { @@ -223,7 +226,7 @@ func (s *SawitServer) getOrCreateDatabase(name string) (*engine.SawitDB, error) } dbPath := filepath.Join(s.Config.DataDir, name+".sawit") - db, err := engine.NewSawitDB(dbPath) + db, err := engine.NewSawitDB(dbPath, s.Config.Event, &s.Config.Options) if err != nil { return nil, err } diff --git a/internal/utils/bulog.go b/internal/utils/bulog.go new file mode 100644 index 0000000..0535305 --- /dev/null +++ b/internal/utils/bulog.go @@ -0,0 +1,86 @@ +package utils + +import ( + "encoding/json" + "os" + "sync" + "time" + + "github.com/WowoEngine/SawitDB-Go/internal/constant" +) + +type LogEntry struct { + Timestamp string `json:"timestamp"` + Level constant.LogLevel `json:"level"` + Message interface{} `json:"message"` +} + +type Bulog struct { + file *os.File + mu sync.Mutex + isWriteToFile bool +} + +// NewLogger membuat logger JSON +func NewBulog(path string) (*Bulog, error) { + + filePath := path + write := false + if filePath != "" { + file, err := os.OpenFile( + filePath, + os.O_APPEND|os.O_CREATE|os.O_WRONLY, + 0666, + ) + if err == nil { + write = true + } + return &Bulog{ + file: file, + isWriteToFile: write, + }, nil + } + + return &Bulog{ + file: nil, + isWriteToFile: write, + }, nil +} + +func (l *Bulog) log(level constant.LogLevel, message interface{}) { + l.mu.Lock() + defer l.mu.Unlock() + + entry := LogEntry{ + Level: level, + Message: message, + Timestamp: time.Now().Format(time.RFC3339), + } + + jsonData, _ := json.Marshal(entry) + + // write to file + if l.isWriteToFile { + l.file.Write(jsonData) + l.file.Write([]byte("\n")) + } + + os.Stdout.Write(jsonData) + os.Stdout.Write([]byte("\n")) +} + +func (l *Bulog) Kabarin(msg interface{}) { + l.log(constant.LOG_INFO, msg) +} + +func (l *Bulog) Awas(msg interface{}) { + l.log(constant.LOG_WARN, msg) +} + +func (l *Bulog) Kacau(msg interface{}) { + l.log(constant.LOG_ERROR, msg) +} + +func (l *Bulog) Dolog(msg interface{}) { + l.log(constant.LOG_DEBUG, msg) +}