From 43c2575a0433bb42f600da0232f165cd0caa70e3 Mon Sep 17 00:00:00 2001 From: DanB Date: Tue, 28 Jul 2015 18:49:41 +0200 Subject: [PATCH] Initial working CDRC .fwv implementation --- cdrc/cdrc.go | 48 +++++---- cdrc/csv.go | 30 +++--- cdrc/fwv.go | 132 ++++++++++++++++++++----- cdrc/fwv_test.go | 22 +++++ config/config_json_test.go | 5 +- config/libconfig_json.go | 1 + data/conf/cgrates/cgrates.json | 1 + data/conf/samples/cdrcfwv/cgrates.json | 30 +++--- engine/storedcdr.go | 14 +++ engine/storedcdr_test.go | 12 +++ 10 files changed, 215 insertions(+), 80 deletions(-) diff --git a/cdrc/cdrc.go b/cdrc/cdrc.go index b5a7f704b..df8804938 100644 --- a/cdrc/cdrc.go +++ b/cdrc/cdrc.go @@ -46,23 +46,23 @@ func populateStoredCdrField(cdr *engine.StoredCdr, fieldId, fieldVal string) err var err error switch fieldId { case utils.TOR: - cdr.TOR = fieldVal + cdr.TOR += fieldVal case utils.ACCID: - cdr.AccId = fieldVal + cdr.AccId += fieldVal case utils.REQTYPE: - cdr.ReqType = fieldVal + cdr.ReqType += fieldVal case utils.DIRECTION: - cdr.Direction = fieldVal + cdr.Direction += fieldVal case utils.TENANT: - cdr.Tenant = fieldVal + cdr.Tenant += fieldVal case utils.CATEGORY: - cdr.Category = fieldVal + cdr.Category += fieldVal case utils.ACCOUNT: - cdr.Account = fieldVal + cdr.Account += fieldVal case utils.SUBJECT: - cdr.Subject = fieldVal + cdr.Subject += fieldVal case utils.DESTINATION: - cdr.Destination = fieldVal + cdr.Destination += fieldVal case utils.SETUP_TIME: if cdr.SetupTime, err = utils.ParseTimeDetectLayout(fieldVal); err != nil { return fmt.Errorf("Cannot parse answer time field with value: %s, err: %s", fieldVal, err.Error()) @@ -80,11 +80,11 @@ func populateStoredCdrField(cdr *engine.StoredCdr, fieldId, fieldVal string) err return fmt.Errorf("Cannot parse duration field with value: %s, err: %s", fieldVal, err.Error()) } case utils.SUPPLIER: - cdr.Supplier = fieldVal + cdr.Supplier += fieldVal case utils.DISCONNECT_CAUSE: - cdr.DisconnectCause = fieldVal + cdr.DisconnectCause += fieldVal default: // Extra fields will not match predefined so they all show up here - cdr.ExtraFields[fieldId] = fieldVal + cdr.ExtraFields[fieldId] += fieldVal } return nil } @@ -108,7 +108,7 @@ func NewCdrc(cdrcCfgs map[string]*config.CdrcConfig, httpSkipTlsCheck bool, cdrs } cdrc := &Cdrc{cdrFormat: cdrcCfg.CdrFormat, cdrInDir: cdrcCfg.CdrInDir, cdrOutDir: cdrcCfg.CdrOutDir, runDelay: cdrcCfg.RunDelay, csvSep: cdrcCfg.FieldSeparator, - httpSkipTlsCheck: httpSkipTlsCheck, cdrcCfgs: cdrcCfgs, cdrs: cdrs, exitChan: exitChan, maxOpenFiles: make(chan struct{}, cdrcCfg.MaxOpenFiles), + httpSkipTlsCheck: httpSkipTlsCheck, cdrcCfgs: cdrcCfgs, dfltCdrcCfg: cdrcCfg, cdrs: cdrs, exitChan: exitChan, maxOpenFiles: make(chan struct{}, cdrcCfg.MaxOpenFiles), } var processFile struct{} for i := 0; i < cdrcCfg.MaxOpenFiles; i++ { @@ -156,6 +156,7 @@ type Cdrc struct { cdrFields [][]*config.CfgCdrField // Profiles directly connected with cdrFilters httpSkipTlsCheck bool cdrcCfgs map[string]*config.CdrcConfig // All cdrc config profiles attached to this CDRC (key will be profile instance name) + dfltCdrcCfg *config.CdrcConfig cdrs engine.Connector httpClient *http.Client exitChan chan struct{} @@ -245,28 +246,35 @@ func (self *Cdrc) processFile(filePath string) error { return err } var recordsProcessor RecordsProcessor - if utils.IsSliceMember([]string{CSV, FS_CSV, utils.KAM_FLATSTORE, utils.OSIPS_FLATSTORE}, self.cdrFormat) { + switch self.cdrFormat { + case CSV, FS_CSV, utils.KAM_FLATSTORE, utils.OSIPS_FLATSTORE: csvReader := csv.NewReader(bufio.NewReader(file)) csvReader.Comma = self.csvSep recordsProcessor = NewCsvRecordsProcessor(csvReader, self.cdrFormat, fn, self.failedCallsPrefix, self.cdrSourceIds, self.duMultiplyFactors, self.cdrFilters, self.cdrFields, self.httpSkipTlsCheck, self.partialRecordsCache) - } else if self.cdrFormat == utils.FWV { - recordsProcessor = NewFwvRecordsProcessor(file, self.cdrcCfgs) + case utils.FWV: + recordsProcessor = NewFwvRecordsProcessor(file, self.cdrcCfgs, self.dfltCdrcCfg, self.httpClient, self.httpSkipTlsCheck) + default: + return fmt.Errorf("Unsupported CDR format: %s", self.cdrFormat) } procRowNr := 0 timeStart := time.Now() for { cdrs, err := recordsProcessor.ProcessNextRecord() + if err != nil && err == io.EOF { + break + } + procRowNr += 1 if err != nil { - if err == io.EOF { - break - } engine.Logger.Err(fmt.Sprintf(" Row %d, error: %s", procRowNr, err.Error())) continue } - procRowNr += 1 for _, storedCdr := range cdrs { // Send CDRs to CDRS var reply string + if self.dfltCdrcCfg.DryRun { + engine.Logger.Info(fmt.Sprintf(" DryRun CDR: %+v", storedCdr)) + continue + } if err := self.cdrs.ProcessCdr(storedCdr, &reply); err != nil { engine.Logger.Err(fmt.Sprintf(" Failed sending CDR, %+v, error: %s", storedCdr, err.Error())) } else if reply != "OK" { diff --git a/cdrc/csv.go b/cdrc/csv.go index 88a6548e7..95df1d3a6 100644 --- a/cdrc/csv.go +++ b/cdrc/csv.go @@ -285,26 +285,22 @@ func (self *CsvRecordsProcessor) recordToStoredCdr(record []string, cfgIdx int) } var fieldVal string - if utils.IsSliceMember([]string{CSV, FS_CSV, utils.KAM_FLATSTORE, utils.OSIPS_FLATSTORE}, self.cdrFormat) { - if cdrFldCfg.Type == utils.CDRFIELD { - for _, cfgFieldRSR := range cdrFldCfg.Value { - if cfgFieldRSR.IsStatic() { - fieldVal += cfgFieldRSR.ParseValue("") - } else { // Dynamic value extracted using index - if cfgFieldIdx, _ := strconv.Atoi(cfgFieldRSR.Id); len(record) <= cfgFieldIdx { - return nil, fmt.Errorf("Ignoring record: %v - cannot extract field %s", record, cdrFldCfg.Tag) - } else { - fieldVal += cfgFieldRSR.ParseValue(record[cfgFieldIdx]) - } + if cdrFldCfg.Type == utils.CDRFIELD { + for _, cfgFieldRSR := range cdrFldCfg.Value { + if cfgFieldRSR.IsStatic() { + fieldVal += cfgFieldRSR.ParseValue("") + } else { // Dynamic value extracted using index + if cfgFieldIdx, _ := strconv.Atoi(cfgFieldRSR.Id); len(record) <= cfgFieldIdx { + return nil, fmt.Errorf("Ignoring record: %v - cannot extract field %s", record, cdrFldCfg.Tag) + } else { + fieldVal += cfgFieldRSR.ParseValue(record[cfgFieldIdx]) } } - } else if cdrFldCfg.Type == utils.HTTP_POST { - lazyHttpFields = append(lazyHttpFields, cdrFldCfg) // Will process later so we can send an estimation of storedCdr to http server - } else { - return nil, fmt.Errorf("Unsupported field type: %s", cdrFldCfg.Type) } - } else { // Modify here when we add more supported cdr formats - return nil, fmt.Errorf("Unsupported CDR file format: %s", self.cdrFormat) + } else if cdrFldCfg.Type == utils.HTTP_POST { + lazyHttpFields = append(lazyHttpFields, cdrFldCfg) // Will process later so we can send an estimation of storedCdr to http server + } else { + return nil, fmt.Errorf("Unsupported field type: %s", cdrFldCfg.Type) } if err := populateStoredCdrField(storedCdr, cdrFldCfg.CdrFieldId, fieldVal); err != nil { return nil, err diff --git a/cdrc/fwv.go b/cdrc/fwv.go index b30e6bf90..8ec7c2c31 100644 --- a/cdrc/fwv.go +++ b/cdrc/fwv.go @@ -23,37 +23,44 @@ import ( "fmt" "github.com/cgrates/cgrates/config" "github.com/cgrates/cgrates/engine" + "github.com/cgrates/cgrates/utils" "io" + "net/http" "os" + "strconv" + "strings" + "time" ) -/*file, _ := os.Open(path.Join("/tmp", "acc_1.log")) -defer file.Close() -fs, _ := file.Stat() -fmt.Printf("FileSize: %d, content size: %d, %q", fs.Size(), len([]byte(fullSuccessfull)), fullSuccessfull) -buf := make([]byte, 109) -_, err := file.ReadAt(buf, fs.Size()-int64(len(buf))) -if err != nil { - t.Error(err) -} -fmt.Printf("Have read in buffer: <%q>, len: %d", string(buf), len(string(buf))) -*/ - -func NewFwvRecordsProcessor(file *os.File, cdrcCfgs map[string]*config.CdrcConfig) *FwvRecordsProcessor { - frp := &FwvRecordsProcessor{file: file, cdrcCfgs: cdrcCfgs} - for _, frp.dfltCfg = range cdrcCfgs { // Set the first available instance to be used for common parameters - break +func fwvValue(cdrLine string, indexStart, width int, padding string) string { + rawVal := cdrLine[indexStart : indexStart+width] + switch padding { + case "left": + rawVal = strings.TrimLeft(rawVal, " ") + case "right": + rawVal = strings.TrimRight(rawVal, " ") + case "zeroleft": + rawVal = strings.TrimLeft(rawVal, "0 ") + case "zeroright": + rawVal = strings.TrimRight(rawVal, "0 ") } - return frp + return rawVal +} + +func NewFwvRecordsProcessor(file *os.File, cdrcCfgs map[string]*config.CdrcConfig, dfltCfg *config.CdrcConfig, httpClient *http.Client, httpSkipTlsCheck bool) *FwvRecordsProcessor { + return &FwvRecordsProcessor{file: file, cdrcCfgs: cdrcCfgs, dfltCfg: dfltCfg, httpSkipTlsCheck: httpSkipTlsCheck} } type FwvRecordsProcessor struct { - file *os.File - cdrcCfgs map[string]*config.CdrcConfig - dfltCfg *config.CdrcConfig // General parameters - lineLen int64 // Length of the line in the file - offset int64 // Index of the next byte to process - trailerOffset int64 // Index where trailer starts, to be used as boundary when reading cdrs + file *os.File + cdrcCfgs map[string]*config.CdrcConfig + dfltCfg *config.CdrcConfig // General parameters + httpClient *http.Client + httpSkipTlsCheck bool + lineLen int64 // Length of the line in the file + offset int64 // Index of the next byte to process + trailerOffset int64 // Index where trailer starts, to be used as boundary when reading cdrs + headerCdr *engine.StoredCdr // Cache here the general purpose stored CDR } // Sets the line length based on first line, sets offset back to initial after reading @@ -116,16 +123,84 @@ func (self *FwvRecordsProcessor) ProcessNextRecord() ([]*engine.StoredCdr, error } if storedCdr, err := self.recordToStoredCdr(string(buf), cfgKey); err != nil { return nil, fmt.Errorf("Failed converting to StoredCdr, error: %s", err.Error()) - } else if storedCdr != nil { + } else { recordCdrs = append(recordCdrs, storedCdr) } } return recordCdrs, nil } +// Converts a record (header or normal) to StoredCdr func (self *FwvRecordsProcessor) recordToStoredCdr(record string, cfgKey string) (*engine.StoredCdr, error) { - //engine.Logger.Debug(fmt.Sprintf("RecordToStoredCdr: <%q>, cfgKey: %s, offset: %d, trailerOffset: %d, lineLen: %d", record, cfgKey, self.offset, self.trailerOffset, self.lineLen)) - return nil, nil + var err error + var lazyHttpFields []*config.CfgCdrField + var cfgFields []*config.CfgCdrField + var duMultiplyFactor float64 + var storedCdr *engine.StoredCdr + if self.headerCdr != nil { // Clone the header CDR so we can use it as base to future processing (inherit fields defined there) + storedCdr = self.headerCdr.Clone() + } else { + storedCdr = &engine.StoredCdr{CdrHost: "0.0.0.0", ExtraFields: make(map[string]string), Cost: -1} + } + if cfgKey == "*header" { + cfgFields = self.dfltCfg.HeaderFields + storedCdr.CdrSource = self.dfltCfg.CdrSourceId + duMultiplyFactor = self.dfltCfg.DataUsageMultiplyFactor + } else { + cfgFields = self.cdrcCfgs[cfgKey].ContentFields + storedCdr.CdrSource = self.cdrcCfgs[cfgKey].CdrSourceId + duMultiplyFactor = self.cdrcCfgs[cfgKey].DataUsageMultiplyFactor + } + for _, cdrFldCfg := range cfgFields { + var fieldVal string + switch cdrFldCfg.Type { + case utils.CDRFIELD: + for _, cfgFieldRSR := range cdrFldCfg.Value { + if cfgFieldRSR.IsStatic() { + fieldVal += cfgFieldRSR.ParseValue("") + } else { // Dynamic value extracted using index + if cfgFieldIdx, _ := strconv.Atoi(cfgFieldRSR.Id); len(record) <= cfgFieldIdx { + return nil, fmt.Errorf("Ignoring record: %v - cannot extract field %s", record, cdrFldCfg.Tag) + } else { + fieldVal += cfgFieldRSR.ParseValue(fwvValue(record, cfgFieldIdx, cdrFldCfg.Width, cdrFldCfg.Padding)) + } + } + } + case utils.HTTP_POST: + lazyHttpFields = append(lazyHttpFields, cdrFldCfg) // Will process later so we can send an estimation of storedCdr to http server + default: + //return nil, fmt.Errorf("Unsupported field type: %s", cdrFldCfg.Type) + continue // Don't do anything for unsupported fields + } + if err := populateStoredCdrField(storedCdr, cdrFldCfg.CdrFieldId, fieldVal); err != nil { + return nil, err + } + } + if storedCdr.CgrId == "" && storedCdr.AccId != "" && cfgKey != "*header" { + storedCdr.CgrId = utils.Sha1(storedCdr.AccId, storedCdr.SetupTime.String()) + } + if storedCdr.TOR == utils.DATA && duMultiplyFactor != 0 { + storedCdr.Usage = time.Duration(float64(storedCdr.Usage.Nanoseconds()) * duMultiplyFactor) + } + for _, httpFieldCfg := range lazyHttpFields { // Lazy process the http fields + var outValByte []byte + var fieldVal, httpAddr string + for _, rsrFld := range httpFieldCfg.Value { + httpAddr += rsrFld.ParseValue("") + } + if outValByte, err = utils.HttpJsonPost(httpAddr, self.httpSkipTlsCheck, storedCdr); err != nil && httpFieldCfg.Mandatory { + return nil, err + } else { + fieldVal = string(outValByte) + if len(fieldVal) == 0 && httpFieldCfg.Mandatory { + return nil, fmt.Errorf("MandatoryIeMissing: Empty result for http_post field: %s", httpFieldCfg.Tag) + } + if err := populateStoredCdrField(storedCdr, httpFieldCfg.CdrFieldId, fieldVal); err != nil { + return nil, err + } + } + } + return storedCdr, nil } func (self *FwvRecordsProcessor) processHeader() error { @@ -135,7 +210,10 @@ func (self *FwvRecordsProcessor) processHeader() error { } else if nRead != len(buf) { return fmt.Errorf("In header, line len: %d, have read: %d", self.lineLen, nRead) } - //engine.Logger.Debug(fmt.Sprintf("Have read header: <%q>", string(buf))) + var err error + if self.headerCdr, err = self.recordToStoredCdr(string(buf), "*header"); err != nil { + return err + } return nil } diff --git a/cdrc/fwv_test.go b/cdrc/fwv_test.go index 5c5d65648..3d4d9cd16 100644 --- a/cdrc/fwv_test.go +++ b/cdrc/fwv_test.go @@ -17,3 +17,25 @@ along with this program. If not, see */ package cdrc + +import ( + "testing" +) + +func TestFwvValue(t *testing.T) { + cdrLine := "CDR0000010 0 20120708181506000123451234 0040123123120 004 000018009980010001ISDN ABC 10Buiten uw regio EHV 00000009190000000009" + if val := fwvValue(cdrLine, 30, 19, "right"); val != "0123451234" { + t.Errorf("Received: <%s>", val) + } + if val := fwvValue(cdrLine, 14, 16, "right"); val != "2012070818150600" { // SetupTime + t.Errorf("Received: <%s>", val) + } + if val := fwvValue(cdrLine, 127, 8, "right"); val != "00001800" { // Usage + t.Errorf("Received: <%s>", val) + } + cdrLine = "HDR0001DDB ABC Some Connect A.B. DDB-Some-10022-20120711-309.CDR 00030920120711100255 " + if val := fwvValue(cdrLine, 135, 6, "zeroleft"); val != "309" { + t.Errorf("Received: <%s>", val) + } + +} diff --git a/config/config_json_test.go b/config/config_json_test.go index 1a5741fd3..02869c4c7 100644 --- a/config/config_json_test.go +++ b/config/config_json_test.go @@ -101,8 +101,8 @@ func TestDfDbJsonCfg(t *testing.T) { Db_name: utils.StringPointer("cgrates"), Db_user: utils.StringPointer("cgrates"), Db_passwd: utils.StringPointer("CGRateS.org"), - Max_open_conns: utils.IntPointer(0), - Max_idle_conns: utils.IntPointer(-1), + Max_open_conns: utils.IntPointer(100), + Max_idle_conns: utils.IntPointer(10), } if cfg, err := dfCgrJsonCfg.DbJsonCfg(STORDB_JSN); err != nil { t.Error(err) @@ -289,6 +289,7 @@ func TestDfCdrcJsonCfg(t *testing.T) { eCfg := map[string]*CdrcJsonCfg{ "*default": &CdrcJsonCfg{ Enabled: utils.BoolPointer(false), + Dry_run: utils.BoolPointer(false), Cdrs: utils.StringPointer("internal"), Cdr_format: utils.StringPointer("csv"), Field_separator: utils.StringPointer(","), diff --git a/config/libconfig_json.go b/config/libconfig_json.go index f4477f451..3ca603aef 100644 --- a/config/libconfig_json.go +++ b/config/libconfig_json.go @@ -128,6 +128,7 @@ type CdreJsonCfg struct { // Cdrc config section type CdrcJsonCfg struct { Enabled *bool + Dry_run *bool Cdrs *string Cdr_format *string Field_separator *string diff --git a/data/conf/cgrates/cgrates.json b/data/conf/cgrates/cgrates.json index 5781bdaf1..4971943a5 100644 --- a/data/conf/cgrates/cgrates.json +++ b/data/conf/cgrates/cgrates.json @@ -136,6 +136,7 @@ //"cdrc": { // "*default": { // "enabled": false, // enable CDR client functionality +// "dry_run": false, // do not send the CDRs to CDRS, just parse them // "cdrs": "internal", // address where to reach CDR server. // "cdr_format": "csv", // CDR file format // "field_separator": ",", // separator used in case of csv files diff --git a/data/conf/samples/cdrcfwv/cgrates.json b/data/conf/samples/cdrcfwv/cgrates.json index da26848bc..c9b30158e 100644 --- a/data/conf/samples/cdrcfwv/cgrates.json +++ b/data/conf/samples/cdrcfwv/cgrates.json @@ -25,6 +25,7 @@ "cdrc": { "FWV1": { "enabled": true, // enable CDR client functionality + "dry_run": true, "cdrs": "internal", // address where to reach CDR server. "cdr_format": "fwv", // CDR file format "cdr_in_dir": "/tmp/cgr_fwv/cdrc/in", // absolute path towards the directory where the CDRs are stored @@ -32,26 +33,27 @@ "cdr_source_id": "fwv_localtest", // free form field, tag identifying the source of the CDRs within CDRS database "cdr_filter": "", // filter CDR records to import "header_fields": [ - {"tag": "FileName", "cdr_field_id": "CdrFileName", "type": "cdrfield", "value": "96", "width": 40}, - {"tag": "FileSeqNr", "cdr_field_id": "FileSeqNr", "type": "cdrfield", "value": "136", "width": 6}, + {"tag": "FileName", "cdr_field_id": "CdrFileName", "type": "cdrfield", "value": "95", "width": 40, "padding":"right"}, + {"tag": "FileSeqNr", "cdr_field_id": "FileSeqNr", "type": "cdrfield", "value": "135", "width": 6, "padding":"zeroleft"}, + {"tag": "AccId1", "cdr_field_id": "accid", "type": "cdrfield", "value": "135", "width": 6, "padding":"zeroleft"}, ], "content_fields": [ // import template, tag will match internally CDR field, in case of .csv value will be represented by index of the field value {"tag": "Tor", "cdr_field_id": "tor", "type": "cdrfield", "value": "^*voice", "mandatory": true}, - {"tag": "AccId1", "cdr_field_id": "accid", "type": "cdrfield", "value": "4", "width": 3, "mandatory": true}, - {"tag": "AccId2", "cdr_field_id": "accid", "type": "cdrfield", "value": "15", "width": 16, "mandatory": true}, - {"tag": "ReqType", "cdr_field_id": "reqtype", "type": "cdrfield", "value": "7", "mandatory": true}, + {"tag": "AccId1", "cdr_field_id": "accid", "type": "cdrfield", "value": "3", "width": 3, "padding":"zeroleft", "mandatory": true}, + {"tag": "AccId2", "cdr_field_id": "accid", "type": "cdrfield", "value": "14", "width": 16, "padding":"right", "mandatory": true}, + {"tag": "ReqType", "cdr_field_id": "reqtype", "type": "cdrfield", "value": "^rated", "mandatory": true}, {"tag": "Direction", "cdr_field_id": "direction", "type": "cdrfield", "value": "^*out", "mandatory": true}, {"tag": "Tenant", "cdr_field_id": "tenant", "type": "cdrfield", "value": "^cgrates.org", "mandatory": true}, {"tag": "Category", "cdr_field_id": "category", "type": "cdrfield", "value": "^call", "mandatory": true}, - {"tag": "Account", "cdr_field_id": "account", "type": "cdrfield", "value": "31", "width": 19, "mandatory": true}, - {"tag": "Subject", "cdr_field_id": "subject", "type": "cdrfield", "value": "31", "width": 19, "mandatory": true}, - {"tag": "Destination", "cdr_field_id": "destination", "type": "cdrfield", "value": "53", "width": 28, "mandatory": true}, - {"tag": "SetupTime", "cdr_field_id": "setup_time", "type": "cdrfield", "value": "15", "width": 16, "mandatory": true}, - {"tag": "AnswerTime", "cdr_field_id": "answer_time", "type": "cdrfield", "value": "15", "width": 16, "mandatory": true}, - {"tag": "Usage", "cdr_field_id": "usage", "type": "cdrfield", "value": "128", "width": 8, "mandatory": true}, - {"tag": "DisconnectCause", "cdr_field_id": "disconnect_cause", "type": "cdrfield", "value": "139", "width": 1, "mandatory": true}, - {"tag": "RetailAmount", "cdr_field_id": "RetailAmount", "type": "cdrfield", "value": "204", "width": 8}, - {"tag": "WholesaleAmount", "cdr_field_id": "RetailAmount", "type": "cdrfield", "value": "216", "width": 8}, + {"tag": "Account", "cdr_field_id": "account", "type": "cdrfield", "value": "30", "width": 19, "padding":"right", "mandatory": true}, + {"tag": "Subject", "cdr_field_id": "subject", "type": "cdrfield", "value": "30", "width": 19, "padding":"right", "mandatory": true}, + {"tag": "Destination", "cdr_field_id": "destination", "type": "cdrfield", "value": "52", "width": 28, "padding":"right", "mandatory": true}, + {"tag": "SetupTime", "cdr_field_id": "setup_time", "type": "cdrfield", "value": "~14:s/(\\d{4})(\\d{2})(\\d{2})(\\d{2})(\\d{2})(\\d{2})(\\d{2})/${1}-${2}-${3} ${4}:${5}:${6}/", "width": 16, "mandatory": true}, + {"tag": "AnswerTime", "cdr_field_id": "answer_time", "type": "cdrfield", "value": "~14:s/(\\d{4})(\\d{2})(\\d{2})(\\d{2})(\\d{2})(\\d{2})(\\d{2})/${1}-${2}-${3} ${4}:${5}:${6}/", "width": 16, "mandatory": true}, + {"tag": "Usage", "cdr_field_id": "usage", "type": "cdrfield", "value": "~127:s/(\\d{2})(\\d{2})(\\d{2})(\\d{2})/${1}h${2}m${3}s/", "width": 8, "mandatory": true}, + {"tag": "DisconnectCause", "cdr_field_id": "disconnect_cause", "type": "cdrfield", "value": "138", "width": 1, "mandatory": true}, + {"tag": "RetailAmount", "cdr_field_id": "RetailAmount", "type": "cdrfield", "value": "203", "padding":"zeroleft", "width": 8}, + {"tag": "WholesaleAmount", "cdr_field_id": "RetailAmount", "type": "cdrfield", "value": "215", "padding":"zeroleft", "width": 8}, ], "trailer_fields": [ {"tag": "NrOfCdrs", "type": "metatag", "metatag_id":"total_cdrs", "value": "142", "width": 8}, diff --git a/engine/storedcdr.go b/engine/storedcdr.go index ad7c17e73..700d56295 100644 --- a/engine/storedcdr.go +++ b/engine/storedcdr.go @@ -207,6 +207,20 @@ func (storedCdr *StoredCdr) AsStoredCdr() *StoredCdr { return storedCdr } +func (storedCdr *StoredCdr) Clone() *StoredCdr { + clnCdr := *storedCdr + clnCdr.ExtraFields = make(map[string]string) + clnCdr.CostDetails = nil // Clean old reference + for k, v := range storedCdr.ExtraFields { + clnCdr.ExtraFields[k] = v + } + if storedCdr.CostDetails != nil { + cDetails := *storedCdr.CostDetails + clnCdr.CostDetails = &cDetails + } + return &clnCdr +} + // Ability to send the CgrCdr remotely to another CDR server, we do not include rating variables for now func (storedCdr *StoredCdr) AsHttpForm() url.Values { v := url.Values{} diff --git a/engine/storedcdr_test.go b/engine/storedcdr_test.go index 3163f851a..a8afb1e9d 100644 --- a/engine/storedcdr_test.go +++ b/engine/storedcdr_test.go @@ -52,6 +52,18 @@ func TestNewStoredCdrFromExternalCdr(t *testing.T) { } } +func TestStoredCdrClone(t *testing.T) { + storCdr := &StoredCdr{CgrId: utils.Sha1("dsafdsaf", time.Date(2013, 11, 7, 8, 42, 20, 0, time.UTC).String()), OrderId: 123, TOR: utils.VOICE, + AccId: "dsafdsaf", CdrHost: "192.168.1.1", CdrSource: utils.UNIT_TEST, ReqType: utils.META_RATED, Direction: "*out", + Tenant: "cgrates.org", Category: "call", Account: "1001", Subject: "1001", Destination: "1002", Supplier: "SUPPL1", + SetupTime: time.Date(2013, 11, 7, 8, 42, 20, 0, time.UTC), AnswerTime: time.Date(2013, 11, 7, 8, 42, 26, 0, time.UTC), MediationRunId: utils.DEFAULT_RUNID, + Usage: time.Duration(10), Pdd: time.Duration(7) * time.Second, ExtraFields: map[string]string{"field_extr1": "val_extr1", "fieldextr2": "valextr2"}, Cost: 1.01, RatedAccount: "dan", RatedSubject: "dans", Rated: true, + } + if clnStorCdr := storCdr.Clone(); !reflect.DeepEqual(storCdr, clnStorCdr) { + t.Errorf("Expecting: %+v, received: %+v", storCdr, clnStorCdr) + } +} + func TestFieldAsString(t *testing.T) { cdr := StoredCdr{CgrId: utils.Sha1("dsafdsaf", time.Date(2013, 11, 7, 8, 42, 26, 0, time.UTC).String()), OrderId: 123, TOR: utils.VOICE, AccId: "dsafdsaf", CdrHost: "192.168.1.1", CdrSource: "test", ReqType: utils.META_RATED, Direction: "*out", Tenant: "cgrates.org",