Initial working CDRC .fwv implementation

This commit is contained in:
DanB
2015-07-28 18:49:41 +02:00
parent 3fdf084902
commit 43c2575a04
10 changed files with 215 additions and 80 deletions

View File

@@ -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("<Cdrc> 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("<Cdrc> DryRun CDR: %+v", storedCdr))
continue
}
if err := self.cdrs.ProcessCdr(storedCdr, &reply); err != nil {
engine.Logger.Err(fmt.Sprintf("<Cdrc> Failed sending CDR, %+v, error: %s", storedCdr, err.Error()))
} else if reply != "OK" {

View File

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

View File

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

View File

@@ -17,3 +17,25 @@ along with this program. If not, see <http://www.gnu.org/licenses/>
*/
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)
}
}

View File

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

View File

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

View File

@@ -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. <internal|x.y.z.y:1234>
// "cdr_format": "csv", // CDR file format <csv|freeswitch_csv|fwv|opensips_flatstore>
// "field_separator": ",", // separator used in case of csv files

View File

@@ -25,6 +25,7 @@
"cdrc": {
"FWV1": {
"enabled": true, // enable CDR client functionality
"dry_run": true,
"cdrs": "internal", // address where to reach CDR server. <internal|x.y.z.y:1234>
"cdr_format": "fwv", // CDR file format <csv|freeswitch_csv|fwv|opensips_flatstore>
"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},

View File

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

View File

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