From a449194b625953ce4ea67d5747f645f6f585cfb0 Mon Sep 17 00:00:00 2001 From: DanB Date: Wed, 16 Jul 2014 10:56:56 +0200 Subject: [PATCH] CdrExporter with configurable field separator, counters for sms and data usage --- apier/cdre.go | 6 +++++- cdre/cdrexporter.go | 38 ++++++++++++++++++++++++++++++-------- cdre/cdrexporter_test.go | 4 ++-- cdre/csv_test.go | 30 +++++++++++++++++++++++++++++- cdre/fixedwidth_test.go | 18 +++++++++++------- config/cdreconfig.go | 2 ++ config/cdreconfig_test.go | 1 + config/config_test.go | 3 ++- config/xmlcdrc.go | 16 ++++++++-------- config/xmlcdre.go | 5 +++++ config/xmlcdre_test.go | 2 ++ utils/apitpdata.go | 1 + 12 files changed, 98 insertions(+), 28 deletions(-) diff --git a/apier/cdre.go b/apier/cdre.go index 3628aa67d..f1f63933d 100644 --- a/apier/cdre.go +++ b/apier/cdre.go @@ -77,6 +77,10 @@ func (self *ApierV1) ExportCdrsToFile(attr utils.AttrExpFileCdrs, reply *utils.E if !utils.IsSliceMember(utils.CdreCdrFormats, cdrFormat) { return fmt.Errorf("%s:%s", utils.ERR_MANDATORY_IE_MISSING, "CdrFormat") } + fieldSep := exportTemplate.FieldSeparator + if attr.FieldSeparator != nil { + fieldSep = *attr.FieldSeparator + } exportDir := exportTemplate.ExportDir if attr.ExportDir != nil { exportDir = *attr.ExportDir @@ -125,7 +129,7 @@ func (self *ApierV1) ExportCdrsToFile(attr utils.AttrExpFileCdrs, reply *utils.E *reply = utils.ExportedFileCdrs{ExportedFilePath: ""} return nil } - cdrexp, err := cdre.NewCdrExporter(cdrs, self.LogDb, exportTemplate, cdrFormat, exportId, + cdrexp, err := cdre.NewCdrExporter(cdrs, self.LogDb, exportTemplate, cdrFormat, fieldSep, exportId, dataUsageMultiplyFactor, costMultiplyFactor, costShiftDigits, roundingDecimals, self.Config.RoundingDecimals, maskDestId, maskLen, self.Config.HttpSkipTlsVerify) if err != nil { return fmt.Errorf("%s:%s", utils.ERR_SERVER_ERROR, err.Error()) diff --git a/cdre/cdrexporter.go b/cdre/cdrexporter.go index 842bb7deb..4a45008c2 100644 --- a/cdre/cdrexporter.go +++ b/cdre/cdrexporter.go @@ -47,6 +47,8 @@ const ( META_LASTCDRATIME = "last_cdr_atime" META_NRCDRS = "cdrs_number" META_DURCDRS = "cdrs_duration" + META_SMSUSAGE = "sms_usage" + META_DATAUSAGE = "data_usage" META_COSTCDRS = "cdrs_cost" META_MASKDESTINATION = "mask_destination" META_FORMATCOST = "format_cost" @@ -54,7 +56,7 @@ const ( var err error -func NewCdrExporter(cdrs []*utils.StoredCdr, logDb engine.LogStorage, exportTpl *config.CdreConfig, cdrFormat, exportId string, +func NewCdrExporter(cdrs []*utils.StoredCdr, logDb engine.LogStorage, exportTpl *config.CdreConfig, cdrFormat string, fieldSeparator rune, exportId string, dataUsageMultiplyFactor, costMultiplyFactor float64, costShiftDigits, roundDecimals, cgrPrecision int, maskDestId string, maskLen int, httpSkipTlsCheck bool) (*CdrExporter, error) { if len(cdrs) == 0 { // Nothing to export return nil, nil @@ -64,6 +66,7 @@ func NewCdrExporter(cdrs []*utils.StoredCdr, logDb engine.LogStorage, exportTpl logDb: logDb, exportTemplate: exportTpl, cdrFormat: cdrFormat, + fieldSeparator: fieldSeparator, exportId: exportId, dataUsageMultiplyFactor: dataUsageMultiplyFactor, costMultiplyFactor: costMultiplyFactor, @@ -86,6 +89,7 @@ type CdrExporter struct { logDb engine.LogStorage // Used to extract cost_details if these are requested exportTemplate *config.CdreConfig cdrFormat string // csv, fwv + fieldSeparator rune exportId string // Unique identifier or this export dataUsageMultiplyFactor, costMultiplyFactor float64 costShiftDigits, roundDecimals, cgrPrecision int @@ -96,11 +100,12 @@ type CdrExporter struct { content [][]string // Rows of cdr fields firstCdrATime, lastCdrATime time.Time numberOfRecords int - totalDuration time.Duration - totalCost float64 - firstExpOrderId, lastExpOrderId int64 - positiveExports []string // CGRIds of successfully exported CDRs - negativeExports map[string]string // CgrIds of failed exports + totalDuration, totalDataUsage, totalSmsUsage time.Duration + + totalCost float64 + firstExpOrderId, lastExpOrderId int64 + positiveExports []string // CGRIds of successfully exported CDRs + negativeExports map[string]string // CgrIds of failed exports } // Return Json marshaled callCost attached to @@ -207,7 +212,17 @@ func (cdre *CdrExporter) metaHandler(tag, arg string) (string, error) { case META_NRCDRS: return strconv.Itoa(cdre.numberOfRecords), nil case META_DURCDRS: - return strconv.FormatFloat(cdre.totalDuration.Seconds(), 'f', -1, 64), nil + //return strconv.FormatFloat(cdre.totalDuration.Seconds(), 'f', -1, 64), nil + emulatedCdr := &utils.StoredCdr{TOR: utils.VOICE, Usage: cdre.totalDuration} + return emulatedCdr.FormatUsage(arg), nil + case META_SMSUSAGE: + //return strconv.FormatFloat(cdre.totalDuration.Seconds(), 'f', -1, 64), nil + emulatedCdr := &utils.StoredCdr{TOR: utils.SMS, Usage: cdre.totalSmsUsage} + return emulatedCdr.FormatUsage(arg), nil + case META_DATAUSAGE: + //return strconv.FormatFloat(cdre.totalDuration.Seconds(), 'f', -1, 64), nil + emulatedCdr := &utils.StoredCdr{TOR: utils.DATA, Usage: cdre.totalDataUsage} + return emulatedCdr.FormatUsage(arg), nil case META_COSTCDRS: return strconv.FormatFloat(utils.Round(cdre.totalCost, cdre.roundDecimals, utils.ROUNDING_MIDDLE), 'f', -1, 64), nil case META_MASKDESTINATION: @@ -353,9 +368,15 @@ func (cdre *CdrExporter) processCdr(cdr *utils.StoredCdr) error { cdre.lastCdrATime = cdr.AnswerTime } cdre.numberOfRecords += 1 - if !utils.IsSliceMember([]string{utils.DATA, utils.SMS}, cdr.TOR) { // Only count duration for non data cdrs + if cdr.TOR == utils.VOICE { // Only count duration for non data cdrs cdre.totalDuration += cdr.Usage } + if cdr.TOR == utils.SMS { // Count usage for SMS + cdre.totalSmsUsage += cdr.Usage + } + if cdr.TOR == utils.DATA { // Count usage for SMS + cdre.totalDataUsage += cdr.Usage + } if cdr.Cost != -1 { cdre.totalCost += cdr.Cost cdre.totalCost = utils.Round(cdre.totalCost, cdre.roundDecimals, utils.ROUNDING_MIDDLE) @@ -420,6 +441,7 @@ func (cdre *CdrExporter) writeOut(ioWriter io.Writer) error { // csvWriter specific method func (cdre *CdrExporter) writeCsv(csvWriter *csv.Writer) error { + csvWriter.Comma = cdre.fieldSeparator if len(cdre.header) != 0 { if err := csvWriter.Write(cdre.header); err != nil { return err diff --git a/cdre/cdrexporter_test.go b/cdre/cdrexporter_test.go index 31bb06eae..ed2c1afef 100644 --- a/cdre/cdrexporter_test.go +++ b/cdre/cdrexporter_test.go @@ -52,8 +52,8 @@ func TestCdreGetCombimedCdrFieldVal(t *testing.T) { Usage: time.Duration(10) * time.Second, MediationRunId: "RETAIL1", Cost: 5.01}, } - cdre, err := NewCdrExporter(cdrs, logDb, cfg.CdreDefaultInstance, cfg.CdreDefaultInstance.CdrFormat, "firstexport", 0.0, 0.0, 0, 4, - cfg.RoundingDecimals, "", 0, cfg.HttpSkipTlsVerify) + cdre, err := NewCdrExporter(cdrs, logDb, cfg.CdreDefaultInstance, cfg.CdreDefaultInstance.CdrFormat, cfg.CdreDefaultInstance.FieldSeparator, + "firstexport", 0.0, 0.0, 0, 4, cfg.RoundingDecimals, "", 0, cfg.HttpSkipTlsVerify) if err != nil { t.Error("Unexpected error received: ", err) } diff --git a/cdre/csv_test.go b/cdre/csv_test.go index 47f1317d3..257198cf2 100644 --- a/cdre/csv_test.go +++ b/cdre/csv_test.go @@ -39,7 +39,7 @@ func TestCsvCdrWriter(t *testing.T) { Usage: time.Duration(10) * time.Second, MediationRunId: utils.DEFAULT_RUNID, ExtraFields: map[string]string{"extra1": "val_extra1", "extra2": "val_extra2", "extra3": "val_extra3"}, Cost: 1.01, } - cdre, err := NewCdrExporter([]*utils.StoredCdr{storedCdr1}, logDb, cfg.CdreDefaultInstance, utils.CSV, "firstexport", 0.0, 0.0, 0, 4, cfg.RoundingDecimals, "", 0, cfg.HttpSkipTlsVerify) + cdre, err := NewCdrExporter([]*utils.StoredCdr{storedCdr1}, logDb, cfg.CdreDefaultInstance, utils.CSV, ',', "firstexport", 0.0, 0.0, 0, 4, cfg.RoundingDecimals, "", 0, cfg.HttpSkipTlsVerify) if err != nil { t.Error("Unexpected error received: ", err) } @@ -56,3 +56,31 @@ func TestCsvCdrWriter(t *testing.T) { t.Error("Unexpected TotalCost: ", cdre.TotalCost()) } } + +func TestAlternativeFieldSeparator(t *testing.T) { + writer := &bytes.Buffer{} + cfg, _ := config.NewDefaultCGRConfig() + logDb, _ := engine.NewMapStorage() + storedCdr1 := &utils.StoredCdr{CgrId: utils.Sha1("dsafdsaf", time.Unix(1383813745, 0).UTC().String()), TOR: utils.VOICE, AccId: "dsafdsaf", CdrHost: "192.168.1.1", + ReqType: "rated", Direction: "*out", Tenant: "cgrates.org", + Category: "call", Account: "1001", Subject: "1001", Destination: "1002", SetupTime: time.Unix(1383813745, 0).UTC(), AnswerTime: time.Unix(1383813746, 0).UTC(), + Usage: time.Duration(10) * time.Second, MediationRunId: utils.DEFAULT_RUNID, + ExtraFields: map[string]string{"extra1": "val_extra1", "extra2": "val_extra2", "extra3": "val_extra3"}, Cost: 1.01, + } + cdre, err := NewCdrExporter([]*utils.StoredCdr{storedCdr1}, logDb, cfg.CdreDefaultInstance, utils.CSV, '|', "firstexport", 0.0, 0.0, 0, 4, cfg.RoundingDecimals, "", 0, cfg.HttpSkipTlsVerify) + if err != nil { + t.Error("Unexpected error received: ", err) + } + csvWriter := csv.NewWriter(writer) + if err := cdre.writeCsv(csvWriter); err != nil { + t.Error("Unexpected error: ", err) + } + expected := `dbafe9c8614c785a65aabd116dd3959c3c56f7f6|default|*voice|dsafdsaf|rated|*out|cgrates.org|call|1001|1001|1002|2013-11-07T08:42:25Z|2013-11-07T08:42:26Z|10000000000|1.0100` + result := strings.TrimSpace(writer.String()) + if result != expected { + t.Errorf("Expected: \n%s received: \n%s.", expected, result) + } + if cdre.TotalCost() != 1.01 { + t.Error("Unexpected TotalCost: ", cdre.TotalCost()) + } +} diff --git a/cdre/fixedwidth_test.go b/cdre/fixedwidth_test.go index c004c55f5..44613217f 100644 --- a/cdre/fixedwidth_test.go +++ b/cdre/fixedwidth_test.go @@ -68,7 +68,7 @@ var trailerCfgFlds = []*config.CgrXmlCfgCdrField{ &config.CgrXmlCfgCdrField{Name: "DistributorCode", Type: CONSTANT, Value: "VOI", Width: 3}, &config.CgrXmlCfgCdrField{Name: "FileSeqNr", Type: METATAG, Value: META_EXPORTID, Width: 5, Strip: "right", Padding: "zeroleft"}, &config.CgrXmlCfgCdrField{Name: "NumberOfRecords", Type: METATAG, Value: META_NRCDRS, Width: 6, Padding: "zeroleft"}, - &config.CgrXmlCfgCdrField{Name: "CdrsDuration", Type: METATAG, Value: META_DURCDRS, Width: 8, Padding: "zeroleft"}, + &config.CgrXmlCfgCdrField{Name: "CdrsDuration", Type: METATAG, Value: META_DURCDRS, Width: 8, Padding: "zeroleft", Layout: "seconds"}, &config.CgrXmlCfgCdrField{Name: "FirstCdrTime", Type: METATAG, Value: META_FIRSTCDRATIME, Width: 12, Layout: "020106150400"}, &config.CgrXmlCfgCdrField{Name: "LastCdrTime", Type: METATAG, Value: META_LASTCDRATIME, Width: 12, Layout: "020106150400"}, &config.CgrXmlCfgCdrField{Name: "Filler2", Type: FILLER, Width: 93}, @@ -86,7 +86,8 @@ func TestWriteCdr(t *testing.T) { Content: &config.CgrXmlCfgCdrContent{Fields: contentCfgFlds}, Trailer: &config.CgrXmlCfgCdrTrailer{Fields: trailerCfgFlds}, } - cdr := &utils.StoredCdr{CgrId: utils.Sha1("dsafdsaf", time.Date(2013, 11, 7, 8, 42, 20, 0, time.UTC).String()), OrderId: 1, AccId: "dsafdsaf", CdrHost: "192.168.1.1", + cdr := &utils.StoredCdr{CgrId: utils.Sha1("dsafdsaf", time.Date(2013, 11, 7, 8, 42, 20, 0, time.UTC).String()), + TOR: utils.VOICE, OrderId: 1, AccId: "dsafdsaf", CdrHost: "192.168.1.1", ReqType: "rated", Direction: "*out", Tenant: "cgrates.org", Category: "call", Account: "1001", Subject: "1001", Destination: "1002", SetupTime: time.Date(2013, 11, 7, 8, 42, 20, 0, time.UTC), @@ -94,7 +95,7 @@ func TestWriteCdr(t *testing.T) { Usage: time.Duration(10) * time.Second, MediationRunId: utils.DEFAULT_RUNID, Cost: 2.34567, ExtraFields: map[string]string{"field_extr1": "val_extr1", "fieldextr2": "valextr2"}, } - cdre, err := NewCdrExporter([]*utils.StoredCdr{cdr}, logDb, exportTpl.AsCdreConfig(), utils.CDRE_FIXED_WIDTH, "fwv_1", 0.0, 0.0, 0, 4, cfg.RoundingDecimals, "", -1, cfg.HttpSkipTlsVerify) + cdre, err := NewCdrExporter([]*utils.StoredCdr{cdr}, logDb, exportTpl.AsCdreConfig(), utils.CDRE_FIXED_WIDTH, ',', "fwv_1", 0.0, 0.0, 0, 4, cfg.RoundingDecimals, "", -1, cfg.HttpSkipTlsVerify) if err != nil { t.Error(err) } @@ -144,14 +145,16 @@ func TestWriteCdrs(t *testing.T) { Content: &config.CgrXmlCfgCdrContent{Fields: contentCfgFlds}, Trailer: &config.CgrXmlCfgCdrTrailer{Fields: trailerCfgFlds}, } - cdr1 := &utils.StoredCdr{CgrId: utils.Sha1("aaa1", time.Date(2013, 11, 7, 8, 42, 20, 0, time.UTC).String()), OrderId: 2, AccId: "aaa1", CdrHost: "192.168.1.1", ReqType: "rated", Direction: "*out", Tenant: "cgrates.org", + cdr1 := &utils.StoredCdr{CgrId: utils.Sha1("aaa1", time.Date(2013, 11, 7, 8, 42, 20, 0, time.UTC).String()), + TOR: utils.VOICE, OrderId: 2, AccId: "aaa1", CdrHost: "192.168.1.1", ReqType: "rated", Direction: "*out", Tenant: "cgrates.org", Category: "call", Account: "1001", Subject: "1001", Destination: "1010", SetupTime: time.Date(2013, 11, 7, 8, 42, 20, 0, time.UTC), AnswerTime: time.Date(2013, 11, 7, 8, 42, 26, 0, time.UTC), Usage: time.Duration(10) * time.Second, MediationRunId: utils.DEFAULT_RUNID, Cost: 2.25, ExtraFields: map[string]string{"productnumber": "12341", "fieldextr2": "valextr2"}, } - cdr2 := &utils.StoredCdr{CgrId: utils.Sha1("aaa2", time.Date(2013, 11, 7, 7, 42, 20, 0, time.UTC).String()), OrderId: 4, AccId: "aaa2", CdrHost: "192.168.1.2", ReqType: "prepaid", Direction: "*out", Tenant: "cgrates.org", + cdr2 := &utils.StoredCdr{CgrId: utils.Sha1("aaa2", time.Date(2013, 11, 7, 7, 42, 20, 0, time.UTC).String()), + TOR: utils.VOICE, OrderId: 4, AccId: "aaa2", CdrHost: "192.168.1.2", ReqType: "prepaid", Direction: "*out", Tenant: "cgrates.org", Category: "call", Account: "1002", Subject: "1002", Destination: "1011", SetupTime: time.Date(2013, 11, 7, 7, 42, 20, 0, time.UTC), AnswerTime: time.Date(2013, 11, 7, 7, 42, 26, 0, time.UTC), @@ -159,7 +162,8 @@ func TestWriteCdrs(t *testing.T) { ExtraFields: map[string]string{"productnumber": "12342", "fieldextr2": "valextr2"}, } cdr3 := &utils.StoredCdr{} - cdr4 := &utils.StoredCdr{CgrId: utils.Sha1("aaa3", time.Date(2013, 11, 7, 9, 42, 18, 0, time.UTC).String()), OrderId: 3, AccId: "aaa4", CdrHost: "192.168.1.4", ReqType: "postpaid", Direction: "*out", Tenant: "cgrates.org", + cdr4 := &utils.StoredCdr{CgrId: utils.Sha1("aaa3", time.Date(2013, 11, 7, 9, 42, 18, 0, time.UTC).String()), + TOR: utils.VOICE, OrderId: 3, AccId: "aaa4", CdrHost: "192.168.1.4", ReqType: "postpaid", Direction: "*out", Tenant: "cgrates.org", Category: "call", Account: "1004", Subject: "1004", Destination: "1013", SetupTime: time.Date(2013, 11, 7, 9, 42, 18, 0, time.UTC), AnswerTime: time.Date(2013, 11, 7, 9, 42, 26, 0, time.UTC), @@ -167,7 +171,7 @@ func TestWriteCdrs(t *testing.T) { ExtraFields: map[string]string{"productnumber": "12344", "fieldextr2": "valextr2"}, } cfg, _ := config.NewDefaultCGRConfig() - cdre, err := NewCdrExporter([]*utils.StoredCdr{cdr1, cdr2, cdr3, cdr4}, logDb, exportTpl.AsCdreConfig(), utils.CDRE_FIXED_WIDTH, + cdre, err := NewCdrExporter([]*utils.StoredCdr{cdr1, cdr2, cdr3, cdr4}, logDb, exportTpl.AsCdreConfig(), utils.CDRE_FIXED_WIDTH, ',', "fwv_1", 0.0, 0.0, 0, 4, cfg.RoundingDecimals, "", -1, cfg.HttpSkipTlsVerify) if err != nil { t.Error(err) diff --git a/config/cdreconfig.go b/config/cdreconfig.go index 1df24a9fe..2ad092353 100644 --- a/config/cdreconfig.go +++ b/config/cdreconfig.go @@ -52,6 +52,7 @@ func NewDefaultCdreConfig() (*CdreConfig, error) { // One instance of CdrExporter type CdreConfig struct { CdrFormat string + FieldSeparator rune DataUsageMultiplyFactor float64 CostMultiplyFactor float64 CostRoundingDecimals int @@ -67,6 +68,7 @@ type CdreConfig struct { // Set here defaults func (cdreCfg *CdreConfig) setDefaults() error { cdreCfg.CdrFormat = utils.CSV + cdreCfg.FieldSeparator = utils.CSV_SEP cdreCfg.DataUsageMultiplyFactor = 0.0 cdreCfg.CostMultiplyFactor = 0.0 cdreCfg.CostRoundingDecimals = -1 diff --git a/config/cdreconfig_test.go b/config/cdreconfig_test.go index 18e65a8cd..c7edcf7aa 100644 --- a/config/cdreconfig_test.go +++ b/config/cdreconfig_test.go @@ -74,6 +74,7 @@ func TestCdreCfgValueAsRSRField(t *testing.T) { func TestCdreCfgNewDefaultCdreConfig(t *testing.T) { eCdreCfg := new(CdreConfig) eCdreCfg.CdrFormat = utils.CSV + eCdreCfg.FieldSeparator = utils.CSV_SEP eCdreCfg.DataUsageMultiplyFactor = 0.0 eCdreCfg.CostMultiplyFactor = 0.0 eCdreCfg.CostRoundingDecimals = -1 diff --git a/config/config_test.go b/config/config_test.go index 528f9fd76..6f9b06a36 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -207,6 +207,7 @@ func TestConfigFromFile(t *testing.T) { eCfg.CDRSMediator = "test" eCfg.CdreDefaultInstance = &CdreConfig{ CdrFormat: "test", + FieldSeparator: utils.CSV_SEP, DataUsageMultiplyFactor: 99.0, CostMultiplyFactor: 99.0, CostRoundingDecimals: 99, @@ -308,7 +309,7 @@ export_template = cgrid,mediation_runid,accid &CdreCdrField{Name: "accid", Type: utils.CDRFIELD, Value: "accid", valueAsRsrField: &utils.RSRField{Id: "accid"}, Mandatory: true}, } - expCdreCfg := &CdreConfig{CdrFormat: utils.CSV, CostRoundingDecimals: -1, ExportDir: "/var/log/cgrates/cdre", ContentFields: expectedFlds} + expCdreCfg := &CdreConfig{CdrFormat: utils.CSV, FieldSeparator: utils.CSV_SEP, CostRoundingDecimals: -1, ExportDir: "/var/log/cgrates/cdre", ContentFields: expectedFlds} if cfg, err := NewCGRConfigFromBytes(eFieldsCfg); err != nil { t.Error("Could not parse the config", err.Error()) } else if !reflect.DeepEqual(cfg.CdreDefaultInstance, expCdreCfg) { diff --git a/config/xmlcdrc.go b/config/xmlcdrc.go index a9bc24f15..d39458672 100644 --- a/config/xmlcdrc.go +++ b/config/xmlcdrc.go @@ -24,14 +24,14 @@ import ( ) type CgrXmlCdrcCfg struct { - Enabled bool `xml:"enabled"` // Enable/Disable the - CdrsAddress string `xml:"cdrs_address"` // The address where CDRs can be reached - CdrType string `xml:"cdr_type"` // The type of CDR to process - CsvSeparator string `xml:"csv_separator"` // The separator to use when reading csvs - RunDelay int64 `xml:"run_delay"` // Delay between runs - CdrInDir string `xml:"cdr_in_dir"` // Folder to process CDRs from - CdrOutDir string `xml:"cdr_out_dir"` // Folder to move processed CDRs to - CdrSourceId string `xml:"cdr_source_id"` // Source identifier for the processed CDRs + Enabled bool `xml:"enabled"` // Enable/Disable the + CdrsAddress string `xml:"cdrs_address"` // The address where CDRs can be reached + CdrType string `xml:"cdr_type"` // The type of CDR to process + CsvSeparator string `xml:"field_separator"` // The separator to use when reading csvs + RunDelay int64 `xml:"run_delay"` // Delay between runs + CdrInDir string `xml:"cdr_in_dir"` // Folder to process CDRs from + CdrOutDir string `xml:"cdr_out_dir"` // Folder to move processed CDRs to + CdrSourceId string `xml:"cdr_source_id"` // Source identifier for the processed CDRs CdrFields []*CdrcField `xml:"fields>field"` } diff --git a/config/xmlcdre.go b/config/xmlcdre.go index 54315ee40..e775a9e00 100644 --- a/config/xmlcdre.go +++ b/config/xmlcdre.go @@ -26,6 +26,7 @@ import ( // The CdrExporter configuration instance type CgrXmlCdreCfg struct { CdrFormat *string `xml:"cdr_format"` + FieldSeparator *string `xml:"field_separator"` DataUsageMultiplyFactor *float64 `xml:"data_usage_multiply_factor"` CostMultiplyFactor *float64 `xml:"cost_multiply_factor"` CostRoundingDecimals *int `xml:"cost_rounding_decimals"` @@ -43,6 +44,10 @@ func (xmlCdreCfg *CgrXmlCdreCfg) AsCdreConfig() *CdreConfig { if xmlCdreCfg.CdrFormat != nil { cdreCfg.CdrFormat = *xmlCdreCfg.CdrFormat } + if xmlCdreCfg.FieldSeparator != nil && len(*xmlCdreCfg.FieldSeparator) == 1 { + sepStr := *xmlCdreCfg.FieldSeparator + cdreCfg.FieldSeparator = rune(sepStr[0]) + } if xmlCdreCfg.DataUsageMultiplyFactor != nil { cdreCfg.DataUsageMultiplyFactor = *xmlCdreCfg.DataUsageMultiplyFactor } diff --git a/config/xmlcdre_test.go b/config/xmlcdre_test.go index c1802e0c3..3ab0e8e45 100644 --- a/config/xmlcdre_test.go +++ b/config/xmlcdre_test.go @@ -167,6 +167,7 @@ func TestXmlCdreCfgAsCdreConfig(t *testing.T) { fwv + ; 1024.0 1.19 -1 @@ -212,6 +213,7 @@ func TestXmlCdreCfgAsCdreConfig(t *testing.T) { } eCdreCfg := &CdreConfig{ CdrFormat: "fwv", + FieldSeparator: ';', DataUsageMultiplyFactor: 1024.0, CostMultiplyFactor: 1.19, CostRoundingDecimals: -1, diff --git a/utils/apitpdata.go b/utils/apitpdata.go index ad33714cc..176ca105b 100644 --- a/utils/apitpdata.go +++ b/utils/apitpdata.go @@ -328,6 +328,7 @@ type CachedItemAge struct { type AttrExpFileCdrs struct { CdrFormat *string // Cdr output file format + FieldSeparator *rune // Separator used between fields ExportId *string // Optional exportid ExportDir *string // If provided it overwrites the configured export directory ExportFileName *string // If provided the output filename will be set to this