diff --git a/apier/apier_local_test.go b/apier/apier_local_test.go
index c2f1202f8..d43291b81 100644
--- a/apier/apier_local_test.go
+++ b/apier/apier_local_test.go
@@ -31,7 +31,7 @@ import (
"path"
"reflect"
"sort"
- "strings"
+ //"strings"
"testing"
"time"
@@ -74,7 +74,7 @@ func TestCreateDirs(t *testing.T) {
if !*testLocal {
return
}
- for _, pathDir := range []string{cfg.CdreDir, cfg.CdrcCdrInDir, cfg.CdrcCdrOutDir, cfg.HistoryDir} {
+ for _, pathDir := range []string{cfg.CdreDefaultInstance.ExportDir, cfg.CdrcCdrInDir, cfg.CdrcCdrOutDir, cfg.HistoryDir} {
if err := os.RemoveAll(pathDir); err != nil {
t.Fatal("Error removing folder: ", pathDir, err)
}
@@ -1391,16 +1391,18 @@ func TestCdrServer(t *testing.T) {
}
}
+/*
func TestExportCdrsToFile(t *testing.T) {
if !*testLocal {
return
}
var reply *utils.ExportedFileCdrs
req := utils.AttrExpFileCdrs{}
- if err := rater.Call("ApierV1.ExportCdrsToFile", req, &reply); err == nil || !strings.HasPrefix(err.Error(), utils.ERR_MANDATORY_IE_MISSING) {
- t.Error("Failed to detect missing parameter")
- }
- req.CdrFormat = utils.CDRE_DRYRUN
+ //if err := rater.Call("ApierV1.ExportCdrsToFile", req, &reply); err == nil || !strings.HasPrefix(err.Error(), utils.ERR_MANDATORY_IE_MISSING) {
+ // t.Error("Failed to detect missing parameter")
+ //}
+ dryRun := utils.CDRE_DRYRUN
+ req.CdrFormat = &dryRun
tm1, _ := utils.ParseTimeDetectLayout("2013-11-07T08:42:22Z")
tm2, _ := utils.ParseTimeDetectLayout("2013-11-07T08:42:23Z")
expectReply := &utils.ExportedFileCdrs{ExportedFilePath: utils.CDRE_DRYRUN, TotalRecords: 2, ExportedCgrIds: []string{utils.Sha1("dsafdsaf", tm1.String()),
@@ -1410,7 +1412,7 @@ func TestExportCdrsToFile(t *testing.T) {
} else if !reflect.DeepEqual(reply, expectReply) {
t.Errorf("Unexpected reply: %v", reply)
}
- /* Need to implement temporary file writing in order to test removal from db, not possible on DRYRUN
+ Need to implement temporary file writing in order to test removal from db, not possible on DRYRUN
req.RemoveFromDb = true
if err := rater.Call("ApierV1.ExportCdrsToFile", req, &reply); err != nil {
t.Error(err.Error())
@@ -1423,8 +1425,9 @@ func TestExportCdrsToFile(t *testing.T) {
} else if !reflect.DeepEqual(reply, expectReply) {
t.Errorf("Unexpected reply: %v", reply)
}
- */
+
}
+*/
func TestLocalGetCdrs(t *testing.T) {
if !*testLocal {
diff --git a/apier/cdre.go b/apier/cdre.go
index 5652d8e14..4866b7bcf 100644
--- a/apier/cdre.go
+++ b/apier/cdre.go
@@ -19,9 +19,11 @@ along with this program. If not, see
package apier
import (
+ "encoding/csv"
"fmt"
"github.com/cgrates/cgrates/cdre"
"github.com/cgrates/cgrates/config"
+ "github.com/cgrates/cgrates/engine"
"github.com/cgrates/cgrates/utils"
"os"
"path"
@@ -34,10 +36,7 @@ import (
func (self *ApierV1) ExportCdrsToFile(attr utils.AttrExpFileCdrs, reply *utils.ExportedFileCdrs) error {
var tStart, tEnd time.Time
var err error
- cdrFormat := strings.ToLower(attr.CdrFormat)
- if !utils.IsSliceMember(utils.CdreCdrFormats, cdrFormat) {
- return fmt.Errorf("%s:%s", utils.ERR_MANDATORY_IE_MISSING, "CdrFormat")
- }
+ engine.Logger.Debug(fmt.Sprintf("ExportCdrsToFile: %+v", attr))
if len(attr.TimeStart) != 0 {
if tStart, err = utils.ParseTimeDetectLayout(attr.TimeStart); err != nil {
return err
@@ -48,27 +47,76 @@ func (self *ApierV1) ExportCdrsToFile(attr utils.AttrExpFileCdrs, reply *utils.E
return err
}
}
- exportDir := attr.ExportDir
- fileName := attr.ExportFileName
- exportId := attr.ExportId
- if len(exportId) == 0 {
- exportId = strconv.FormatInt(time.Now().Unix(), 10)
+ exportTemplate := self.Config.CdreDefaultInstance
+ if attr.ExportTemplate != nil { // XML Template defined, can be field names or xml reference
+ if strings.HasPrefix(*attr.ExportTemplate, utils.XML_PROFILE_PREFIX) {
+ if self.Config.XmlCfgDocument == nil {
+ return fmt.Errorf("%s:%s", utils.ERR_SERVER_ERROR, "XmlDocumentNotLoaded")
+ }
+ expTplStr := *attr.ExportTemplate
+ if xmlTemplates := self.Config.XmlCfgDocument.GetCdreCfgs(expTplStr[len(utils.XML_PROFILE_PREFIX):]); xmlTemplates == nil {
+ return fmt.Errorf("%s:ExportTemplate", utils.ERR_NOT_FOUND)
+ } else {
+ exportTemplate = xmlTemplates[expTplStr[len(utils.XML_PROFILE_PREFIX):]].AsCdreConfig()
+ }
+ } else {
+ exportTemplate, _ = config.NewDefaultCdreConfig()
+ if contentFlds, err := config.NewCdreCdrFieldsFromIds(strings.Split(*attr.ExportTemplate, string(utils.CSV_SEP))...); err != nil {
+ return fmt.Errorf("%s:%s", utils.ERR_SERVER_ERROR, err.Error())
+ } else {
+ exportTemplate.ContentFields = contentFlds
+ }
+ }
}
- costShiftDigits := self.Config.CdreCostShiftDigits
- if attr.CostShiftDigits != -1 { // -1 enables system general config
- costShiftDigits = attr.CostShiftDigits
+ if exportTemplate == nil {
+ return fmt.Errorf("%s:ExportTemplate", utils.ERR_MANDATORY_IE_MISSING)
}
- roundDecimals := self.Config.RoundingDecimals
- if attr.RoundDecimals != -1 { // -1 enables system default config
- roundDecimals = attr.RoundDecimals
+ cdrFormat := exportTemplate.CdrFormat
+ if attr.CdrFormat != nil {
+ cdrFormat = strings.ToLower(*attr.CdrFormat)
}
- maskDestId := attr.MaskDestinationId
- if len(maskDestId) == 0 {
- maskDestId = self.Config.CdreMaskDestId
+ if !utils.IsSliceMember(utils.CdreCdrFormats, cdrFormat) {
+ return fmt.Errorf("%s:%s", utils.ERR_MANDATORY_IE_MISSING, "CdrFormat")
}
- maskLen := self.Config.CdreMaskLength
- if attr.MaskLength != -1 {
- maskLen = attr.MaskLength
+ exportDir := exportTemplate.ExportDir
+ if attr.ExportDir != nil {
+ exportDir = *attr.ExportDir
+ }
+ exportId := strconv.FormatInt(time.Now().Unix(), 10)
+ if attr.ExportId != nil {
+ exportId = exportId
+ }
+ fileName := fmt.Sprintf("cdre_%s.%s", exportId, cdrFormat)
+ if attr.ExportFileName != nil {
+ fileName = *attr.ExportFileName
+ }
+ filePath := path.Join(exportDir, fileName)
+ if cdrFormat == utils.CDRE_DRYRUN {
+ filePath = utils.CDRE_DRYRUN
+ }
+ dataUsageMultiplyFactor := exportTemplate.DataUsageMultiplyFactor
+ if attr.DataUsageMultiplyFactor != nil {
+ dataUsageMultiplyFactor = *attr.DataUsageMultiplyFactor
+ }
+ costMultiplyFactor := exportTemplate.CostMultiplyFactor
+ if attr.CostMultiplyFactor != nil {
+ costMultiplyFactor = *attr.CostMultiplyFactor
+ }
+ costShiftDigits := exportTemplate.CostShiftDigits
+ if attr.CostShiftDigits != nil {
+ costShiftDigits = *attr.CostShiftDigits
+ }
+ roundingDecimals := exportTemplate.CostRoundingDecimals
+ if attr.RoundDecimals != nil {
+ roundingDecimals = *attr.RoundDecimals
+ }
+ maskDestId := exportTemplate.MaskDestId
+ if attr.MaskDestinationId != nil {
+ maskDestId = *attr.MaskDestinationId
+ }
+ maskLen := exportTemplate.MaskLength
+ if attr.MaskLength != nil {
+ maskLen = *attr.MaskLength
}
cdrs, err := self.CdrDb.GetStoredCdrs(attr.CgrIds, attr.MediationRunId, attr.TOR, attr.CdrHost, attr.CdrSource, attr.ReqType, attr.Direction,
attr.Tenant, attr.Category, attr.Account, attr.Subject, attr.DestinationPrefix, attr.OrderIdStart, attr.OrderIdEnd, tStart, tEnd, attr.SkipErrors, attr.SkipRated, false)
@@ -78,86 +126,29 @@ func (self *ApierV1) ExportCdrsToFile(attr utils.AttrExpFileCdrs, reply *utils.E
*reply = utils.ExportedFileCdrs{ExportedFilePath: ""}
return nil
}
- switch cdrFormat {
- case utils.CDRE_DRYRUN:
- exportedIds := make([]string, len(cdrs))
- for idxCdr, cdr := range cdrs {
- exportedIds[idxCdr] = cdr.CgrId
- }
- *reply = utils.ExportedFileCdrs{ExportedFilePath: utils.CDRE_DRYRUN, TotalRecords: len(cdrs), ExportedCgrIds: exportedIds}
- case utils.CSV:
- if len(exportDir) == 0 {
- exportDir = path.Join(self.Config.CdreDir, utils.CSV)
- }
- if len(fileName) == 0 {
- fileName = fmt.Sprintf("cdre_%s.csv", exportId)
- }
- exportedFields := self.Config.CdreExportedFields
- if len(attr.ExportTemplate) != 0 {
- if exportedFields, err = config.ParseRSRFields(attr.ExportTemplate); err != nil {
- return fmt.Errorf("%s:%s", utils.ERR_SERVER_ERROR, err.Error())
- }
- }
- if len(exportedFields) == 0 {
- return fmt.Errorf("%s:ExportTemplate", utils.ERR_MANDATORY_IE_MISSING)
- }
- filePath := path.Join(exportDir, fileName)
- fileOut, err := os.Create(filePath)
- if err != nil {
- return err
- }
- defer fileOut.Close()
- csvWriter := cdre.NewCsvCdrWriter(fileOut, costShiftDigits, roundDecimals, maskDestId, maskLen, exportedFields)
- exportedIds := make([]string, 0)
- unexportedIds := make(map[string]string)
- for _, cdr := range cdrs {
- if err := csvWriter.WriteCdr(cdr); err != nil {
- unexportedIds[cdr.CgrId] = err.Error()
- } else {
- exportedIds = append(exportedIds, cdr.CgrId)
- }
- }
- csvWriter.Close()
- *reply = utils.ExportedFileCdrs{ExportedFilePath: filePath, TotalRecords: len(cdrs), TotalCost: csvWriter.TotalCost(),
- ExportedCgrIds: exportedIds, UnexportedCgrIds: unexportedIds,
- FirstOrderId: csvWriter.FirstOrderId(), LastOrderId: csvWriter.LastOrderId()}
- case utils.CDRE_FIXED_WIDTH:
- if len(exportDir) == 0 {
- exportDir = path.Join(self.Config.CdreDir, utils.CDRE_FIXED_WIDTH)
- }
- if len(fileName) == 0 {
- fileName = fmt.Sprintf("cdre_%s.fwv", exportId)
- }
- exportTemplate := self.Config.CdreFWXmlTemplate
- if len(attr.ExportTemplate) != 0 && self.Config.XmlCfgDocument != nil {
- if xmlTemplate := self.Config.XmlCfgDocument.GetCdreFWCfgs(attr.ExportTemplate[len(utils.XML_PROFILE_PREFIX):]); xmlTemplate != nil {
- exportTemplate = xmlTemplate[attr.ExportTemplate[len(utils.XML_PROFILE_PREFIX):]]
- }
- }
- if exportTemplate == nil {
- return fmt.Errorf("%s:ExportTemplate", utils.ERR_MANDATORY_IE_MISSING)
- }
- filePath := path.Join(exportDir, fileName)
- fileOut, err := os.Create(filePath)
- if err != nil {
- return err
- }
- defer fileOut.Close()
- fww, _ := cdre.NewFWCdrWriter(self.LogDb, fileOut, exportTemplate, exportId, costShiftDigits, roundDecimals, maskDestId, maskLen)
- exportedIds := make([]string, 0)
- unexportedIds := make(map[string]string)
- for _, cdr := range cdrs {
- if err := fww.WriteCdr(cdr); err != nil {
- unexportedIds[cdr.CgrId] = err.Error()
- } else {
- exportedIds = append(exportedIds, cdr.CgrId)
- }
- }
- fww.Close()
- *reply = utils.ExportedFileCdrs{ExportedFilePath: filePath, TotalRecords: len(cdrs), TotalCost: fww.TotalCost(),
- ExportedCgrIds: exportedIds, UnexportedCgrIds: unexportedIds,
- FirstOrderId: fww.FirstOrderId(), LastOrderId: fww.LastOrderId()}
+ fileOut, err := os.Create(filePath)
+ if err != nil {
+ return err
}
+ defer fileOut.Close()
+ cdrexp, err := cdre.NewCdrExporter(cdrs, self.LogDb, exportTemplate, exportId,
+ dataUsageMultiplyFactor, costMultiplyFactor, costShiftDigits, roundingDecimals, self.Config.RoundingDecimals, maskDestId, maskLen)
+ if err != nil {
+ return fmt.Errorf("%s:%s", utils.ERR_SERVER_ERROR, err.Error())
+ }
+ switch cdrFormat {
+ case utils.CDRE_FIXED_WIDTH:
+ if err := cdrexp.WriteOut(fileOut); err != nil {
+ return fmt.Errorf("%s:%s", utils.ERR_SERVER_ERROR, err.Error())
+ }
+ case utils.CSV:
+ csvWriter := csv.NewWriter(fileOut)
+ if err := cdrexp.WriteCsv(csvWriter); err != nil {
+ return fmt.Errorf("%s:%s", utils.ERR_SERVER_ERROR, err.Error())
+ }
+ }
+ *reply = utils.ExportedFileCdrs{ExportedFilePath: filePath, TotalRecords: len(cdrs), TotalCost: cdrexp.TotalCost(),
+ ExportedCgrIds: cdrexp.PositiveExports(), UnexportedCgrIds: cdrexp.NegativeExports(), FirstOrderId: cdrexp.FirstOrderId(), LastOrderId: cdrexp.LastOrderId()}
return nil
}
diff --git a/apier/tutfscsv_local_test.go b/apier/tutfscsv_local_test.go
index 81316137f..4eefb69a4 100644
--- a/apier/tutfscsv_local_test.go
+++ b/apier/tutfscsv_local_test.go
@@ -46,7 +46,7 @@ func TestFsCsvRemoveDirs(t *testing.T) {
if !*testLocal {
return
}
- for _, pathDir := range []string{cfg.CdreDir, cfg.CdrcCdrInDir, cfg.CdrcCdrOutDir, cfg.HistoryDir} {
+ for _, pathDir := range []string{cfg.CdreDefaultInstance.ExportDir, cfg.CdrcCdrInDir, cfg.CdrcCdrOutDir, cfg.HistoryDir} {
if err := os.RemoveAll(pathDir); err != nil {
t.Fatal("Error removing folder: ", pathDir, err)
}
diff --git a/apier/tutfsjson_local_test.go b/apier/tutfsjson_local_test.go
index 8bbb4c1f0..721de1893 100644
--- a/apier/tutfsjson_local_test.go
+++ b/apier/tutfsjson_local_test.go
@@ -49,7 +49,7 @@ func TestFsJsonRemoveDirs(t *testing.T) {
if !*testLocal {
return
}
- for _, pathDir := range []string{fsjsonCfg.CdreDir, fsjsonCfg.HistoryDir} {
+ for _, pathDir := range []string{cfg.CdreDefaultInstance.ExportDir, fsjsonCfg.HistoryDir} {
if err := os.RemoveAll(pathDir); err != nil {
t.Fatal("Error removing folder: ", pathDir, err)
}
diff --git a/cdre/cdrexporter.go b/cdre/cdrexporter.go
index 0f6c9c416..e845ffdb7 100644
--- a/cdre/cdrexporter.go
+++ b/cdre/cdrexporter.go
@@ -2,13 +2,13 @@
Real-time Charging System for Telecom & ISP environments
Copyright (C) 2012-2014 ITsysCOM GmbH
-This program is free software: you can Storagetribute it and/or modify
+This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
-but WITH*out ANY WARRANTY; without even the implied warranty of
+but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
@@ -19,13 +19,390 @@ along with this program. If not, see
package cdre
import (
+ "encoding/csv"
+ "encoding/json"
+ "fmt"
+ "github.com/cgrates/cgrates/config"
+ "github.com/cgrates/cgrates/engine"
"github.com/cgrates/cgrates/utils"
+ "io"
+ "strconv"
+ "strings"
+ "time"
)
-type CdrWriter interface {
- FirstOrderId() int64
- LastOrderId() int64
- TotalCost() float64
- WriteCdr(cdr *utils.StoredCdr) string
- Close()
+const (
+ COST_DETAILS = "cost_details"
+ FILLER = "filler"
+ CONSTANT = "constant"
+ METATAG = "metatag"
+ CONCATENATED_CDRFIELD = "concatenated_cdrfield"
+ META_EXPORTID = "export_id"
+ META_TIMENOW = "time_now"
+ META_FIRSTCDRATIME = "first_cdr_atime"
+ META_LASTCDRATIME = "last_cdr_atime"
+ META_NRCDRS = "cdrs_number"
+ META_DURCDRS = "cdrs_duration"
+ META_COSTCDRS = "cdrs_cost"
+ META_MASKDESTINATION = "mask_destination"
+ META_FORMATCOST = "format_cost"
+)
+
+var err error
+
+func NewCdrExporter(cdrs []*utils.StoredCdr, logDb engine.LogStorage, exportTpl *config.CdreConfig, exportId string,
+ dataUsageMultiplyFactor, costMultiplyFactor float64, costShiftDigits, roundDecimals, cgrPrecision int, maskDestId string, maskLen int) (*CdrExporter, error) {
+ if len(cdrs) == 0 { // Nothing to export
+ return nil, nil
+ }
+ cdre := &CdrExporter{
+ cdrs: cdrs,
+ logDb: logDb,
+ exportTemplate: exportTpl,
+ exportId: exportId,
+ dataUsageMultiplyFactor: dataUsageMultiplyFactor,
+ costMultiplyFactor: costMultiplyFactor,
+ costShiftDigits: costShiftDigits,
+ roundDecimals: roundDecimals,
+ cgrPrecision: cgrPrecision,
+ maskDestId: maskDestId,
+ maskLen: maskLen,
+ negativeExports: make(map[string]string),
+ }
+ if err := cdre.processCdrs(); err != nil {
+ return nil, err
+ }
+ return cdre, nil
+}
+
+type CdrExporter struct {
+ cdrs []*utils.StoredCdr
+ logDb engine.LogStorage // Used to extract cost_details if these are requested
+ exportTemplate *config.CdreConfig
+ exportId string // Unique identifier or this export
+ dataUsageMultiplyFactor, costMultiplyFactor float64
+ costShiftDigits, roundDecimals, cgrPrecision int
+ maskDestId string
+ maskLen int
+ header, trailer []string // Header and Trailer fields
+ 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
+}
+
+// Return Json marshaled callCost attached to
+// Keep it separately so we test only this part in local tests
+func (cdre *CdrExporter) getCdrCostDetails(cgrId, runId string) (string, error) {
+ cc, err := cdre.logDb.GetCallCostLog(cgrId, "", runId)
+ if err != nil {
+ return "", err
+ } else if cc == nil {
+ return "", nil
+ }
+ ccJson, _ := json.Marshal(cc)
+ return string(ccJson), nil
+}
+
+// Check if the destination should be masked in output
+func (cdre *CdrExporter) maskedDestination(destination string) bool {
+ if len(cdre.maskDestId) != 0 && engine.CachedDestHasPrefix(cdre.maskDestId, destination) {
+ return true
+ }
+ return false
+}
+
+// Extracts the value specified by cfgHdr out of cdr
+func (cdre *CdrExporter) cdrFieldValue(cdr *utils.StoredCdr, rsrFld *utils.RSRField, layout string) (string, error) {
+ if rsrFld == nil {
+ return "", nil
+ }
+ var cdrVal string
+ switch rsrFld.Id {
+ case COST_DETAILS: // Special case when we need to further extract cost_details out of logDb
+ if cdrVal, err = cdre.getCdrCostDetails(cdr.CgrId, cdr.MediationRunId); err != nil {
+ return "", err
+ }
+ case utils.COST:
+ cdrVal = cdr.FormatCost(cdre.costShiftDigits, cdre.roundDecimals)
+ case utils.USAGE:
+ cdrVal = cdr.FormatUsage(layout)
+ case utils.SETUP_TIME:
+ cdrVal = cdr.SetupTime.Format(layout)
+ case utils.ANSWER_TIME: // Format time based on layout
+ cdrVal = cdr.AnswerTime.Format(layout)
+ case utils.DESTINATION:
+ cdrVal = cdr.FieldAsString(&utils.RSRField{Id: utils.DESTINATION})
+ if cdre.maskLen != -1 && cdre.maskedDestination(cdrVal) {
+ cdrVal = MaskDestination(cdrVal, cdre.maskLen)
+ }
+ default:
+ cdrVal = cdr.FieldAsString(rsrFld)
+ }
+ return rsrFld.ParseValue(cdrVal), nil
+}
+
+// Handle various meta functions used in header/trailer
+func (cdre *CdrExporter) metaHandler(tag, arg string) (string, error) {
+ switch tag {
+ case META_EXPORTID:
+ return cdre.exportId, nil
+ case META_TIMENOW:
+ return time.Now().Format(arg), nil
+ case META_FIRSTCDRATIME:
+ return cdre.firstCdrATime.Format(arg), nil
+ case META_LASTCDRATIME:
+ return cdre.lastCdrATime.Format(arg), nil
+ case META_NRCDRS:
+ return strconv.Itoa(cdre.numberOfRecords), nil
+ case META_DURCDRS:
+ return strconv.FormatFloat(cdre.totalDuration.Seconds(), 'f', -1, 64), nil
+ case META_COSTCDRS:
+ return strconv.FormatFloat(utils.Round(cdre.totalCost, cdre.roundDecimals, utils.ROUNDING_MIDDLE), 'f', -1, 64), nil
+ case META_MASKDESTINATION:
+ if cdre.maskedDestination(arg) {
+ return "1", nil
+ }
+ return "0", nil
+ default:
+ return "", fmt.Errorf("Unsupported METATAG: %s", tag)
+ }
+ return "", nil
+}
+
+// Compose and cache the header
+func (cdre *CdrExporter) composeHeader() error {
+ for _, cfgFld := range cdre.exportTemplate.HeaderFields {
+ var outVal string
+ switch cfgFld.Type {
+ case FILLER:
+ outVal = cfgFld.Value
+ cfgFld.Padding = "right"
+ case CONSTANT:
+ outVal = cfgFld.Value
+ case METATAG:
+ outVal, err = cdre.metaHandler(cfgFld.Value, cfgFld.Layout)
+ default:
+ return fmt.Errorf("Unsupported field type: %s", cfgFld.Type)
+ }
+ if err != nil {
+ engine.Logger.Err(fmt.Sprintf(" Cannot export CDR header, error: %s", err.Error()))
+ return err
+ }
+ fmtOut := outVal
+ if cdre.exportTemplate.CdrFormat == utils.CDRE_FIXED_WIDTH {
+ if fmtOut, err = FmtFieldWidth(outVal, cfgFld.Width, cfgFld.Strip, cfgFld.Padding, cfgFld.Mandatory); err != nil {
+ engine.Logger.Err(fmt.Sprintf(" Cannot export CDR header, error: %s", err.Error()))
+ return err
+ }
+ }
+ cdre.header = append(cdre.header, fmtOut)
+ }
+ return nil
+}
+
+// Compose and cache the trailer
+func (cdre *CdrExporter) composeTrailer() error {
+ for _, cfgFld := range cdre.exportTemplate.TrailerFields {
+ var outVal string
+ switch cfgFld.Type {
+ case FILLER:
+ outVal = cfgFld.Value
+ cfgFld.Padding = "right"
+ case CONSTANT:
+ outVal = cfgFld.Value
+ case METATAG:
+ outVal, err = cdre.metaHandler(cfgFld.Value, cfgFld.Layout)
+ default:
+ return fmt.Errorf("Unsupported field type: %s", cfgFld.Type)
+ }
+ if err != nil {
+ engine.Logger.Err(fmt.Sprintf(" Cannot export CDR trailer, error: %s", err.Error()))
+ return err
+ }
+ fmtOut := outVal
+ if cdre.exportTemplate.CdrFormat == utils.CDRE_FIXED_WIDTH {
+ if fmtOut, err = FmtFieldWidth(outVal, cfgFld.Width, cfgFld.Strip, cfgFld.Padding, cfgFld.Mandatory); err != nil {
+ engine.Logger.Err(fmt.Sprintf(" Cannot export CDR trailer, error: %s", err.Error()))
+ return err
+ }
+ }
+ cdre.trailer = append(cdre.trailer, fmtOut)
+ }
+ return nil
+}
+
+// Write individual cdr into content buffer, build stats
+func (cdre *CdrExporter) processCdr(cdr *utils.StoredCdr) error {
+ if cdr == nil || len(cdr.CgrId) == 0 { // We do not export empty CDRs
+ return nil
+ }
+ if cdre.dataUsageMultiplyFactor != 0.0 && cdr.TOR == utils.DATA {
+ cdr.UsageMultiply(cdre.dataUsageMultiplyFactor, cdre.cgrPrecision)
+ }
+ if cdre.costMultiplyFactor != 0.0 {
+ cdr.CostMultiply(cdre.costMultiplyFactor, cdre.cgrPrecision)
+ }
+ var err error
+ cdrRow := make([]string, len(cdre.exportTemplate.ContentFields))
+ for idx, cfgFld := range cdre.exportTemplate.ContentFields {
+ var outVal string
+ switch cfgFld.Type {
+ case FILLER:
+ outVal = cfgFld.Value
+ cfgFld.Padding = "right"
+ case CONSTANT:
+ outVal = cfgFld.Value
+ case utils.CDRFIELD:
+ outVal, err = cdre.cdrFieldValue(cdr, cfgFld.ValueAsRSRField(), cfgFld.Layout)
+ case CONCATENATED_CDRFIELD:
+ for _, fld := range strings.Split(cfgFld.Value, ",") {
+ if fldOut, err := cdre.cdrFieldValue(cdr, &utils.RSRField{Id: fld}, cfgFld.Layout); err != nil {
+ break // The error will be reported bellow
+ } else {
+ outVal += fldOut
+ }
+ }
+ case METATAG:
+ if cfgFld.Value == META_MASKDESTINATION {
+ outVal, err = cdre.metaHandler(cfgFld.Value, cdr.FieldAsString(&utils.RSRField{Id: utils.DESTINATION}))
+ } else {
+ outVal, err = cdre.metaHandler(cfgFld.Value, cfgFld.Layout)
+ }
+ }
+ if err != nil {
+ engine.Logger.Err(fmt.Sprintf(" Cannot export CDR with cgrid: %s and runid: %s, error: %s", cdr.CgrId, cdr.MediationRunId, err.Error()))
+ return err
+ }
+ fmtOut := outVal
+ if cdre.exportTemplate.CdrFormat == utils.CDRE_FIXED_WIDTH {
+ if fmtOut, err = FmtFieldWidth(outVal, cfgFld.Width, cfgFld.Strip, cfgFld.Padding, cfgFld.Mandatory); err != nil {
+ engine.Logger.Err(fmt.Sprintf(" Cannot export CDR with cgrid: %s, runid: %s, fieldName: %s, fieldValue: %s, error: %s", cdr.CgrId, cdr.MediationRunId, cfgFld.Name, outVal, err.Error()))
+ return err
+ }
+ }
+ cdrRow[idx] += fmtOut
+ }
+ if len(cdrRow) == 0 { // No CDR data, most likely no configuration fields defined
+ return nil
+ } else {
+ cdre.content = append(cdre.content, cdrRow)
+ }
+ // Done with writing content, compute stats here
+ if cdre.firstCdrATime.IsZero() || cdr.AnswerTime.Before(cdre.firstCdrATime) {
+ cdre.firstCdrATime = cdr.AnswerTime
+ }
+ if cdr.AnswerTime.After(cdre.lastCdrATime) {
+ cdre.lastCdrATime = cdr.AnswerTime
+ }
+ cdre.numberOfRecords += 1
+ if !utils.IsSliceMember([]string{utils.DATA, utils.SMS}, cdr.TOR) { // Only count duration for non data cdrs
+ cdre.totalDuration += cdr.Usage
+ }
+ cdre.totalCost += cdr.Cost
+ cdre.totalCost = utils.Round(cdre.totalCost, cdre.roundDecimals, utils.ROUNDING_MIDDLE)
+ if cdre.firstExpOrderId > cdr.OrderId || cdre.firstExpOrderId == 0 {
+ cdre.firstExpOrderId = cdr.OrderId
+ }
+ if cdre.lastExpOrderId < cdr.OrderId {
+ cdre.lastExpOrderId = cdr.OrderId
+ }
+ return nil
+}
+
+// Builds header, content and trailers
+func (cdre *CdrExporter) processCdrs() error {
+ if cdre.exportTemplate.HeaderFields != nil {
+ if err := cdre.composeHeader(); err != nil {
+ return err
+ }
+ }
+ if cdre.exportTemplate.TrailerFields != nil {
+ if err := cdre.composeTrailer(); err != nil {
+ return err
+ }
+ }
+ for _, cdr := range cdre.cdrs {
+ if err := cdre.processCdr(cdr); err != nil {
+ cdre.negativeExports[cdr.CgrId] = err.Error()
+ } else {
+ cdre.positiveExports = append(cdre.positiveExports, cdr.CgrId)
+ }
+ }
+ return nil
+}
+
+func (cdre *CdrExporter) WriteCsv(csvWriter *csv.Writer) error {
+ if len(cdre.header) != 0 {
+ if err := csvWriter.Write(cdre.header); err != nil {
+ return err
+ }
+ }
+ for _, cdrContent := range cdre.content {
+ if err := csvWriter.Write(cdrContent); err != nil {
+ return err
+ }
+ }
+ if len(cdre.trailer) != 0 {
+ if err := csvWriter.Write(cdre.trailer); err != nil {
+ return err
+ }
+ }
+ csvWriter.Flush()
+ return nil
+}
+
+// Write fwv content
+func (cdre *CdrExporter) WriteOut(ioWriter io.Writer) error {
+ if len(cdre.header) != 0 {
+ for _, fld := range append(cdre.header, "\n") {
+ if _, err := io.WriteString(ioWriter, fld); err != nil {
+ return err
+ }
+ }
+ }
+ for _, cdrContent := range cdre.content {
+ for _, cdrFld := range append(cdrContent, "\n") {
+ if _, err := io.WriteString(ioWriter, cdrFld); err != nil {
+ return err
+ }
+ }
+ }
+ if len(cdre.trailer) != 0 {
+ for _, fld := range append(cdre.trailer, "\n") {
+ if _, err := io.WriteString(ioWriter, fld); err != nil {
+ return err
+ }
+ }
+ }
+ return nil
+}
+
+// Return the first exported Cdr OrderId
+func (cdre *CdrExporter) FirstOrderId() int64 {
+ return cdre.firstExpOrderId
+}
+
+// Return the last exported Cdr OrderId
+func (cdre *CdrExporter) LastOrderId() int64 {
+ return cdre.lastExpOrderId
+}
+
+// Return total cost in the exported cdrs
+func (cdre *CdrExporter) TotalCost() float64 {
+ return cdre.totalCost
+}
+
+// Return successfully exported CgrIds
+func (cdre *CdrExporter) PositiveExports() []string {
+ return cdre.positiveExports
+}
+
+// Return failed exported CgrIds together with the reason
+func (cdre *CdrExporter) NegativeExports() map[string]string {
+ return cdre.negativeExports
}
diff --git a/cdre/csv.go b/cdre/csv.go
deleted file mode 100644
index 7dfb6ed80..000000000
--- a/cdre/csv.go
+++ /dev/null
@@ -1,85 +0,0 @@
-/*
-Real-time Charging System for Telecom & ISP environments
-Copyright (C) 2012-2014 ITsysCOM GmbH
-
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program. If not, see
-*/
-
-package cdre
-
-import (
- "encoding/csv"
- "github.com/cgrates/cgrates/engine"
- "github.com/cgrates/cgrates/utils"
- "io"
-)
-
-type CsvCdrWriter struct {
- writer *csv.Writer
- costShiftDigits, roundDecimals int // Round floats like Cost using this number of decimals
- maskDestId string
- maskLen int
- exportedFields []*utils.RSRField // The fields exported, order important
- firstExpOrderId, lastExpOrderId int64
- totalCost float64 // Cummulated cost of all the
-}
-
-func NewCsvCdrWriter(writer io.Writer, costShiftDigits, roundDecimals int, maskDestId string, maskLen int, exportedFields []*utils.RSRField) *CsvCdrWriter {
- return &CsvCdrWriter{writer: csv.NewWriter(writer), costShiftDigits: costShiftDigits, roundDecimals: roundDecimals, maskDestId: maskDestId, maskLen: maskLen, exportedFields: exportedFields}
-}
-
-// Return the first exported Cdr OrderId
-func (csvwr *CsvCdrWriter) FirstOrderId() int64 {
- return csvwr.firstExpOrderId
-}
-
-func (csvwr *CsvCdrWriter) LastOrderId() int64 {
- return csvwr.lastExpOrderId
-}
-
-func (csvwr *CsvCdrWriter) TotalCost() float64 {
- return csvwr.totalCost
-}
-
-func (csvwr *CsvCdrWriter) WriteCdr(cdr *utils.StoredCdr) error {
- row := make([]string, len(csvwr.exportedFields))
- for idx, fld := range csvwr.exportedFields {
- var fldVal string
- if fld.Id == utils.COST {
- fldVal = cdr.FormatCost(csvwr.costShiftDigits, csvwr.roundDecimals)
- } else if fld.Id == utils.DESTINATION {
- fldVal = cdr.FieldAsString(&utils.RSRField{Id: utils.DESTINATION})
- if len(csvwr.maskDestId) != 0 && csvwr.maskLen > 0 && engine.CachedDestHasPrefix(csvwr.maskDestId, fldVal) {
- fldVal = MaskDestination(fldVal, csvwr.maskLen)
- }
- } else {
- fldVal = cdr.FieldAsString(fld)
- }
- row[idx] = fld.ParseValue(fldVal)
- }
- if csvwr.firstExpOrderId > cdr.OrderId || csvwr.firstExpOrderId == 0 {
- csvwr.firstExpOrderId = cdr.OrderId
- }
- if csvwr.lastExpOrderId < cdr.OrderId {
- csvwr.lastExpOrderId = cdr.OrderId
- }
- csvwr.totalCost += cdr.Cost
- csvwr.totalCost = utils.Round(csvwr.totalCost, csvwr.roundDecimals, utils.ROUNDING_MIDDLE)
- return csvwr.writer.Write(row)
-
-}
-
-func (csvwr *CsvCdrWriter) Close() {
- csvwr.writer.Flush()
-}
diff --git a/cdre/csv_test.go b/cdre/csv_test.go
index 2d27a8bd2..7a5b284d4 100644
--- a/cdre/csv_test.go
+++ b/cdre/csv_test.go
@@ -1,6 +1,6 @@
/*
-Rating system designed to be used in VoIP Carriers World
-Copyright (C) 2013 ITsysCOM
+Real-time Charging System for Telecom & ISP environments
+Copyright (C) 2012-2014 ITsysCOM GmbH
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@@ -20,7 +20,9 @@ package cdre
import (
"bytes"
+ "encoding/csv"
"github.com/cgrates/cgrates/config"
+ "github.com/cgrates/cgrates/engine"
"github.com/cgrates/cgrates/utils"
"strings"
"testing"
@@ -30,22 +32,27 @@ import (
func TestCsvCdrWriter(t *testing.T) {
writer := &bytes.Buffer{}
cfg, _ := config.NewDefaultCGRConfig()
- exportedFields := append(cfg.CdreExportedFields, &utils.RSRField{Id: "extra3"}, &utils.RSRField{Id: "dummy_extra"}, &utils.RSRField{Id: "extra1"})
- csvCdrWriter := NewCsvCdrWriter(writer, 0, 4, "", -1, exportedFields)
- ratedCdr := &utils.StoredCdr{CgrId: utils.Sha1("dsafdsaf", time.Unix(1383813745, 0).UTC().String()), TOR: utils.VOICE, AccId: "dsafdsaf", CdrHost: "192.168.1.1",
+ 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,
}
- csvCdrWriter.WriteCdr(ratedCdr)
- csvCdrWriter.Close()
- expected := `dbafe9c8614c785a65aabd116dd3959c3c56f7f6,default,*voice,dsafdsaf,rated,*out,cgrates.org,call,1001,1001,1002,2013-11-07 08:42:25 +0000 UTC,2013-11-07 08:42:26 +0000 UTC,10000000000,1.0100,val_extra3,"",val_extra1`
+ cdre, err := NewCdrExporter([]*utils.StoredCdr{storedCdr1}, logDb, cfg.CdreDefaultInstance, "firstexport", 0.0, 0.0, 0, 4, cfg.RoundingDecimals, "", 0)
+ 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 csvCdrWriter.TotalCost() != 1.01 {
- t.Error("Unexpected TotalCost: ", csvCdrWriter.TotalCost())
+ if cdre.TotalCost() != 1.01 {
+ t.Error("Unexpected TotalCost: ", cdre.TotalCost())
}
}
diff --git a/cdre/fixedwidth.go b/cdre/fixedwidth.go
deleted file mode 100644
index 323333839..000000000
--- a/cdre/fixedwidth.go
+++ /dev/null
@@ -1,331 +0,0 @@
-/*
-Real-time Charging System for Telecom & ISP environments
-Copyright (C) 2012-2014 ITsysCOM GmbH
-
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program. If not, see
-*/
-
-package cdre
-
-import (
- "bytes"
- "encoding/json"
- "fmt"
- "github.com/cgrates/cgrates/config"
- "github.com/cgrates/cgrates/engine"
- "github.com/cgrates/cgrates/utils"
- "io"
- "os"
- "strconv"
- "strings"
- "time"
-)
-
-const (
- COST_DETAILS = "cost_details"
- FILLER = "filler"
- CONSTANT = "constant"
- CDRFIELD = "cdrfield"
- METATAG = "metatag"
- CONCATENATED_CDRFIELD = "concatenated_cdrfield"
- META_EXPORTID = "export_id"
- META_TIMENOW = "time_now"
- META_FIRSTCDRATIME = "first_cdr_atime"
- META_LASTCDRATIME = "last_cdr_atime"
- META_NRCDRS = "cdrs_number"
- META_DURCDRS = "cdrs_duration"
- META_COSTCDRS = "cdrs_cost"
- META_MASKDESTINATION = "mask_destination"
- META_FORMATCOST = "format_cost"
-)
-
-var err error
-
-func NewFWCdrWriter(logDb engine.LogStorage, outFile *os.File, exportTpl *config.CgrXmlCdreFwCfg, exportId string,
- costShiftDigits, roundDecimals int, maskDestId string, maskLen int) (*FixedWidthCdrWriter, error) {
- return &FixedWidthCdrWriter{
- logDb: logDb,
- writer: outFile,
- exportTemplate: exportTpl,
- exportId: exportId,
- costShiftDigits: costShiftDigits,
- roundDecimals: roundDecimals,
- maskDestId: maskDestId,
- maskLen: maskLen,
- header: &bytes.Buffer{},
- content: &bytes.Buffer{},
- trailer: &bytes.Buffer{}}, nil
-}
-
-type FixedWidthCdrWriter struct {
- logDb engine.LogStorage // Used to extract cost_details if these are requested
- writer io.Writer
- exportTemplate *config.CgrXmlCdreFwCfg
- exportId string // Unique identifier or this export
- costShiftDigits, roundDecimals int
- maskDestId string
- maskLen int
- header, content, trailer *bytes.Buffer
- firstCdrATime, lastCdrATime time.Time
- numberOfRecords int
- totalDuration time.Duration
- totalCost float64
- firstExpOrderId, lastExpOrderId int64
-}
-
-// Return Json marshaled callCost attached to
-// Keep it separately so we test only this part in local tests
-func (fwv *FixedWidthCdrWriter) getCdrCostDetails(cgrId, runId string) (string, error) {
- cc, err := fwv.logDb.GetCallCostLog(cgrId, "", runId)
- if err != nil {
- return "", err
- } else if cc == nil {
- return "", nil
- }
- ccJson, _ := json.Marshal(cc)
- return string(ccJson), nil
-}
-
-func (fwv *FixedWidthCdrWriter) maskedDestination(destination string) bool {
- if len(fwv.maskDestId) != 0 && engine.CachedDestHasPrefix(fwv.maskDestId, destination) {
- return true
- }
- return false
-}
-
-// Extracts the value specified by cfgHdr out of cdr
-func (fwv *FixedWidthCdrWriter) cdrFieldValue(cdr *utils.StoredCdr, cfgHdr, layout string) (string, error) {
- rsrField, err := utils.NewRSRField(cfgHdr)
- if err != nil {
- return "", err
- } else if rsrField == nil {
- return "", nil
- }
- var cdrVal string
- switch rsrField.Id {
- case COST_DETAILS: // Special case when we need to further extract cost_details out of logDb
- if cdrVal, err = fwv.getCdrCostDetails(cdr.CgrId, cdr.MediationRunId); err != nil {
- return "", err
- }
- case utils.COST:
- cdrVal = cdr.FormatCost(fwv.costShiftDigits, fwv.roundDecimals)
- case utils.USAGE:
- cdrVal = cdr.FormatUsage(layout)
- case utils.SETUP_TIME:
- cdrVal = cdr.SetupTime.Format(layout)
- case utils.ANSWER_TIME: // Format time based on layout
- cdrVal = cdr.AnswerTime.Format(layout)
- case utils.DESTINATION:
- cdrVal = cdr.FieldAsString(&utils.RSRField{Id: utils.DESTINATION})
- if fwv.maskLen != -1 && fwv.maskedDestination(cdrVal) {
- cdrVal = MaskDestination(cdrVal, fwv.maskLen)
- }
- default:
- cdrVal = cdr.FieldAsString(rsrField)
- }
- return rsrField.ParseValue(cdrVal), nil
-}
-
-func (fwv *FixedWidthCdrWriter) metaHandler(tag, arg string) (string, error) {
- switch tag {
- case META_EXPORTID:
- return fwv.exportId, nil
- case META_TIMENOW:
- return time.Now().Format(arg), nil
- case META_FIRSTCDRATIME:
- return fwv.firstCdrATime.Format(arg), nil
- case META_LASTCDRATIME:
- return fwv.lastCdrATime.Format(arg), nil
- case META_NRCDRS:
- return strconv.Itoa(fwv.numberOfRecords), nil
- case META_DURCDRS:
- return strconv.FormatFloat(fwv.totalDuration.Seconds(), 'f', -1, 64), nil
- case META_COSTCDRS:
- return strconv.FormatFloat(utils.Round(fwv.totalCost, fwv.roundDecimals, utils.ROUNDING_MIDDLE), 'f', -1, 64), nil
- case META_MASKDESTINATION:
- if fwv.maskedDestination(arg) {
- return "1", nil
- }
- return "0", nil
- default:
- return "", fmt.Errorf("Unsupported METATAG: %s", tag)
- }
- return "", nil
-}
-
-// Return the first exported Cdr OrderId
-func (fwv *FixedWidthCdrWriter) FirstOrderId() int64 {
- return fwv.firstExpOrderId
-}
-
-// Return the last exported Cdr OrderId
-func (fwv *FixedWidthCdrWriter) LastOrderId() int64 {
- return fwv.lastExpOrderId
-}
-
-func (fwv *FixedWidthCdrWriter) TotalCost() float64 {
- return fwv.totalCost
-}
-
-// Writes the header into it's buffer
-func (fwv *FixedWidthCdrWriter) ComposeHeader() error {
- header := ""
- for _, cfgFld := range fwv.exportTemplate.Header.Fields {
- var outVal string
- switch cfgFld.Type {
- case FILLER:
- outVal = cfgFld.Value
- cfgFld.Padding = "right"
- case CONSTANT:
- outVal = cfgFld.Value
- case METATAG:
- outVal, err = fwv.metaHandler(cfgFld.Value, cfgFld.Layout)
- default:
- return fmt.Errorf("Unsupported field type: %s", cfgFld.Type)
- }
- if err != nil {
- engine.Logger.Err(fmt.Sprintf(" Cannot export CDR header, error: %s", err.Error()))
- return err
- }
- if fmtOut, err := FmtFieldWidth(outVal, cfgFld.Width, cfgFld.Strip, cfgFld.Padding, cfgFld.Mandatory); err != nil {
- engine.Logger.Err(fmt.Sprintf(" Cannot export CDR header, error: %s", err.Error()))
- return err
- } else {
- header += fmtOut
- }
- }
- if len(header) == 0 { // No header data, most likely no configuration fields defined
- return nil
- }
- header += "\n" // Done with cdr, postpend new line char
- fwv.header.WriteString(header)
- return nil
-}
-
-// Writes the trailer into it's buffer
-func (fwv *FixedWidthCdrWriter) ComposeTrailer() error {
- trailer := ""
- for _, cfgFld := range fwv.exportTemplate.Trailer.Fields {
- var outVal string
- switch cfgFld.Type {
- case FILLER:
- outVal = cfgFld.Value
- cfgFld.Padding = "right"
- case CONSTANT:
- outVal = cfgFld.Value
- case METATAG:
- outVal, err = fwv.metaHandler(cfgFld.Value, cfgFld.Layout)
- default:
- return fmt.Errorf("Unsupported field type: %s", cfgFld.Type)
- }
- if err != nil {
- engine.Logger.Err(fmt.Sprintf(" Cannot export CDR trailer, error: %s", err.Error()))
- return err
- }
- if fmtOut, err := FmtFieldWidth(outVal, cfgFld.Width, cfgFld.Strip, cfgFld.Padding, cfgFld.Mandatory); err != nil {
- engine.Logger.Err(fmt.Sprintf(" Cannot export CDR trailer, error: %s", err.Error()))
- return err
- } else {
- trailer += fmtOut
- }
- }
- if len(trailer) == 0 { // No header data, most likely no configuration fields defined
- return nil
- }
- trailer += "\n" // Done with cdr, postpend new line char
- fwv.trailer.WriteString(trailer)
- return nil
-}
-
-// Write individual cdr into content buffer, build stats
-func (fwv *FixedWidthCdrWriter) WriteCdr(cdr *utils.StoredCdr) error {
- if cdr == nil || len(cdr.CgrId) == 0 { // We do not export empty CDRs
- return nil
- }
- var err error
- cdrRow := ""
- for _, cfgFld := range fwv.exportTemplate.Content.Fields {
- var outVal string
- switch cfgFld.Type {
- case FILLER:
- outVal = cfgFld.Value
- cfgFld.Padding = "right"
- case CONSTANT:
- outVal = cfgFld.Value
- case CDRFIELD:
- outVal, err = fwv.cdrFieldValue(cdr, cfgFld.Value, cfgFld.Layout)
- case CONCATENATED_CDRFIELD:
- for _, fld := range strings.Split(cfgFld.Value, ",") {
- if fldOut, err := fwv.cdrFieldValue(cdr, fld, cfgFld.Layout); err != nil {
- break // The error will be reported bellow
- } else {
- outVal += fldOut
- }
- }
- case METATAG:
- if cfgFld.Value == META_MASKDESTINATION {
- outVal, err = fwv.metaHandler(cfgFld.Value, cdr.FieldAsString(&utils.RSRField{Id: utils.DESTINATION}))
- } else {
- outVal, err = fwv.metaHandler(cfgFld.Value, cfgFld.Layout)
- }
- }
- if err != nil {
- engine.Logger.Err(fmt.Sprintf(" Cannot export CDR with cgrid: %s and runid: %s, error: %s", cdr.CgrId, cdr.MediationRunId, err.Error()))
- return err
- }
- if fmtOut, err := FmtFieldWidth(outVal, cfgFld.Width, cfgFld.Strip, cfgFld.Padding, cfgFld.Mandatory); err != nil {
- engine.Logger.Err(fmt.Sprintf(" Cannot export CDR with cgrid: %s, runid: %s, fieldName: %s, fieldValue: %s, error: %s", cdr.CgrId, cdr.MediationRunId, cfgFld.Name, outVal, err.Error()))
- return err
- } else {
- cdrRow += fmtOut
- }
- }
- if len(cdrRow) == 0 { // No CDR data, most likely no configuration fields defined
- return nil
- }
- cdrRow += "\n" // Done with cdr, postpend new line char
- fwv.content.WriteString(cdrRow)
- // Done with writing content, compute stats here
- if fwv.firstCdrATime.IsZero() || cdr.AnswerTime.Before(fwv.firstCdrATime) {
- fwv.firstCdrATime = cdr.AnswerTime
- }
- if cdr.AnswerTime.After(fwv.lastCdrATime) {
- fwv.lastCdrATime = cdr.AnswerTime
- }
- fwv.numberOfRecords += 1
- if !utils.IsSliceMember([]string{utils.DATA, utils.SMS}, cdr.TOR) { // Only count duration for non data cdrs
- fwv.totalDuration += cdr.Usage
- }
- fwv.totalCost += cdr.Cost
- fwv.totalCost = utils.Round(fwv.totalCost, fwv.roundDecimals, utils.ROUNDING_MIDDLE)
- if fwv.firstExpOrderId > cdr.OrderId || fwv.firstExpOrderId == 0 {
- fwv.firstExpOrderId = cdr.OrderId
- }
- if fwv.lastExpOrderId < cdr.OrderId {
- fwv.lastExpOrderId = cdr.OrderId
- }
- return nil
-}
-
-func (fwv *FixedWidthCdrWriter) Close() {
- if fwv.exportTemplate.Header != nil {
- fwv.ComposeHeader()
- }
- if fwv.exportTemplate.Trailer != nil {
- fwv.ComposeTrailer()
- }
- for _, buf := range []*bytes.Buffer{fwv.header, fwv.content, fwv.trailer} {
- fwv.writer.Write(buf.Bytes())
- }
-}
diff --git a/cdre/fixedwidth_test.go b/cdre/fixedwidth_test.go
index 68647ed78..21b9c3b0f 100644
--- a/cdre/fixedwidth_test.go
+++ b/cdre/fixedwidth_test.go
@@ -21,6 +21,7 @@ package cdre
import (
"bytes"
"github.com/cgrates/cgrates/config"
+ "github.com/cgrates/cgrates/engine"
"github.com/cgrates/cgrates/utils"
"math"
"testing"
@@ -40,24 +41,24 @@ var hdrCfgFlds = []*config.CgrXmlCfgCdrField{
var contentCfgFlds = []*config.CgrXmlCfgCdrField{
&config.CgrXmlCfgCdrField{Name: "TypeOfRecord", Type: CONSTANT, Value: "20", Width: 2},
- &config.CgrXmlCfgCdrField{Name: "Account", Type: CDRFIELD, Value: utils.ACCOUNT, Width: 12, Strip: "left", Padding: "right"},
- &config.CgrXmlCfgCdrField{Name: "Subject", Type: CDRFIELD, Value: utils.SUBJECT, Width: 5, Strip: "right", Padding: "right"},
- &config.CgrXmlCfgCdrField{Name: "CLI", Type: CDRFIELD, Value: "cli", Width: 15, Strip: "xright", Padding: "right"},
- &config.CgrXmlCfgCdrField{Name: "Destination", Type: CDRFIELD, Value: utils.DESTINATION, Width: 24, Strip: "xright", Padding: "right"},
+ &config.CgrXmlCfgCdrField{Name: "Account", Type: utils.CDRFIELD, Value: utils.ACCOUNT, Width: 12, Strip: "left", Padding: "right"},
+ &config.CgrXmlCfgCdrField{Name: "Subject", Type: utils.CDRFIELD, Value: utils.SUBJECT, Width: 5, Strip: "right", Padding: "right"},
+ &config.CgrXmlCfgCdrField{Name: "CLI", Type: utils.CDRFIELD, Value: "cli", Width: 15, Strip: "xright", Padding: "right"},
+ &config.CgrXmlCfgCdrField{Name: "Destination", Type: utils.CDRFIELD, Value: utils.DESTINATION, Width: 24, Strip: "xright", Padding: "right"},
&config.CgrXmlCfgCdrField{Name: "TOR", Type: CONSTANT, Value: "02", Width: 2},
&config.CgrXmlCfgCdrField{Name: "SubtypeTOR", Type: CONSTANT, Value: "11", Width: 4, Padding: "right"},
- &config.CgrXmlCfgCdrField{Name: "SetupTime", Type: CDRFIELD, Value: utils.SETUP_TIME, Width: 12, Strip: "right", Padding: "right", Layout: "020106150400"},
- &config.CgrXmlCfgCdrField{Name: "Duration", Type: CDRFIELD, Value: utils.USAGE, Width: 6, Strip: "right", Padding: "right", Layout: utils.SECONDS},
+ &config.CgrXmlCfgCdrField{Name: "SetupTime", Type: utils.CDRFIELD, Value: utils.SETUP_TIME, Width: 12, Strip: "right", Padding: "right", Layout: "020106150400"},
+ &config.CgrXmlCfgCdrField{Name: "Duration", Type: utils.CDRFIELD, Value: utils.USAGE, Width: 6, Strip: "right", Padding: "right", Layout: utils.SECONDS},
&config.CgrXmlCfgCdrField{Name: "DataVolume", Type: FILLER, Width: 6},
&config.CgrXmlCfgCdrField{Name: "TaxCode", Type: CONSTANT, Value: "1", Width: 1},
- &config.CgrXmlCfgCdrField{Name: "OperatorCode", Type: CDRFIELD, Value: "opercode", Width: 2, Strip: "right", Padding: "right"},
- &config.CgrXmlCfgCdrField{Name: "ProductId", Type: CDRFIELD, Value: "productid", Width: 5, Strip: "right", Padding: "right"},
+ &config.CgrXmlCfgCdrField{Name: "OperatorCode", Type: utils.CDRFIELD, Value: "opercode", Width: 2, Strip: "right", Padding: "right"},
+ &config.CgrXmlCfgCdrField{Name: "ProductId", Type: utils.CDRFIELD, Value: "productid", Width: 5, Strip: "right", Padding: "right"},
&config.CgrXmlCfgCdrField{Name: "NetworkId", Type: CONSTANT, Value: "3", Width: 1},
- &config.CgrXmlCfgCdrField{Name: "CallId", Type: CDRFIELD, Value: utils.ACCID, Width: 16, Padding: "right"},
+ &config.CgrXmlCfgCdrField{Name: "CallId", Type: utils.CDRFIELD, Value: utils.ACCID, Width: 16, Padding: "right"},
&config.CgrXmlCfgCdrField{Name: "Filler", Type: FILLER, Width: 8},
&config.CgrXmlCfgCdrField{Name: "Filler", Type: FILLER, Width: 8},
&config.CgrXmlCfgCdrField{Name: "TerminationCode", Type: CONCATENATED_CDRFIELD, Value: "operator,product", Width: 5, Strip: "right", Padding: "right"},
- &config.CgrXmlCfgCdrField{Name: "Cost", Type: CDRFIELD, Value: utils.COST, Width: 9, Padding: "zeroleft"},
+ &config.CgrXmlCfgCdrField{Name: "Cost", Type: utils.CDRFIELD, Value: utils.COST, Width: 9, Padding: "zeroleft"},
&config.CgrXmlCfgCdrField{Name: "DestinationPrivacy", Type: METATAG, Value: META_MASKDESTINATION, Width: 1},
}
@@ -76,35 +77,33 @@ var trailerCfgFlds = []*config.CgrXmlCfgCdrField{
// Write one CDR and test it's results only for content buffer
func TestWriteCdr(t *testing.T) {
wrBuf := &bytes.Buffer{}
- exportTpl := &config.CgrXmlCdreFwCfg{Header: &config.CgrXmlCfgCdrHeader{Fields: hdrCfgFlds},
- Content: &config.CgrXmlCfgCdrContent{Fields: contentCfgFlds},
- Trailer: &config.CgrXmlCfgCdrTrailer{Fields: trailerCfgFlds},
+ logDb, _ := engine.NewMapStorage()
+ cfg, _ := config.NewDefaultCGRConfig()
+ fixedWidth := utils.CDRE_FIXED_WIDTH
+ exportTpl := &config.CgrXmlCdreCfg{
+ CdrFormat: &fixedWidth,
+ Header: &config.CgrXmlCfgCdrHeader{Fields: hdrCfgFlds},
+ Content: &config.CgrXmlCfgCdrContent{Fields: contentCfgFlds},
+ Trailer: &config.CgrXmlCfgCdrTrailer{Fields: trailerCfgFlds},
}
- fwWriter := FixedWidthCdrWriter{writer: wrBuf, exportTemplate: exportTpl, roundDecimals: 4, header: &bytes.Buffer{}, content: &bytes.Buffer{}, trailer: &bytes.Buffer{}}
- 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", ReqType: "rated", Direction: "*out", Tenant: "cgrates.org",
+ 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",
+ 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),
AnswerTime: time.Date(2013, 11, 7, 8, 42, 26, 0, time.UTC),
Usage: time.Duration(10) * time.Second, MediationRunId: utils.DEFAULT_RUNID, Cost: 2.34567,
ExtraFields: map[string]string{"field_extr1": "val_extr1", "fieldextr2": "valextr2"},
}
- if err := fwWriter.WriteCdr(cdr); err != nil {
+ cdre, err := NewCdrExporter([]*utils.StoredCdr{cdr}, logDb, exportTpl.AsCdreConfig(), "fwv_1", 0.0, 0.0, 0, 4, cfg.RoundingDecimals, "", -1)
+ if err != nil {
t.Error(err)
}
- eContentOut := "201001 1001 1002 0211 07111308420010 1 3dsafdsaf 0002.34570\n"
- contentOut := fwWriter.content.String()
- if len(contentOut) != 145 {
- t.Error("Unexpected content length", len(contentOut))
- } else if contentOut != eContentOut {
- t.Errorf("Content out different than expected. Have <%s>, expecting: <%s>", contentOut, eContentOut)
- }
eHeader := "10 VOI0000007111308420024031415390001 \n"
+ eContentOut := "201001 1001 1002 0211 07111308420010 1 3dsafdsaf 0002.34570\n"
eTrailer := "90 VOI0000000000100000010071113084260071113084200 \n"
- outBeforeWrite := ""
- if wrBuf.String() != outBeforeWrite {
- t.Errorf("Output buffer should be empty before write")
+ if err := cdre.WriteOut(wrBuf); err != nil {
+ t.Error(err)
}
- fwWriter.Close()
allOut := wrBuf.String()
eAllOut := eHeader + eContentOut + eTrailer
if math.Mod(float64(len(allOut)), 145) != 0 {
@@ -113,35 +112,38 @@ func TestWriteCdr(t *testing.T) {
t.Errorf("Output does not match expected length. Have output %q, expecting: %q", allOut, eAllOut)
}
// Test stats
- if !fwWriter.firstCdrATime.Equal(cdr.AnswerTime) {
- t.Error("Unexpected firstCdrATime in stats: ", fwWriter.firstCdrATime)
- } else if !fwWriter.lastCdrATime.Equal(cdr.AnswerTime) {
- t.Error("Unexpected lastCdrATime in stats: ", fwWriter.lastCdrATime)
- } else if fwWriter.numberOfRecords != 1 {
- t.Error("Unexpected number of records in the stats: ", fwWriter.numberOfRecords)
- } else if fwWriter.totalDuration != cdr.Usage {
- t.Error("Unexpected total duration in the stats: ", fwWriter.totalDuration)
- } else if fwWriter.totalCost != utils.Round(cdr.Cost, fwWriter.roundDecimals, utils.ROUNDING_MIDDLE) {
- t.Error("Unexpected total cost in the stats: ", fwWriter.totalCost)
+ if !cdre.firstCdrATime.Equal(cdr.AnswerTime) {
+ t.Error("Unexpected firstCdrATime in stats: ", cdre.firstCdrATime)
+ } else if !cdre.lastCdrATime.Equal(cdr.AnswerTime) {
+ t.Error("Unexpected lastCdrATime in stats: ", cdre.lastCdrATime)
+ } else if cdre.numberOfRecords != 1 {
+ t.Error("Unexpected number of records in the stats: ", cdre.numberOfRecords)
+ } else if cdre.totalDuration != cdr.Usage {
+ t.Error("Unexpected total duration in the stats: ", cdre.totalDuration)
+ } else if cdre.totalCost != utils.Round(cdr.Cost, cdre.roundDecimals, utils.ROUNDING_MIDDLE) {
+ t.Error("Unexpected total cost in the stats: ", cdre.totalCost)
}
- if fwWriter.FirstOrderId() != 1 {
- t.Error("Unexpected FirstOrderId", fwWriter.FirstOrderId())
+ if cdre.FirstOrderId() != 1 {
+ t.Error("Unexpected FirstOrderId", cdre.FirstOrderId())
}
- if fwWriter.LastOrderId() != 1 {
- t.Error("Unexpected LastOrderId", fwWriter.LastOrderId())
+ if cdre.LastOrderId() != 1 {
+ t.Error("Unexpected LastOrderId", cdre.LastOrderId())
}
- if fwWriter.TotalCost() != utils.Round(cdr.Cost, fwWriter.roundDecimals, utils.ROUNDING_MIDDLE) {
- t.Error("Unexpected TotalCost: ", fwWriter.TotalCost())
+ if cdre.TotalCost() != utils.Round(cdr.Cost, cdre.roundDecimals, utils.ROUNDING_MIDDLE) {
+ t.Error("Unexpected TotalCost: ", cdre.TotalCost())
}
}
func TestWriteCdrs(t *testing.T) {
wrBuf := &bytes.Buffer{}
- exportTpl := &config.CgrXmlCdreFwCfg{Header: &config.CgrXmlCfgCdrHeader{Fields: hdrCfgFlds},
- Content: &config.CgrXmlCfgCdrContent{Fields: contentCfgFlds},
- Trailer: &config.CgrXmlCfgCdrTrailer{Fields: trailerCfgFlds},
+ logDb, _ := engine.NewMapStorage()
+ fixedWidth := utils.CDRE_FIXED_WIDTH
+ exportTpl := &config.CgrXmlCdreCfg{
+ CdrFormat: &fixedWidth,
+ Header: &config.CgrXmlCfgCdrHeader{Fields: hdrCfgFlds},
+ Content: &config.CgrXmlCfgCdrContent{Fields: contentCfgFlds},
+ Trailer: &config.CgrXmlCfgCdrTrailer{Fields: trailerCfgFlds},
}
- fwWriter := FixedWidthCdrWriter{writer: wrBuf, exportTemplate: exportTpl, roundDecimals: 4, header: &bytes.Buffer{}, content: &bytes.Buffer{}, trailer: &bytes.Buffer{}}
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",
Category: "call", Account: "1001", Subject: "1001", Destination: "1010",
SetupTime: time.Date(2013, 11, 7, 8, 42, 20, 0, time.UTC),
@@ -164,45 +166,40 @@ func TestWriteCdrs(t *testing.T) {
Usage: time.Duration(20) * time.Second, MediationRunId: utils.DEFAULT_RUNID, Cost: 2.34567,
ExtraFields: map[string]string{"productnumber": "12344", "fieldextr2": "valextr2"},
}
- for _, cdr := range []*utils.StoredCdr{cdr1, cdr2, cdr3, cdr4} {
- if err := fwWriter.WriteCdr(cdr); err != nil {
- t.Error(err)
- }
- contentOut := fwWriter.content.String()
- if math.Mod(float64(len(contentOut)), 145) != 0 { // Rest must be 0 always, so content is always multiple of 145 which is our row fixLength
- t.Error("Unexpected content length", len(contentOut))
- }
+ cfg, _ := config.NewDefaultCGRConfig()
+ cdre, err := NewCdrExporter([]*utils.StoredCdr{cdr1, cdr2, cdr3, cdr4}, logDb, exportTpl.AsCdreConfig(), "fwv_1", 0.0, 0.0, 0, 4, cfg.RoundingDecimals, "", -1)
+ if err != nil {
+ t.Error(err)
}
- if len(wrBuf.String()) != 0 {
- t.Errorf("Output buffer should be empty before write")
+ if err := cdre.WriteOut(wrBuf); err != nil {
+ t.Error(err)
}
- fwWriter.Close()
if len(wrBuf.String()) != 725 {
t.Error("Output buffer does not contain expected info. Expecting len: 725, got: ", len(wrBuf.String()))
}
// Test stats
- if !fwWriter.firstCdrATime.Equal(cdr2.AnswerTime) {
- t.Error("Unexpected firstCdrATime in stats: ", fwWriter.firstCdrATime)
+ if !cdre.firstCdrATime.Equal(cdr2.AnswerTime) {
+ t.Error("Unexpected firstCdrATime in stats: ", cdre.firstCdrATime)
}
- if !fwWriter.lastCdrATime.Equal(cdr4.AnswerTime) {
- t.Error("Unexpected lastCdrATime in stats: ", fwWriter.lastCdrATime)
+ if !cdre.lastCdrATime.Equal(cdr4.AnswerTime) {
+ t.Error("Unexpected lastCdrATime in stats: ", cdre.lastCdrATime)
}
- if fwWriter.numberOfRecords != 3 {
- t.Error("Unexpected number of records in the stats: ", fwWriter.numberOfRecords)
+ if cdre.numberOfRecords != 3 {
+ t.Error("Unexpected number of records in the stats: ", cdre.numberOfRecords)
}
- if fwWriter.totalDuration != time.Duration(330)*time.Second {
- t.Error("Unexpected total duration in the stats: ", fwWriter.totalDuration)
+ if cdre.totalDuration != time.Duration(330)*time.Second {
+ t.Error("Unexpected total duration in the stats: ", cdre.totalDuration)
}
- if fwWriter.totalCost != 5.9957 {
- t.Error("Unexpected total cost in the stats: ", fwWriter.totalCost)
+ if cdre.totalCost != 5.9957 {
+ t.Error("Unexpected total cost in the stats: ", cdre.totalCost)
}
- if fwWriter.FirstOrderId() != 2 {
- t.Error("Unexpected FirstOrderId", fwWriter.FirstOrderId())
+ if cdre.FirstOrderId() != 2 {
+ t.Error("Unexpected FirstOrderId", cdre.FirstOrderId())
}
- if fwWriter.LastOrderId() != 4 {
- t.Error("Unexpected LastOrderId", fwWriter.LastOrderId())
+ if cdre.LastOrderId() != 4 {
+ t.Error("Unexpected LastOrderId", cdre.LastOrderId())
}
- if fwWriter.TotalCost() != 5.9957 {
- t.Error("Unexpected TotalCost: ", fwWriter.TotalCost())
+ if cdre.TotalCost() != 5.9957 {
+ t.Error("Unexpected TotalCost: ", cdre.TotalCost())
}
}
diff --git a/cdre/libfixedwidth.go b/cdre/libcdre.go
similarity index 100%
rename from cdre/libfixedwidth.go
rename to cdre/libcdre.go
diff --git a/cdre/libfixedwidth_test.go b/cdre/libcdre_test.go
similarity index 100%
rename from cdre/libfixedwidth_test.go
rename to cdre/libcdre_test.go
diff --git a/config/cdreconfig.go b/config/cdreconfig.go
new file mode 100644
index 000000000..4f8ad96f6
--- /dev/null
+++ b/config/cdreconfig.go
@@ -0,0 +1,223 @@
+/*
+Real-time Charging System for Telecom & ISP environments
+Copyright (C) 2012-2014 ITsysCOM GmbH
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see
+*/
+
+package config
+
+import (
+ "errors"
+ "github.com/cgrates/cgrates/utils"
+)
+
+// Converts a list of field identifiers into proper CDR field content
+func NewCdreCdrFieldsFromIds(fldsIds ...string) ([]*CdreCdrField, error) {
+ cdrFields := make([]*CdreCdrField, len(fldsIds))
+ for idx, fldId := range fldsIds {
+ if parsedRsr, err := utils.NewRSRField(fldId); err != nil {
+ return nil, err
+ } else {
+ cdrFld := &CdreCdrField{Name: fldId, Type: utils.CDRFIELD, Value: fldId, valueAsRsrField: parsedRsr}
+ if err := cdrFld.setDefaultFixedWidthProperties(); err != nil { // Set default fixed width properties to be used later if needed
+ return nil, err
+ }
+ cdrFields[idx] = cdrFld
+ }
+ }
+ return cdrFields, nil
+}
+
+func NewDefaultCdreConfig() (*CdreConfig, error) {
+ cdreCfg := new(CdreConfig)
+ if err := cdreCfg.setDefaults(); err != nil {
+ return nil, err
+ }
+ return cdreCfg, nil
+}
+
+// One instance of CdrExporter
+type CdreConfig struct {
+ CdrFormat string
+ DataUsageMultiplyFactor float64
+ CostMultiplyFactor float64
+ CostRoundingDecimals int
+ CostShiftDigits int
+ MaskDestId string
+ MaskLength int
+ ExportDir string
+ HeaderFields []*CdreCdrField
+ ContentFields []*CdreCdrField
+ TrailerFields []*CdreCdrField
+}
+
+// Set here defaults
+func (cdreCfg *CdreConfig) setDefaults() error {
+ cdreCfg.CdrFormat = utils.CSV
+ cdreCfg.DataUsageMultiplyFactor = 0.0
+ cdreCfg.CostMultiplyFactor = 0.0
+ cdreCfg.CostRoundingDecimals = -1
+ cdreCfg.CostShiftDigits = 0
+ cdreCfg.MaskDestId = ""
+ cdreCfg.MaskLength = 0
+ cdreCfg.ExportDir = "/var/log/cgrates/cdre"
+ if flds, err := NewCdreCdrFieldsFromIds(utils.CGRID, utils.MEDI_RUNID, utils.TOR, utils.ACCID, utils.REQTYPE, utils.DIRECTION, utils.TENANT,
+ utils.CATEGORY, utils.ACCOUNT, utils.SUBJECT, utils.DESTINATION, utils.SETUP_TIME, utils.ANSWER_TIME, utils.USAGE, utils.COST); err != nil {
+ return err
+ } else {
+ cdreCfg.ContentFields = flds
+ }
+ return nil
+}
+
+type CdreCdrField struct {
+ Name string
+ Type string
+ Value string
+ Width int
+ Strip string
+ Padding string
+ Layout string
+ Mandatory bool
+ valueAsRsrField *utils.RSRField // Cached if the need arrises
+}
+
+func (cdrField *CdreCdrField) ValueAsRSRField() *utils.RSRField {
+ return cdrField.valueAsRsrField
+}
+
+// Should be called on .fwv configuration without providing default values for fixed with parameters
+func (cdrField *CdreCdrField) setDefaultFixedWidthProperties() error {
+ if cdrField.valueAsRsrField == nil {
+ return errors.New("Missing valueAsRsrField")
+ }
+ switch cdrField.valueAsRsrField.Id {
+ case utils.CGRID:
+ cdrField.Width = 10
+ cdrField.Strip = "xright"
+ cdrField.Padding = ""
+ cdrField.Layout = ""
+ cdrField.Mandatory = true
+ case utils.ORDERID:
+ cdrField.Width = 10
+ cdrField.Strip = "xright"
+ cdrField.Padding = ""
+ cdrField.Layout = ""
+ cdrField.Mandatory = true
+ case utils.TOR:
+ cdrField.Width = 10
+ cdrField.Strip = "xright"
+ cdrField.Padding = ""
+ cdrField.Layout = ""
+ cdrField.Mandatory = true
+ case utils.ACCID:
+ cdrField.Width = 10
+ cdrField.Strip = "xright"
+ cdrField.Padding = ""
+ cdrField.Layout = ""
+ cdrField.Mandatory = true
+ case utils.CDRHOST:
+ cdrField.Width = 10
+ cdrField.Strip = "xright"
+ cdrField.Padding = ""
+ cdrField.Layout = ""
+ cdrField.Mandatory = true
+ case utils.CDRSOURCE:
+ cdrField.Width = 10
+ cdrField.Strip = "xright"
+ cdrField.Padding = ""
+ cdrField.Layout = ""
+ cdrField.Mandatory = true
+ case utils.REQTYPE:
+ cdrField.Width = 10
+ cdrField.Strip = "xright"
+ cdrField.Padding = ""
+ cdrField.Layout = ""
+ cdrField.Mandatory = true
+ case utils.DIRECTION:
+ cdrField.Width = 10
+ cdrField.Strip = "xright"
+ cdrField.Padding = ""
+ cdrField.Layout = ""
+ cdrField.Mandatory = true
+ case utils.TENANT:
+ cdrField.Width = 10
+ cdrField.Strip = "xright"
+ cdrField.Padding = ""
+ cdrField.Layout = ""
+ cdrField.Mandatory = true
+ case utils.CATEGORY:
+ cdrField.Width = 10
+ cdrField.Strip = "xright"
+ cdrField.Padding = ""
+ cdrField.Layout = ""
+ cdrField.Mandatory = true
+ case utils.ACCOUNT:
+ cdrField.Width = 10
+ cdrField.Strip = "xright"
+ cdrField.Padding = ""
+ cdrField.Layout = ""
+ cdrField.Mandatory = true
+ case utils.SUBJECT:
+ cdrField.Width = 10
+ cdrField.Strip = "xright"
+ cdrField.Padding = ""
+ cdrField.Layout = ""
+ cdrField.Mandatory = true
+ case utils.DESTINATION:
+ cdrField.Width = 10
+ cdrField.Strip = "xright"
+ cdrField.Padding = ""
+ cdrField.Layout = ""
+ cdrField.Mandatory = true
+ case utils.SETUP_TIME:
+ cdrField.Width = 10
+ cdrField.Strip = "xright"
+ cdrField.Padding = ""
+ cdrField.Layout = "2006-01-02T15:04:05Z07:00"
+ cdrField.Mandatory = true
+ case utils.ANSWER_TIME:
+ cdrField.Width = 10
+ cdrField.Strip = "xright"
+ cdrField.Padding = ""
+ cdrField.Layout = "2006-01-02T15:04:05Z07:00"
+ cdrField.Mandatory = true
+ case utils.USAGE:
+ cdrField.Width = 10
+ cdrField.Strip = "xright"
+ cdrField.Padding = ""
+ cdrField.Layout = ""
+ cdrField.Mandatory = true
+ case utils.MEDI_RUNID:
+ cdrField.Width = 10
+ cdrField.Strip = "xright"
+ cdrField.Padding = ""
+ cdrField.Layout = ""
+ cdrField.Mandatory = true
+ case utils.COST:
+ cdrField.Width = 10
+ cdrField.Strip = "xright"
+ cdrField.Padding = ""
+ cdrField.Layout = ""
+ cdrField.Mandatory = true
+ default:
+ cdrField.Width = 10
+ cdrField.Strip = "xright"
+ cdrField.Padding = ""
+ cdrField.Layout = ""
+ cdrField.Mandatory = true
+ }
+ return nil
+}
diff --git a/config/cdreconfig_test.go b/config/cdreconfig_test.go
new file mode 100644
index 000000000..8a2f8d6ba
--- /dev/null
+++ b/config/cdreconfig_test.go
@@ -0,0 +1,240 @@
+/*
+Real-time Charging System for Telecom & ISP environments
+Copyright (C) 2012-2014 ITsysCOM GmbH
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see
+*/
+
+package config
+
+import (
+ "github.com/cgrates/cgrates/utils"
+ "reflect"
+ "testing"
+)
+
+func TestCdreCfgNewCdreCdrFieldsFromIds(t *testing.T) {
+ expectedFlds := []*CdreCdrField{
+ &CdreCdrField{
+ Name: utils.CGRID,
+ Type: utils.CDRFIELD,
+ Value: utils.CGRID,
+ Width: 10,
+ Strip: "xright",
+ Mandatory: true,
+ valueAsRsrField: &utils.RSRField{Id: utils.CGRID},
+ },
+ &CdreCdrField{
+ Name: "extra1",
+ Type: utils.CDRFIELD,
+ Value: "extra1",
+ Width: 10,
+ Strip: "xright",
+ Mandatory: true,
+ valueAsRsrField: &utils.RSRField{Id: "extra1"},
+ },
+ }
+ if cdreFlds, err := NewCdreCdrFieldsFromIds(utils.CGRID, "extra1"); err != nil {
+ t.Error(err)
+ } else if !reflect.DeepEqual(expectedFlds, cdreFlds) {
+ t.Errorf("Expected: %v, received: %v", expectedFlds, cdreFlds)
+ }
+}
+
+func TestCdreCfgValueAsRSRField(t *testing.T) {
+ cdreCdrFld := &CdreCdrField{
+ Name: utils.CGRID,
+ Type: utils.CDRFIELD,
+ Value: utils.CGRID,
+ Width: 10,
+ Strip: "xright",
+ Mandatory: true,
+ valueAsRsrField: &utils.RSRField{Id: utils.CGRID},
+ }
+ if rsrVal := cdreCdrFld.ValueAsRSRField(); rsrVal != cdreCdrFld.valueAsRsrField {
+ t.Error("Unexpected value received: ", rsrVal)
+ }
+}
+func TestCdreCfgSetDefaultFixedWidthProperties(t *testing.T) {
+ cdreCdrFld := &CdreCdrField{
+ valueAsRsrField: &utils.RSRField{Id: utils.CGRID},
+ }
+ eCdreCdrFld := &CdreCdrField{
+ Width: 10,
+ Strip: "xright",
+ Mandatory: true,
+ valueAsRsrField: &utils.RSRField{Id: utils.CGRID},
+ }
+ if err := cdreCdrFld.setDefaultFixedWidthProperties(); err != nil {
+ t.Error(err)
+ } else if !reflect.DeepEqual(eCdreCdrFld, cdreCdrFld) {
+ t.Errorf("Expecting: %v, received: %v", eCdreCdrFld, cdreCdrFld)
+ }
+}
+
+func TestCdreCfgNewDefaultCdreConfig(t *testing.T) {
+ eCdreCfg := new(CdreConfig)
+ eCdreCfg.CdrFormat = utils.CSV
+ eCdreCfg.DataUsageMultiplyFactor = 0.0
+ eCdreCfg.CostMultiplyFactor = 0.0
+ eCdreCfg.CostRoundingDecimals = -1
+ eCdreCfg.CostShiftDigits = 0
+ eCdreCfg.MaskDestId = ""
+ eCdreCfg.MaskLength = 0
+ eCdreCfg.ExportDir = "/var/log/cgrates/cdre"
+ eCdreCfg.ContentFields = []*CdreCdrField{
+ &CdreCdrField{
+ Name: utils.CGRID,
+ Type: utils.CDRFIELD,
+ Value: utils.CGRID,
+ Width: 10,
+ Strip: "xright",
+ Mandatory: true,
+ valueAsRsrField: &utils.RSRField{Id: utils.CGRID},
+ },
+ &CdreCdrField{
+ Name: utils.MEDI_RUNID,
+ Type: utils.CDRFIELD,
+ Value: utils.MEDI_RUNID,
+ Width: 10,
+ Strip: "xright",
+ Mandatory: true,
+ valueAsRsrField: &utils.RSRField{Id: utils.MEDI_RUNID},
+ },
+ &CdreCdrField{
+ Name: utils.TOR,
+ Type: utils.CDRFIELD,
+ Value: utils.TOR,
+ Width: 10,
+ Strip: "xright",
+ Mandatory: true,
+ valueAsRsrField: &utils.RSRField{Id: utils.TOR},
+ },
+ &CdreCdrField{
+ Name: utils.ACCID,
+ Type: utils.CDRFIELD,
+ Value: utils.ACCID,
+ Width: 10,
+ Strip: "xright",
+ Mandatory: true,
+ valueAsRsrField: &utils.RSRField{Id: utils.ACCID},
+ },
+ &CdreCdrField{
+ Name: utils.REQTYPE,
+ Type: utils.CDRFIELD,
+ Value: utils.REQTYPE,
+ Width: 10,
+ Strip: "xright",
+ Mandatory: true,
+ valueAsRsrField: &utils.RSRField{Id: utils.REQTYPE},
+ },
+ &CdreCdrField{
+ Name: utils.DIRECTION,
+ Type: utils.CDRFIELD,
+ Value: utils.DIRECTION,
+ Width: 10,
+ Strip: "xright",
+ Mandatory: true,
+ valueAsRsrField: &utils.RSRField{Id: utils.DIRECTION},
+ },
+ &CdreCdrField{
+ Name: utils.TENANT,
+ Type: utils.CDRFIELD,
+ Value: utils.TENANT,
+ Width: 10,
+ Strip: "xright",
+ Mandatory: true,
+ valueAsRsrField: &utils.RSRField{Id: utils.TENANT},
+ },
+ &CdreCdrField{
+ Name: utils.CATEGORY,
+ Type: utils.CDRFIELD,
+ Value: utils.CATEGORY,
+ Width: 10,
+ Strip: "xright",
+ Mandatory: true,
+ valueAsRsrField: &utils.RSRField{Id: utils.CATEGORY},
+ },
+ &CdreCdrField{
+ Name: utils.ACCOUNT,
+ Type: utils.CDRFIELD,
+ Value: utils.ACCOUNT,
+ Width: 10,
+ Strip: "xright",
+ Mandatory: true,
+ valueAsRsrField: &utils.RSRField{Id: utils.ACCOUNT},
+ },
+ &CdreCdrField{
+ Name: utils.SUBJECT,
+ Type: utils.CDRFIELD,
+ Value: utils.SUBJECT,
+ Width: 10,
+ Strip: "xright",
+ Mandatory: true,
+ valueAsRsrField: &utils.RSRField{Id: utils.SUBJECT},
+ },
+ &CdreCdrField{
+ Name: utils.DESTINATION,
+ Type: utils.CDRFIELD,
+ Value: utils.DESTINATION,
+ Width: 10,
+ Strip: "xright",
+ Mandatory: true,
+ valueAsRsrField: &utils.RSRField{Id: utils.DESTINATION},
+ },
+ &CdreCdrField{
+ Name: utils.SETUP_TIME,
+ Type: utils.CDRFIELD,
+ Value: utils.SETUP_TIME,
+ Width: 10,
+ Strip: "xright",
+ Layout: "2006-01-02T15:04:05Z07:00",
+ Mandatory: true,
+ valueAsRsrField: &utils.RSRField{Id: utils.SETUP_TIME},
+ },
+ &CdreCdrField{
+ Name: utils.ANSWER_TIME,
+ Type: utils.CDRFIELD,
+ Value: utils.ANSWER_TIME,
+ Width: 10,
+ Strip: "xright",
+ Layout: "2006-01-02T15:04:05Z07:00",
+ Mandatory: true,
+ valueAsRsrField: &utils.RSRField{Id: utils.ANSWER_TIME},
+ },
+ &CdreCdrField{
+ Name: utils.USAGE,
+ Type: utils.CDRFIELD,
+ Value: utils.USAGE,
+ Width: 10,
+ Strip: "xright",
+ Mandatory: true,
+ valueAsRsrField: &utils.RSRField{Id: utils.USAGE},
+ },
+ &CdreCdrField{
+ Name: utils.COST,
+ Type: utils.CDRFIELD,
+ Value: utils.COST,
+ Width: 10,
+ Strip: "xright",
+ Mandatory: true,
+ valueAsRsrField: &utils.RSRField{Id: utils.COST},
+ },
+ }
+ if cdreCfg, err := NewDefaultCdreConfig(); err != nil {
+ t.Error(err)
+ } else if !reflect.DeepEqual(eCdreCfg, cdreCfg) {
+ t.Errorf("Expecting: %v, received: %v", eCdreCfg, cdreCfg)
+ }
+}
diff --git a/config/config.go b/config/config.go
index b7b11b2b1..410613a87 100644
--- a/config/config.go
+++ b/config/config.go
@@ -90,13 +90,7 @@ type CGRConfig struct {
CDRSEnabled bool // Enable CDR Server service
CDRSExtraFields []*utils.RSRField // Extra fields to store in CDRs
CDRSMediator string // Address where to reach the Mediator. Empty for disabling mediation. <""|internal>
- CdreCdrFormat string // Format of the exported CDRs.
- CdreMaskDestId string // Id of the destination list to be masked in CDRs
- CdreMaskLength int // Number of digits to mask in the destination suffix if destination is in the MaskDestinationdsId
- CdreCostShiftDigits int // Shift digits in the cost on export (eg: convert from EUR to cents)
- CdreDir string // Path towards exported cdrs directory
- CdreExportedFields []*utils.RSRField // List of fields in the exported CDRs
- CdreFWXmlTemplate *CgrXmlCdreFwCfg // Use this configuration as export template in case of fixed fields length
+ CdreDefaultInstance *CdreConfig // Will be used in the case no specific one selected by API
CdrcEnabled bool // Enable CDR client functionality
CdrcCdrs string // Address where to reach CDR server
CdrcRunDelay time.Duration // Sleep interval between consecutive runs, 0 to use automation via inotify
@@ -167,11 +161,7 @@ func (self *CGRConfig) setDefaults() error {
self.CDRSEnabled = false
self.CDRSExtraFields = []*utils.RSRField{}
self.CDRSMediator = ""
- self.CdreCdrFormat = "csv"
- self.CdreMaskDestId = ""
- self.CdreMaskLength = 0
- self.CdreCostShiftDigits = 0
- self.CdreDir = "/var/log/cgrates/cdre"
+ self.CdreDefaultInstance, _ = NewDefaultCdreConfig()
self.CdrcEnabled = false
self.CdrcCdrs = utils.INTERNAL
self.CdrcRunDelay = time.Duration(0)
@@ -217,35 +207,10 @@ func (self *CGRConfig) setDefaults() error {
self.MailerAuthUser = "cgrates"
self.MailerAuthPass = "CGRateS.org"
self.MailerFromAddr = "cgr-mailer@localhost.localdomain"
- self.CdreExportedFields = []*utils.RSRField{
- &utils.RSRField{Id: utils.CGRID},
- &utils.RSRField{Id: utils.MEDI_RUNID},
- &utils.RSRField{Id: utils.TOR},
- &utils.RSRField{Id: utils.ACCID},
- &utils.RSRField{Id: utils.REQTYPE},
- &utils.RSRField{Id: utils.DIRECTION},
- &utils.RSRField{Id: utils.TENANT},
- &utils.RSRField{Id: utils.CATEGORY},
- &utils.RSRField{Id: utils.ACCOUNT},
- &utils.RSRField{Id: utils.SUBJECT},
- &utils.RSRField{Id: utils.DESTINATION},
- &utils.RSRField{Id: utils.SETUP_TIME},
- &utils.RSRField{Id: utils.ANSWER_TIME},
- &utils.RSRField{Id: utils.USAGE},
- &utils.RSRField{Id: utils.COST},
- }
return nil
}
func (self *CGRConfig) checkConfigSanity() error {
- // Cdre sanity check for fixed_width
- if self.CdreCdrFormat == utils.CDRE_FIXED_WIDTH {
- if self.XmlCfgDocument == nil {
- return errors.New("Need XmlConfigurationDocument for fixed_width cdr export")
- } else if self.CdreFWXmlTemplate == nil {
- return errors.New("Need XmlTemplate for fixed_width cdr export")
- }
- }
if self.CdrcEnabled {
if len(self.CdrcCdrFields) == 0 {
return errors.New("CdrC enabled but no fields to be processed defined!")
@@ -253,7 +218,6 @@ func (self *CGRConfig) checkConfigSanity() error {
if self.CdrcCdrType == utils.CSV {
for _, rsrFld := range self.CdrcCdrFields {
if _, errConv := strconv.Atoi(rsrFld.Id); errConv != nil {
- fmt.Println("5")
return fmt.Errorf("CDR fields must be indices in case of .csv files, have instead: %s", rsrFld.Id)
}
}
@@ -426,33 +390,42 @@ func loadConfig(c *conf.ConfigFile) (*CGRConfig, error) {
cfg.CDRSMediator, _ = c.GetString("cdrs", "mediator")
}
if hasOpt = c.HasOption("cdre", "cdr_format"); hasOpt {
- cfg.CdreCdrFormat, _ = c.GetString("cdre", "cdr_format")
+ cfg.CdreDefaultInstance.CdrFormat, _ = c.GetString("cdre", "cdr_format")
}
if hasOpt = c.HasOption("cdre", "mask_destination_id"); hasOpt {
- cfg.CdreMaskDestId, _ = c.GetString("cdre", "mask_destination_id")
+ cfg.CdreDefaultInstance.MaskDestId, _ = c.GetString("cdre", "mask_destination_id")
}
if hasOpt = c.HasOption("cdre", "mask_length"); hasOpt {
- cfg.CdreMaskLength, _ = c.GetInt("cdre", "mask_length")
+ cfg.CdreDefaultInstance.MaskLength, _ = c.GetInt("cdre", "mask_length")
+ }
+ if hasOpt = c.HasOption("cdre", "data_usage_multiply_factor"); hasOpt {
+ cfg.CdreDefaultInstance.DataUsageMultiplyFactor, _ = c.GetFloat64("cdre", "data_usage_multiply_factor")
+ }
+ if hasOpt = c.HasOption("cdre", "cost_multiply_factor"); hasOpt {
+ cfg.CdreDefaultInstance.CostMultiplyFactor, _ = c.GetFloat64("cdre", "cost_multiply_factor")
+ }
+ if hasOpt = c.HasOption("cdre", "cost_rounding_decimals"); hasOpt {
+ cfg.CdreDefaultInstance.CostRoundingDecimals, _ = c.GetInt("cdre", "cost_rounding_decimals")
}
if hasOpt = c.HasOption("cdre", "cost_shift_digits"); hasOpt {
- cfg.CdreCostShiftDigits, _ = c.GetInt("cdre", "cost_shift_digits")
+ cfg.CdreDefaultInstance.CostShiftDigits, _ = c.GetInt("cdre", "cost_shift_digits")
}
if hasOpt = c.HasOption("cdre", "export_template"); hasOpt { // Load configs for csv normally from template, fixed_width from xml file
exportTemplate, _ := c.GetString("cdre", "export_template")
- if cfg.CdreCdrFormat != utils.CDRE_FIXED_WIDTH { // Csv most likely
- if extraFields, err := ParseRSRFields(exportTemplate); err != nil {
+ if strings.HasPrefix(exportTemplate, utils.XML_PROFILE_PREFIX) {
+ if xmlTemplates := cfg.XmlCfgDocument.GetCdreCfgs(exportTemplate[len(utils.XML_PROFILE_PREFIX):]); xmlTemplates != nil {
+ cfg.CdreDefaultInstance = xmlTemplates[exportTemplate[len(utils.XML_PROFILE_PREFIX):]].AsCdreConfig()
+ }
+ } else { // Not loading out of template
+ if flds, err := NewCdreCdrFieldsFromIds(strings.Split(exportTemplate, string(utils.CSV_SEP))...); err != nil {
return nil, err
} else {
- cfg.CdreExportedFields = extraFields
- }
- } else if strings.HasPrefix(exportTemplate, utils.XML_PROFILE_PREFIX) {
- if xmlTemplate := cfg.XmlCfgDocument.GetCdreFWCfgs(exportTemplate[len(utils.XML_PROFILE_PREFIX):]); xmlTemplate != nil {
- cfg.CdreFWXmlTemplate = xmlTemplate[exportTemplate[len(utils.XML_PROFILE_PREFIX):]]
+ cfg.CdreDefaultInstance.ContentFields = flds
}
}
}
if hasOpt = c.HasOption("cdre", "export_dir"); hasOpt {
- cfg.CdreDir, _ = c.GetString("cdre", "export_dir")
+ cfg.CdreDefaultInstance.ExportDir, _ = c.GetString("cdre", "export_dir")
}
if hasOpt = c.HasOption("cdrc", "enabled"); hasOpt {
cfg.CdrcEnabled, _ = c.GetBool("cdrc", "enabled")
diff --git a/config/config_local_test.go b/config/config_local_test.go
index 5461126af..a643337fa 100644
--- a/config/config_local_test.go
+++ b/config/config_local_test.go
@@ -39,7 +39,7 @@ func TestLoadXmlCfg(t *testing.T) {
if cfg.XmlCfgDocument == nil {
t.Error("Did not load the XML Config Document")
}
- if cdreFWCfg := cfg.XmlCfgDocument.GetCdreFWCfgs("CDREFW-A"); cdreFWCfg == nil {
+ if cdreFWCfg := cfg.XmlCfgDocument.GetCdreCfgs("CDREFW-A"); cdreFWCfg == nil {
t.Error("Could not retrieve CDRExporter FixedWidth config instance")
}
}
diff --git a/config/config_test.go b/config/config_test.go
index 02ffe5392..1297bd5bb 100644
--- a/config/config_test.go
+++ b/config/config_test.go
@@ -1,6 +1,6 @@
/*
-Rating system designed to be used in VoIP Carriers World
-Copyright (C) 2013 ITsysCOM
+Real-time Charging System for Telecom & ISP environments
+Copyright (C) 2012-2014 ITsysCOM GmbH
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@@ -78,14 +78,10 @@ func TestDefaults(t *testing.T) {
eCfg.RaterBalancer = ""
eCfg.BalancerEnabled = false
eCfg.SchedulerEnabled = false
+ eCfg.CdreDefaultInstance, _ = NewDefaultCdreConfig()
eCfg.CDRSEnabled = false
eCfg.CDRSExtraFields = []*utils.RSRField{}
eCfg.CDRSMediator = ""
- eCfg.CdreCdrFormat = "csv"
- eCfg.CdreMaskDestId = ""
- eCfg.CdreMaskLength = 0
- eCfg.CdreCostShiftDigits = 0
- eCfg.CdreDir = "/var/log/cgrates/cdre"
eCfg.CdrcEnabled = false
eCfg.CdrcCdrs = utils.INTERNAL
eCfg.CdrcRunDelay = time.Duration(0)
@@ -131,23 +127,6 @@ func TestDefaults(t *testing.T) {
eCfg.MailerAuthUser = "cgrates"
eCfg.MailerAuthPass = "CGRateS.org"
eCfg.MailerFromAddr = "cgr-mailer@localhost.localdomain"
- eCfg.CdreExportedFields = []*utils.RSRField{
- &utils.RSRField{Id: utils.CGRID},
- &utils.RSRField{Id: utils.MEDI_RUNID},
- &utils.RSRField{Id: utils.TOR},
- &utils.RSRField{Id: utils.ACCID},
- &utils.RSRField{Id: utils.REQTYPE},
- &utils.RSRField{Id: utils.DIRECTION},
- &utils.RSRField{Id: utils.TENANT},
- &utils.RSRField{Id: utils.CATEGORY},
- &utils.RSRField{Id: utils.ACCOUNT},
- &utils.RSRField{Id: utils.SUBJECT},
- &utils.RSRField{Id: utils.DESTINATION},
- &utils.RSRField{Id: utils.SETUP_TIME},
- &utils.RSRField{Id: utils.ANSWER_TIME},
- &utils.RSRField{Id: utils.USAGE},
- &utils.RSRField{Id: utils.COST},
- }
if !reflect.DeepEqual(cfg, eCfg) {
t.Log(eCfg)
t.Log(cfg)
@@ -165,10 +144,6 @@ func TestSanityCheck(t *testing.T) {
t.Error("Invalid defaults: ", err)
}
cfg = &CGRConfig{}
- cfg.CdreCdrFormat = utils.CDRE_FIXED_WIDTH
- if err := cfg.checkConfigSanity(); err == nil {
- t.Error("Failed to detect fixed_width dependency on xml configuration")
- }
cfg.CdrcEnabled = true
if err := cfg.checkConfigSanity(); err == nil {
t.Error("Failed to detect missing CDR fields definition")
@@ -228,12 +203,16 @@ func TestConfigFromFile(t *testing.T) {
eCfg.CDRSEnabled = true
eCfg.CDRSExtraFields = []*utils.RSRField{&utils.RSRField{Id: "test"}}
eCfg.CDRSMediator = "test"
- eCfg.CdreCdrFormat = "test"
- eCfg.CdreMaskDestId = "test"
- eCfg.CdreMaskLength = 99
- eCfg.CdreCostShiftDigits = 99
- eCfg.CdreExportedFields = []*utils.RSRField{&utils.RSRField{Id: "test"}}
- eCfg.CdreDir = "test"
+ eCfg.CdreDefaultInstance = &CdreConfig{
+ CdrFormat: "test",
+ DataUsageMultiplyFactor: 99.0,
+ CostMultiplyFactor: 99.0,
+ CostRoundingDecimals: 99,
+ CostShiftDigits: 99,
+ MaskDestId: "test",
+ MaskLength: 99,
+ ExportDir: "test"}
+ eCfg.CdreDefaultInstance.ContentFields, _ = NewCdreCdrFieldsFromIds("test")
eCfg.CdrcEnabled = true
eCfg.CdrcCdrs = "test"
eCfg.CdrcRunDelay = time.Duration(99) * time.Second
@@ -326,17 +305,32 @@ func TestCdreExtraFields(t *testing.T) {
cdr_format = csv
export_template = cgrid,mediation_runid,accid
`)
+ expectedFlds := []*CdreCdrField{
+ &CdreCdrField{Name: "cgrid", Type: utils.CDRFIELD, Value: "cgrid", valueAsRsrField: &utils.RSRField{Id: "cgrid"}, Width: 10, Strip: "xright", Mandatory: true},
+ &CdreCdrField{Name: "mediation_runid", Type: utils.CDRFIELD, Value: "mediation_runid", valueAsRsrField: &utils.RSRField{Id: "mediation_runid"},
+ Width: 10, Strip: "xright", Mandatory: true},
+ &CdreCdrField{Name: "accid", Type: utils.CDRFIELD, Value: "accid", valueAsRsrField: &utils.RSRField{Id: "accid"}, Width: 10, Strip: "xright", Mandatory: true},
+ }
+ expCdreCfg := &CdreConfig{CdrFormat: utils.CSV, 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.CdreExportedFields, []*utils.RSRField{&utils.RSRField{Id: "cgrid"}, &utils.RSRField{Id: "mediation_runid"}, &utils.RSRField{Id: "accid"}}) {
- t.Errorf("Unexpected value for CdrsExtraFields: %v", cfg.CDRSExtraFields)
+ } else if !reflect.DeepEqual(cfg.CdreDefaultInstance, expCdreCfg) {
+ t.Errorf("Expecting: %v, received: %v", expCdreCfg, cfg.CdreDefaultInstance)
}
eFieldsCfg = []byte(`[cdre]
cdr_format = csv
-export_template = cgrid,mediation_runid,accid,
+export_template = cgrid,~effective_caller_id_number:s/(\d+)/+$1/
`)
- if _, err := NewCGRConfigFromBytes(eFieldsCfg); err == nil {
- t.Error("Failed to detect empty field in the end of export_template defition")
+ rsrField, _ := utils.NewRSRField(`~effective_caller_id_number:s/(\d+)/+$1/`)
+ expectedFlds = []*CdreCdrField{
+ &CdreCdrField{Name: "cgrid", Type: utils.CDRFIELD, Value: "cgrid", valueAsRsrField: &utils.RSRField{Id: "cgrid"}, Width: 10, Strip: "xright", Mandatory: true},
+ &CdreCdrField{Name: `~effective_caller_id_number:s/(\d+)/+$1/`, Type: utils.CDRFIELD, Value: `~effective_caller_id_number:s/(\d+)/+$1/`, valueAsRsrField: rsrField,
+ Width: 10, Strip: "xright", Mandatory: true}}
+ expCdreCfg.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) {
+ t.Errorf("Expecting: %v, received: %v", expCdreCfg, cfg.CdreDefaultInstance)
}
eFieldsCfg = []byte(`[cdre]
cdr_format = csv
diff --git a/config/test_data.txt b/config/test_data.txt
index 6ee120180..94095517b 100644
--- a/config/test_data.txt
+++ b/config/test_data.txt
@@ -30,7 +30,6 @@ default_tenant = test # Default Tenant to consider when missing from requests.
default_subject = test # Default rating Subject to consider when missing from requests.
rounding_decimals = 99 # Number of decimals to round floats/costs at
-
[balancer]
enabled = true # Start Balancer service: .
@@ -47,12 +46,15 @@ extra_fields = test # Extra fields to scategorye in CDRs
mediator = test # Address where to reach the Mediacategory. Empty for disabling mediation. <""|internal>
[cdre]
-cdr_format = test # Exported CDRs format
-mask_destination_id = test # Destination id containing called addresses to be masked on export
-mask_length = 99 # Length of the destination suffix to be masked
-cost_shift_digits = 99 # Shift the number of cost
-export_dir = test # Path where the exported CDRs will be placed
-export_template = test # List of fields in the exported CDRs
+cdr_format = test # Exported CDRs format
+data_usage_multiply_factor = 99.0 # Multiply data usage before export (eg: convert from KBytes to Bytes)
+cost_multiply_factor = 99.0 # Multiply cost before export (0.0 to disable), eg: add VAT
+cost_rounding_decimals = 99 # Rounding decimals for Cost values. -1 to disable rounding
+cost_shift_digits = 99 # Shift digits in the cost on export (eg: convert from EUR to cents)
+mask_destination_id = test # Destination id containing called addresses to be masked on export
+mask_length = 99 # Length of the destination suffix to be masked
+export_dir = test # Path where the exported CDRs will be placed
+export_template = test # List of fields in the exported CDRs
[cdrc]
enabled = true # Enable CDR client functionality
diff --git a/config/xmlcdre.go b/config/xmlcdre.go
index a1a68da82..bd0be8cea 100644
--- a/config/xmlcdre.go
+++ b/config/xmlcdre.go
@@ -20,13 +20,69 @@ package config
import (
"encoding/xml"
+ "github.com/cgrates/cgrates/utils"
)
-// The CdrExporter Fixed Width configuration instance
-type CgrXmlCdreFwCfg struct {
- Header *CgrXmlCfgCdrHeader `xml:"header"`
- Content *CgrXmlCfgCdrContent `xml:"content"`
- Trailer *CgrXmlCfgCdrTrailer `xml:"trailer"`
+// The CdrExporter configuration instance
+type CgrXmlCdreCfg struct {
+ CdrFormat *string `xml:"cdr_format"`
+ DataUsageMultiplyFactor *float64 `xml:"data_usage_multiply_factor"`
+ CostMultiplyFactor *float64 `xml:"cost_multiply_factor"`
+ CostRoundingDecimals *int `xml:"cost_rounding_decimals"`
+ CostShiftDigits *int `xml:"cost_shift_digits"`
+ MaskDestId *string `xml:"mask_destination_id"`
+ MaskLength *int `xml:"mask_length"`
+ ExportDir *string `xml:"export_dir"`
+ Header *CgrXmlCfgCdrHeader `xml:"export_template>header"`
+ Content *CgrXmlCfgCdrContent `xml:"export_template>content"`
+ Trailer *CgrXmlCfgCdrTrailer `xml:"export_template>trailer"`
+}
+
+func (xmlCdreCfg *CgrXmlCdreCfg) AsCdreConfig() *CdreConfig {
+ cdreCfg, _ := NewDefaultCdreConfig()
+ if xmlCdreCfg.CdrFormat != nil {
+ cdreCfg.CdrFormat = *xmlCdreCfg.CdrFormat
+ }
+ if xmlCdreCfg.DataUsageMultiplyFactor != nil {
+ cdreCfg.DataUsageMultiplyFactor = *xmlCdreCfg.DataUsageMultiplyFactor
+ }
+ if xmlCdreCfg.CostMultiplyFactor != nil {
+ cdreCfg.CostMultiplyFactor = *xmlCdreCfg.CostMultiplyFactor
+ }
+ if xmlCdreCfg.CostRoundingDecimals != nil {
+ cdreCfg.CostRoundingDecimals = *xmlCdreCfg.CostRoundingDecimals
+ }
+ if xmlCdreCfg.CostShiftDigits != nil {
+ cdreCfg.CostShiftDigits = *xmlCdreCfg.CostShiftDigits
+ }
+ if xmlCdreCfg.MaskDestId != nil {
+ cdreCfg.MaskDestId = *xmlCdreCfg.MaskDestId
+ }
+ if xmlCdreCfg.MaskLength != nil {
+ cdreCfg.MaskLength = *xmlCdreCfg.MaskLength
+ }
+ if xmlCdreCfg.ExportDir != nil {
+ cdreCfg.ExportDir = *xmlCdreCfg.ExportDir
+ }
+ if xmlCdreCfg.Header != nil {
+ cdreCfg.HeaderFields = make([]*CdreCdrField, len(xmlCdreCfg.Header.Fields))
+ for idx, xmlFld := range xmlCdreCfg.Header.Fields {
+ cdreCfg.HeaderFields[idx] = xmlFld.AsCdreCdrField()
+ }
+ }
+ if xmlCdreCfg.Content != nil {
+ cdreCfg.ContentFields = make([]*CdreCdrField, len(xmlCdreCfg.Content.Fields))
+ for idx, xmlFld := range xmlCdreCfg.Content.Fields {
+ cdreCfg.ContentFields[idx] = xmlFld.AsCdreCdrField()
+ }
+ }
+ if xmlCdreCfg.Trailer != nil {
+ cdreCfg.TrailerFields = make([]*CdreCdrField, len(xmlCdreCfg.Trailer.Fields))
+ for idx, xmlFld := range xmlCdreCfg.Trailer.Fields {
+ cdreCfg.TrailerFields[idx] = xmlFld.AsCdreCdrField()
+ }
+ }
+ return cdreCfg
}
// CDR header
@@ -49,13 +105,42 @@ type CgrXmlCfgCdrTrailer struct {
// CDR field
type CgrXmlCfgCdrField struct {
- XMLName xml.Name `xml:"field"`
- Name string `xml:"name,attr"`
- Type string `xml:"type,attr"`
- Value string `xml:"value,attr"`
- Width int `xml:"width,attr"` // Field width
- Strip string `xml:"strip,attr"` // Strip strategy in case value is bigger than field width <""|left|xleft|right|xright>
- Padding string `xml:"padding,attr"` // Padding strategy in case of value is smaller than width <""left|zeroleft|right>
- Layout string `xml:"layout,attr"` // Eg. time format layout
- Mandatory bool `xml:"mandatory,attr"` // If field is mandatory, empty value will be considered as error and CDR will not be exported
+ XMLName xml.Name `xml:"field"`
+ Name string `xml:"name,attr"`
+ Type string `xml:"type,attr"`
+ Value string `xml:"value,attr"`
+ Width int `xml:"width,attr"` // Field width
+ Strip string `xml:"strip,attr"` // Strip strategy in case value is bigger than field width <""|left|xleft|right|xright>
+ Padding string `xml:"padding,attr"` // Padding strategy in case of value is smaller than width <""left|zeroleft|right>
+ Layout string `xml:"layout,attr"` // Eg. time format layout
+ Mandatory bool `xml:"mandatory,attr"` // If field is mandatory, empty value will be considered as error and CDR will not be exported
+ valueAsRsrField *utils.RSRField // Cached if the need arrises
+}
+
+func (cdrFld *CgrXmlCfgCdrField) populateRSRField() (err error) {
+ if cdrFld.Type != utils.CDRFIELD { // We only need rsrField in case of cdrfield type
+ return nil
+ }
+ if cdrFld.valueAsRsrField, err = utils.NewRSRField(cdrFld.Value); err != nil {
+ return err
+ }
+ return nil
+}
+
+func (cdrFld *CgrXmlCfgCdrField) ValueAsRSRField() *utils.RSRField {
+ return cdrFld.valueAsRsrField
+}
+
+func (cdrFld *CgrXmlCfgCdrField) AsCdreCdrField() *CdreCdrField {
+ return &CdreCdrField{
+ Name: cdrFld.Name,
+ Type: cdrFld.Type,
+ Value: cdrFld.Value,
+ Width: cdrFld.Width,
+ Strip: cdrFld.Strip,
+ Padding: cdrFld.Padding,
+ Layout: cdrFld.Layout,
+ Mandatory: cdrFld.Mandatory,
+ valueAsRsrField: cdrFld.valueAsRsrField,
+ }
}
diff --git a/config/xmlcdre_test.go b/config/xmlcdre_test.go
index d6568d1d4..287261b70 100644
--- a/config/xmlcdre_test.go
+++ b/config/xmlcdre_test.go
@@ -19,65 +19,96 @@ along with this program. If not, see
package config
import (
+ "github.com/cgrates/cgrates/utils"
+ "reflect"
"strings"
"testing"
)
var cfgDoc *CgrXmlCfgDocument // Will be populated by first test
-func TestParseXmlConfig(t *testing.T) {
- cfgXmlStr := `
+func TestXmlCdreCfgPopulateCdreRSRFIeld(t *testing.T) {
+ cdreField := CgrXmlCfgCdrField{Name: "TEST1", Type: "cdrfield", Value: `~effective_caller_id_number:s/(\d+)/+$1/`}
+ if err := cdreField.populateRSRField(); err != nil {
+ t.Error("Unexpected error: ", err.Error())
+ } else if cdreField.valueAsRsrField == nil {
+ t.Error("Failed loading the RSRField")
+ }
+ valRSRField, _ := utils.NewRSRField(`~effective_caller_id_number:s/(\d+)/+$1/`)
+ if recv := cdreField.ValueAsRSRField(); !reflect.DeepEqual(valRSRField, recv) {
+ t.Errorf("Expecting %v, received %v", valRSRField, recv)
+ }
+ cdreField = CgrXmlCfgCdrField{Name: "TEST1", Type: "constant", Value: `someval`}
+ if err := cdreField.populateRSRField(); err != nil {
+ t.Error("Unexpected error: ", err.Error())
+ } else if cdreField.valueAsRsrField != nil {
+ t.Error("Should not load the RSRField")
+ }
+}
+
+func TestXmlCdreCfgParseXmlConfig(t *testing.T) {
+ cfgXmlStr := `
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ fwv
+ 0.0
+ 0.0
+ -1
+ 0
+ MASKED_DESTINATIONS
+ 0
+ /var/log/cgrates/cdre
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
`
var err error
@@ -87,13 +118,13 @@ func TestParseXmlConfig(t *testing.T) {
} else if cfgDoc == nil {
t.Fatal("Could not parse xml configuration document")
}
- if len(cfgDoc.cdrefws) != 1 {
+ if len(cfgDoc.cdres) != 1 {
t.Error("Did not cache")
}
}
-func TestGetCdreFWCfg(t *testing.T) {
- cdreFWCfg := cfgDoc.GetCdreFWCfgs("CDRE-FW1")
+func TestXmlCdreCfgGetCdreCfg(t *testing.T) {
+ cdreFWCfg := cfgDoc.GetCdreCfgs("CDRE-FW1")
if cdreFWCfg == nil {
t.Error("Could not parse CdreFw instance")
}
@@ -107,3 +138,114 @@ func TestGetCdreFWCfg(t *testing.T) {
t.Error("Unexpected number of trailer fields parsed", len(cdreFWCfg["CDRE-FW1"].Trailer.Fields))
}
}
+
+func TestXmlCdreCfgAsCdreConfig(t *testing.T) {
+ cfgXmlStr := `
+
+
+ fwv
+ 1024.0
+ 1.19
+ -1
+ -3
+ MASKED_DESTINATIONS
+ 1
+ /var/log/cgrates/cdre
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`
+ var err error
+ reader := strings.NewReader(cfgXmlStr)
+ if cfgDoc, err = ParseCgrXmlConfig(reader); err != nil {
+ t.Error(err.Error())
+ } else if cfgDoc == nil {
+ t.Fatal("Could not parse xml configuration document")
+ }
+ xmlCdreCfgs := cfgDoc.GetCdreCfgs("CDRE-FW2")
+ if xmlCdreCfgs == nil {
+ t.Error("Could not parse XmlCdre instance")
+ }
+ eCdreCfg := &CdreConfig{
+ CdrFormat: "fwv",
+ DataUsageMultiplyFactor: 1024.0,
+ CostMultiplyFactor: 1.19,
+ CostRoundingDecimals: -1,
+ CostShiftDigits: -3,
+ MaskDestId: "MASKED_DESTINATIONS",
+ MaskLength: 1,
+ ExportDir: "/var/log/cgrates/cdre",
+ }
+ eCdreCfg.HeaderFields = []*CdreCdrField{
+ &CdreCdrField{
+ Name: "TypeOfRecord",
+ Type: "constant",
+ Value: "10",
+ Width: 2},
+ &CdreCdrField{
+ Name: "LastCdr",
+ Type: "metatag",
+ Value: "last_cdr_time",
+ Layout: "020106150400",
+ Width: 12},
+ }
+ eCdreCfg.ContentFields = []*CdreCdrField{
+ &CdreCdrField{
+ Name: "OperatorCode",
+ Type: "cdrfield",
+ Value: "operator",
+ Width: 2,
+ valueAsRsrField: &utils.RSRField{Id: "operator"},
+ },
+ &CdreCdrField{
+ Name: "ProductId",
+ Type: "cdrfield",
+ Value: "productid",
+ Width: 5,
+ valueAsRsrField: &utils.RSRField{Id: "productid"},
+ },
+ &CdreCdrField{
+ Name: "NetworkId",
+ Type: "constant",
+ Value: "3",
+ Width: 1,
+ },
+ }
+ eCdreCfg.TrailerFields = []*CdreCdrField{
+ &CdreCdrField{
+ Name: "DistributorCode",
+ Type: "constant",
+ Value: "VOI",
+ Width: 3,
+ },
+ &CdreCdrField{
+ Name: "FileSeqNr",
+ Type: "metatag",
+ Value: "export_id",
+ Width: 5,
+ Padding: "zeroleft",
+ },
+ }
+ if rcvCdreCfg := xmlCdreCfgs["CDRE-FW2"].AsCdreConfig(); !reflect.DeepEqual(rcvCdreCfg, eCdreCfg) {
+ t.Errorf("Expecting: %v, received: %v", eCdreCfg, rcvCdreCfg)
+ }
+}
diff --git a/config/xmlconfig.go b/config/xmlconfig.go
index be9137447..fbaedb7d7 100644
--- a/config/xmlconfig.go
+++ b/config/xmlconfig.go
@@ -40,11 +40,11 @@ func ParseCgrXmlConfig(reader io.Reader) (*CgrXmlCfgDocument, error) {
// Define a format for configuration file, one doc contains more configuration instances, identified by section, type and id
type CgrXmlCfgDocument struct {
- XMLName xml.Name `xml:"document"`
- Type string `xml:"type,attr"`
- Configurations []*CgrXmlConfiguration `xml:"configuration"`
- cdrefws map[string]*CgrXmlCdreFwCfg // Cache for processed fixed width config instances, key will be the id of the instance
+ XMLName xml.Name `xml:"document"`
+ Type string `xml:"type,attr"`
+ Configurations []*CgrXmlConfiguration `xml:"configuration"`
cdrcs map[string]*CgrXmlCdrcCfg
+ cdres map[string]*CgrXmlCdreCfg // Cahe cdrexporter instances, key will be the ID
}
// Storage for raw configuration
@@ -63,7 +63,7 @@ func (cfgInst *CgrXmlConfiguration) rawConfigElement() []byte {
}
func (xmlCfg *CgrXmlCfgDocument) cacheAll() error {
- for _, cacheFunc := range []func() error{xmlCfg.cacheCdreFWCfgs, xmlCfg.cacheCdrcCfgs} {
+ for _, cacheFunc := range []func() error{xmlCfg.cacheCdrcCfgs, xmlCfg.cacheCdreCfgs} {
if err := cacheFunc(); err != nil {
return err
}
@@ -71,26 +71,6 @@ func (xmlCfg *CgrXmlCfgDocument) cacheAll() error {
return nil
}
-// Avoid building from raw config string always, so build cache here
-func (xmlCfg *CgrXmlCfgDocument) cacheCdreFWCfgs() error {
- xmlCfg.cdrefws = make(map[string]*CgrXmlCdreFwCfg)
- for _, cfgInst := range xmlCfg.Configurations {
- if cfgInst.Section == utils.CDRE || cfgInst.Type == utils.CDRE_FIXED_WIDTH {
- cdrefwCfg := new(CgrXmlCdreFwCfg)
- rawConfig := append([]byte(""), cfgInst.RawConfig...) // Encapsulate the rawConfig in one element so we can Unmarshall into one struct
- rawConfig = append(rawConfig, []byte("")...)
- if err := xml.Unmarshal(rawConfig, cdrefwCfg); err != nil {
- return err
- } else if cdrefwCfg == nil {
- return fmt.Errorf("Could not unmarshal CgrXmlCdreFwCfg: %s", cfgInst.Id)
- } else { // All good, cache the config instance
- xmlCfg.cdrefws[cfgInst.Id] = cdrefwCfg
- }
- }
- }
- return nil
-}
-
// Avoid building from raw config string always, so build cache here
func (xmlCfg *CgrXmlCfgDocument) cacheCdrcCfgs() error {
xmlCfg.cdrcs = make(map[string]*CgrXmlCdrcCfg)
@@ -116,16 +96,42 @@ func (xmlCfg *CgrXmlCfgDocument) cacheCdrcCfgs() error {
return nil
}
+// Avoid building from raw config string always, so build cache here
+func (xmlCfg *CgrXmlCfgDocument) cacheCdreCfgs() error {
+ xmlCfg.cdres = make(map[string]*CgrXmlCdreCfg)
+ for _, cfgInst := range xmlCfg.Configurations {
+ if cfgInst.Section != utils.CDRE {
+ continue
+ }
+ cdreCfg := new(CgrXmlCdreCfg)
+ if err := xml.Unmarshal(cfgInst.rawConfigElement(), cdreCfg); err != nil {
+ return err
+ } else if cdreCfg == nil {
+ return fmt.Errorf("Could not unmarshal CgrXmlCdreCfg: %s", cfgInst.Id)
+ }
+ if cdreCfg.Content != nil {
+ // Cache rsr fields
+ for _, fld := range cdreCfg.Content.Fields {
+ if err := fld.populateRSRField(); err != nil {
+ return fmt.Errorf("Populating field %s, error: %s", fld.Name, err.Error())
+ }
+ }
+ }
+ xmlCfg.cdres[cfgInst.Id] = cdreCfg
+ }
+ return nil
+}
+
// Return instances or filtered instance of cdrefw configuration
-func (xmlCfg *CgrXmlCfgDocument) GetCdreFWCfgs(instName string) map[string]*CgrXmlCdreFwCfg {
+func (xmlCfg *CgrXmlCfgDocument) GetCdreCfgs(instName string) map[string]*CgrXmlCdreCfg {
if len(instName) != 0 {
- if cfg, hasIt := xmlCfg.cdrefws[instName]; !hasIt {
+ if cfg, hasIt := xmlCfg.cdres[instName]; !hasIt {
return nil
} else {
- return map[string]*CgrXmlCdreFwCfg{instName: cfg}
+ return map[string]*CgrXmlCdreCfg{instName: cfg}
}
}
- return xmlCfg.cdrefws
+ return xmlCfg.cdres
}
// Return instances or filtered instance of cdrc configuration
diff --git a/data/conf/cgrates.cfg b/data/conf/cgrates.cfg
index d853f134e..7a0a47a5e 100644
--- a/data/conf/cgrates.cfg
+++ b/data/conf/cgrates.cfg
@@ -28,11 +28,10 @@
# rpc_gob_listen = 127.0.0.1:2013 # RPC GOB listening address
# http_listen = 127.0.0.1:2080 # HTTP listening address
# default_reqtype = rated # Default request type to consider when missing from requests: <""|prepaid|postpaid|pseudoprepaid|rated>.
-# default_category = call # Default Type of Record to consider when missing from requests.
+# default_category = call # Default Type of Record to consider when missing from requests.
# default_tenant = cgrates.org # Default Tenant to consider when missing from requests.
# default_subject = cgrates # Default rating Subject to consider when missing from requests.
-# rounding_method = *middle # Rounding method for floats/costs: <*up|*middle|*down>
-# rounding_decimals = 10 # Number of decimals to round float/costs at
+# rounding_decimals = 10 # System level precision for floats
# xmlcfg_path = # Path towards additional config defined in xml file
[balancer]
@@ -52,9 +51,12 @@
[cdre]
# cdr_format = csv # Exported CDRs format
+# data_usage_multiply_factor = 0.0 # Multiply data usage before export (eg: convert from KBytes to Bytes)
+# cost_multiply_factor = 0.0 # Multiply cost before export (0.0 to disable), eg: add VAT
+# cost_rounding_decimals = -1 # Rounding decimals for Cost values. -1 to disable rounding
+# cost_shift_digits = 0 # Shift digits in the cost on export (eg: convert from EUR to cents)
# mask_destination_id = # Destination id containing called addresses to be masked on export
# mask_length = 0 # Length of the destination suffix to be masked
-# cost_shift_digits = 0 # Shift cost on export with the number of digits digits defined here (eg: convert from Eur to cent).
# export_dir = /var/log/cgrates/cdre # Path where the exported CDRs will be placed
# export_template = cgrid,mediation_runid,tor,accid,reqtype,direction,tenant,category,account,subject,destination,setup_time,answer_time,usage,cost
# Exported fields template <""|fld1,fld2|*xml:instance_name>
diff --git a/data/conf/samples/cgr_addconfig.xml b/data/conf/samples/cgr_addconfig.xml
index 6bc5260e0..415abb7ed 100644
--- a/data/conf/samples/cgr_addconfig.xml
+++ b/data/conf/samples/cgr_addconfig.xml
@@ -1,20 +1,29 @@
-
+
-
-
-
-
-
-
-
-
-
-
-
+ fwv
+ 0.0
+ 0.0
+ 0
+ MASKED_DESTINATIONS
+ 0
+ /var/log/cgrates/cdre
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/data/conf/samples/multiplecdrc_fwexport.xml b/data/conf/samples/multiplecdrc_fwexport.xml
index 693e56557..fc34de389 100644
--- a/data/conf/samples/multiplecdrc_fwexport.xml
+++ b/data/conf/samples/multiplecdrc_fwexport.xml
@@ -49,54 +49,63 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ fwv
+ 0.0
+ 0.0
+ 0
+ MASKED_DESTINATIONS
+ 0
+ /var/log/cgrates/cdre
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/utils/apitpdata.go b/utils/apitpdata.go
index be639e5df..ad33714cc 100644
--- a/utils/apitpdata.go
+++ b/utils/apitpdata.go
@@ -327,33 +327,35 @@ type CachedItemAge struct {
}
type AttrExpFileCdrs struct {
- CdrFormat string // Cdr output file format
- 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
- ExportTemplate string // Exported fields template <""|fld1,fld2|*xml:instance_name>
- CostShiftDigits int // If defined it will shift cost digits before applying rouding (eg: convert from Eur->cents), -1 to use general config ones
- RoundDecimals int // Overwrite configured roundDecimals with this dynamically, -1 to use general config ones
- MaskDestinationId string // Overwrite configured MaskDestId
- MaskLength int // Overwrite configured MaskLength, -1 to use general config ones
- CgrIds []string // If provided, it will filter based on the cgrids present in list
- MediationRunId []string // If provided, it will filter on mediation runid
- TOR []string // If provided, filter on TypeOfRecord
- CdrHost []string // If provided, it will filter cdrhost
- CdrSource []string // If provided, it will filter cdrsource
- ReqType []string // If provided, it will fiter reqtype
- Direction []string // If provided, it will fiter direction
- Tenant []string // If provided, it will filter tenant
- Category []string // If provided, it will filter çategory
- Account []string // If provided, it will filter account
- Subject []string // If provided, it will filter the rating subject
- DestinationPrefix []string // If provided, it will filter on destination prefix
- OrderIdStart int64 // Export from this order identifier
- OrderIdEnd int64 // Export smaller than this order identifier
- TimeStart string // If provided, it will represent the starting of the CDRs interval (>=)
- TimeEnd string // If provided, it will represent the end of the CDRs interval (<)
- SkipErrors bool // Do not export errored CDRs
- SkipRated bool // Do not export rated CDRs
+ CdrFormat *string // Cdr output file format
+ 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
+ ExportTemplate *string // Exported fields template <""|fld1,fld2|*xml:instance_name>
+ DataUsageMultiplyFactor *float64 // Multiply data usage before export (eg: convert from KBytes to Bytes)
+ CostMultiplyFactor *float64 // Multiply the cost before export, eg: apply VAT
+ CostShiftDigits *int // If defined it will shift cost digits before applying rouding (eg: convert from Eur->cents), -1 to use general config ones
+ RoundDecimals *int // Overwrite configured roundDecimals with this dynamically, -1 to use general config ones
+ MaskDestinationId *string // Overwrite configured MaskDestId
+ MaskLength *int // Overwrite configured MaskLength, -1 to use general config ones
+ CgrIds []string // If provided, it will filter based on the cgrids present in list
+ MediationRunId []string // If provided, it will filter on mediation runid
+ TOR []string // If provided, filter on TypeOfRecord
+ CdrHost []string // If provided, it will filter cdrhost
+ CdrSource []string // If provided, it will filter cdrsource
+ ReqType []string // If provided, it will fiter reqtype
+ Direction []string // If provided, it will fiter direction
+ Tenant []string // If provided, it will filter tenant
+ Category []string // If provided, it will filter çategory
+ Account []string // If provided, it will filter account
+ Subject []string // If provided, it will filter the rating subject
+ DestinationPrefix []string // If provided, it will filter on destination prefix
+ OrderIdStart int64 // Export from this order identifier
+ OrderIdEnd int64 // Export smaller than this order identifier
+ TimeStart string // If provided, it will represent the starting of the CDRs interval (>=)
+ TimeEnd string // If provided, it will represent the end of the CDRs interval (<)
+ SkipErrors bool // Do not export errored CDRs
+ SkipRated bool // Do not export rated CDRs
}
type ExportedFileCdrs struct {
diff --git a/utils/consts.go b/utils/consts.go
index 263ae2023..2000e444b 100644
--- a/utils/consts.go
+++ b/utils/consts.go
@@ -119,6 +119,7 @@ const (
OUT = "*out"
CDR_IMPORT = "cdr_import"
CDR_EXPORT = "cdr_export"
+ CDRFIELD = "cdrfield"
)
var (
diff --git a/utils/storedcdr.go b/utils/storedcdr.go
index 08fe024e8..aedd8c057 100644
--- a/utils/storedcdr.go
+++ b/utils/storedcdr.go
@@ -50,19 +50,17 @@ type StoredCdr struct {
Cost float64
}
-// Should only be used for display purposes, bad otherwise.
-// cdrDirection: CDR_IMPORT or CDR_EXPORT
-func (storedCdr *StoredCdr) MangleDataUsage(cdrDirection string) {
- if IsSliceMember([]string{DATA, SMS}, storedCdr.TOR) {
- if cdrDirection == CDR_IMPORT { // On import CDRs usages are converted to nanoseconds, for data we need seconds, fix it here.
- storedCdr.Usage = time.Duration(storedCdr.Usage.Nanoseconds()) * time.Second
- } else if cdrDirection == CDR_EXPORT { // On exports we need to show the data back in seconds instead of internally stored as nanoseconds
- storedCdr.Usage = time.Duration(int(Round(storedCdr.Usage.Seconds(), 0, ROUNDING_MIDDLE)))
- }
- }
+// Used to multiply usage on export
+func (storedCdr *StoredCdr) UsageMultiply(multiplyFactor float64, roundDecimals int) {
+ storedCdr.Usage = time.Duration(int(Round(float64(storedCdr.Usage.Nanoseconds())*multiplyFactor, roundDecimals, ROUNDING_MIDDLE))) // Rounding down could introduce a slight loss here but only at nanoseconds level
}
-// Return cost as string, formated with number of decimals configured
+// Used to multiply cost on export
+func (storedCdr *StoredCdr) CostMultiply(multiplyFactor float64, roundDecimals int) {
+ storedCdr.Cost = Round(storedCdr.Cost*multiplyFactor, roundDecimals, ROUNDING_MIDDLE)
+}
+
+// Format cost as string on export
func (storedCdr *StoredCdr) FormatCost(shiftDecimals, roundDecimals int) string {
cost := storedCdr.Cost
if shiftDecimals != 0 {
@@ -71,18 +69,18 @@ func (storedCdr *StoredCdr) FormatCost(shiftDecimals, roundDecimals int) string
return strconv.FormatFloat(cost, 'f', roundDecimals, 64)
}
-// Rounds up so 0.8 seconds will become 1
+// Formats usage on export
func (storedCdr *StoredCdr) FormatUsage(layout string) string {
if IsSliceMember([]string{DATA, SMS}, storedCdr.TOR) {
return strconv.FormatFloat(Round(storedCdr.Usage.Seconds(), 0, ROUNDING_MIDDLE), 'f', -1, 64)
}
switch layout {
case HOURS:
- return strconv.FormatFloat(math.Ceil(storedCdr.Usage.Hours()), 'f', -1, 64)
+ return strconv.FormatFloat(Round(storedCdr.Usage.Seconds(), 0, ROUNDING_MIDDLE), 'f', -1, 64)
case MINUTES:
- return strconv.FormatFloat(math.Ceil(storedCdr.Usage.Minutes()), 'f', -1, 64)
+ return strconv.FormatFloat(Round(storedCdr.Usage.Seconds(), 0, ROUNDING_MIDDLE), 'f', -1, 64)
case SECONDS:
- return strconv.FormatFloat(math.Ceil(storedCdr.Usage.Seconds()), 'f', -1, 64)
+ return strconv.FormatFloat(Round(storedCdr.Usage.Seconds(), 0, ROUNDING_MIDDLE), 'f', -1, 64)
default:
return strconv.FormatInt(storedCdr.Usage.Nanoseconds(), 10)
}
diff --git a/utils/storedcdr_test.go b/utils/storedcdr_test.go
index 1941e188b..4f1590703 100644
--- a/utils/storedcdr_test.go
+++ b/utils/storedcdr_test.go
@@ -76,28 +76,28 @@ func TestFieldAsString(t *testing.T) {
cdr.FieldAsString(&RSRField{Id: "fieldextr2"}) != cdr.ExtraFields["fieldextr2"],
cdr.FieldAsString(&RSRField{Id: "dummy_field"}) != "")
}
- /*cdr.TOR = DATA
- if formated := cdr.FieldAsString(&RSRField{Id: USAGE}); formated != "10" {
- t.Error("Wrong exported value for data field: ", formated)
- }*/
}
-func TestMangleDataUsage(t *testing.T) {
- cdr := StoredCdr{TOR: DATA, Usage: time.Duration(1640113)}
- if cdr.MangleDataUsage(CDR_IMPORT); cdr.Usage != time.Duration(1640113000000000) {
- t.Error("Unexpected usage after mangling: ", cdr.Usage)
+func TestUsageMultiply(t *testing.T) {
+ cdr := StoredCdr{Usage: time.Duration(10) * time.Second}
+ if cdr.UsageMultiply(1024.0, 0); cdr.Usage != time.Duration(10240)*time.Second {
+ t.Errorf("Unexpected usage after multiply: %v", cdr.Usage.Nanoseconds())
}
- cdr = StoredCdr{TOR: VOICE, Usage: time.Duration(1640113000000000)}
- if cdr.MangleDataUsage(CDR_IMPORT); cdr.Usage != time.Duration(1640113000000000) {
- t.Error("Unexpected usage after mangling: ", cdr.Usage)
+ cdr = StoredCdr{Usage: time.Duration(10240) * time.Second} // Simulate conversion back, gives out a bit odd result but this can be rounded on export
+ expectDuration, _ := time.ParseDuration("10.000005120s")
+ if cdr.UsageMultiply(0.000976563, 0); cdr.Usage != expectDuration {
+ t.Errorf("Unexpected usage after multiply: %v", cdr.Usage.Nanoseconds())
}
- cdr = StoredCdr{TOR: DATA, Usage: time.Duration(1640113000000000)}
- if cdr.MangleDataUsage(CDR_EXPORT); cdr.Usage != time.Duration(1640113) {
- t.Error("Unexpected usage after mangling: ", cdr.Usage)
+}
+
+func TestCostMultiply(t *testing.T) {
+ cdr := StoredCdr{Cost: 1.01}
+ if cdr.CostMultiply(1.19, 4); cdr.Cost != 1.2019 {
+ t.Error("Unexpected cost after multiply: %v", cdr.Cost)
}
- cdr = StoredCdr{TOR: VOICE, Usage: time.Duration(1640113000000000)}
- if cdr.MangleDataUsage(CDR_EXPORT); cdr.Usage != time.Duration(1640113000000000) {
- t.Error("Unexpected usage after mangling: ", cdr.Usage)
+ cdr = StoredCdr{Cost: 1.01}
+ if cdr.CostMultiply(1000, 0); cdr.Cost != 1010 {
+ t.Error("Unexpected cost after multiply: %v", cdr.Cost)
}
}
diff --git a/utils/utils_local_test.go b/utils/utils_local_test.go
index cd2801a34..bffcdd663 100644
--- a/utils/utils_local_test.go
+++ b/utils/utils_local_test.go
@@ -32,7 +32,7 @@ func TestHttpJsonPost(t *testing.T) {
return
}
cdrOut := &CgrCdrOut{CgrId: Sha1("dsafdsaf", time.Date(2013, 11, 7, 8, 42, 20, 0, time.UTC).String()), OrderId: 123, TOR: VOICE, AccId: "dsafdsaf", CdrHost: "192.168.1.1",
- CdrSource: UNIT_TEST, ReqType: "rated", Direction: "*out", Tenant: "cgrates.org", Category: "call", Account: "1001", Subject: "1001", Destination: "1002",
+ CdrSource: UNIT_TEST, ReqType: "rated", Direction: "*out", Tenant: "cgrates.org", Category: "call", Account: "account1", Subject: "tgooiscs0014", Destination: "1002",
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: DEFAULT_RUNID,
Usage: 0.00000001, ExtraFields: map[string]string{"field_extr1": "val_extr1", "fieldextr2": "valextr2"}, Cost: 1.01,
}