mirror of
https://github.com/cgrates/cgrates.git
synced 2026-02-11 18:16:24 +05:00
Refactored CDRExporter to merge common exports into one mechanism, added CdreConfig type to collect configuration for CDRE from more sources
This commit is contained in:
@@ -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 {
|
||||
|
||||
191
apier/cdre.go
191
apier/cdre.go
@@ -19,9 +19,11 @@ along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 <http://www.gnu.org/licenses/>
|
||||
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("<CdreFw> 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("<CdrExporter> 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("<CdreFw> 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("<CdreFw> 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("<CdreFw> 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("<CdreFw> 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
|
||||
}
|
||||
|
||||
85
cdre/csv.go
85
cdre/csv.go
@@ -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 <http://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
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()
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 <http://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
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("<CdreFw> 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("<CdreFw> 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("<CdreFw> 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("<CdreFw> 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("<CdreFw> 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("<CdreFw> 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())
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
223
config/cdreconfig.go
Normal file
223
config/cdreconfig.go
Normal file
@@ -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 <http://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
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
|
||||
}
|
||||
240
config/cdreconfig_test.go
Normal file
240
config/cdreconfig_test.go
Normal file
@@ -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 <http://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -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. <csv>
|
||||
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")
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: <true|false>.
|
||||
|
||||
@@ -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 <csv>
|
||||
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 <csv>
|
||||
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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,65 +19,96 @@ along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
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 := `<?xml version="1.0" encoding="UTF-8" ?>
|
||||
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 := `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="cgrates/xml">
|
||||
<configuration section="cdre" type="fixed_width" id="CDRE-FW1">
|
||||
<header>
|
||||
<fields>
|
||||
<field name="TypeOfRecord" type="constant" value="10" width="2"/>
|
||||
<field name="Filler1" type="filler" width="3"/>
|
||||
<field name="DistributorCode" type="constant" value="VOI" width="3"/>
|
||||
<field name="FileSeqNr" type="metatag" value="export_id" padding="zeroleft" width="5"/>
|
||||
<field name="LastCdr" type="metatag" value="last_cdr_time" layout="020106150400" width="12"/>
|
||||
<field name="FileCreationfTime" type="metatag" value="time_now" layout="020106150400" width="12"/>
|
||||
<field name="Version" type="constant" value="01" width="2"/>
|
||||
<field name="Filler2" type="filler" width="105"/>
|
||||
</fields>
|
||||
</header>
|
||||
<content>
|
||||
<fields>
|
||||
<field name="TypeOfRecord" type="constant" value="20" width="2"/>
|
||||
<field name="Account" type="cdrfield" value="cgrid" width="12" mandatory="true"/>
|
||||
<field name="Subject" type="cdrfield" value="subject" strip="left" padding="left" width="5"/>
|
||||
<field name="CLI" type="cdrfield" value="cli" strip="xright" width="15"/>
|
||||
<field name="Destination" type="cdrfield" value="destination" strip="xright" width="24"/>
|
||||
<field name="TOR" type="constant" value="02" width="2"/>
|
||||
<field name="SubtypeTOR" type="constant" value="11" width="4"/>
|
||||
<field name="SetupTime" type="cdrfield" value="start_time" layout="020106150400" width="12"/>
|
||||
<field name="Duration" type="cdrfield" value="duration" width="6"/>
|
||||
<field name="DataVolume" type="filler" width="6"/>
|
||||
<field name="TaxCode" type="constant" value="1" width="1"/>
|
||||
<field name="OperatorCode" type="cdrfield" value="operator" width="2"/>
|
||||
<field name="ProductId" type="cdrfield" value="productid" width="5"/>
|
||||
<field name="NetworkId" type="constant" value="3" width="1"/>
|
||||
<field name="CallId" type="cdrfield" value="accid" width="16"/>
|
||||
<field name="Filler" type="filler" width="8"/>
|
||||
<field name="Filler" type="filler" width="8"/>
|
||||
<field name="TerminationCode" type="cdrfield" value='~cost_details:s/"MatchedDestId":".+_(\s\s\s\s\s)"/$1/' width="5"/>
|
||||
<field name="Cost" type="cdrfield" value="cost" padding="zeroleft" width="9"/>
|
||||
<field name="CalledMask" type="cdrfield" value="calledmask" width="1"/>
|
||||
</fields>
|
||||
</content>
|
||||
<trailer>
|
||||
<fields>
|
||||
<field name="TypeOfRecord" type="constant" value="90" width="2"/>
|
||||
<field name="Filler1" type="filler" width="3"/>
|
||||
<field name="DistributorCode" type="constant" value="VOI" width="3"/>
|
||||
<field name="FileSeqNr" type="metatag" value="export_id" padding="zeroleft" width="5"/>
|
||||
<field name="NumberOfRecords" type="metatag" value="cdrs_number" padding="zeroleft" width="6"/>
|
||||
<field name="CdrsDuration" type="metatag" value="cdrs_duration" padding="zeroleft" width="8"/>
|
||||
<field name="FirstCdrTime" type="metatag" value="first_cdr_time" layout="020106150400" width="12"/>
|
||||
<field name="LastCdrTime" type="metatag" value="last_cdr_time" layout="020106150400" width="12"/>
|
||||
<field name="Filler1" type="filler" width="93"/>
|
||||
</fields>
|
||||
</trailer>
|
||||
<cdr_format>fwv</cdr_format>
|
||||
<data_usage_multiply_factor>0.0</data_usage_multiply_factor>
|
||||
<cost_multiply_factor>0.0</cost_multiply_factor>
|
||||
<cost_rounding_decimals>-1</cost_rounding_decimals>
|
||||
<cost_shift_digits>0</cost_shift_digits>
|
||||
<mask_destination_id>MASKED_DESTINATIONS</mask_destination_id>
|
||||
<mask_length>0</mask_length>
|
||||
<export_dir>/var/log/cgrates/cdre</export_dir>
|
||||
<export_template>
|
||||
<header>
|
||||
<fields>
|
||||
<field name="TypeOfRecord" type="constant" value="10" width="2" />
|
||||
<field name="Filler1" type="filler" width="3" />
|
||||
<field name="DistributorCode" type="constant" value="VOI" width="3" />
|
||||
<field name="FileSeqNr" type="metatag" value="export_id" padding="zeroleft" width="5" />
|
||||
<field name="LastCdr" type="metatag" value="last_cdr_time" layout="020106150400" width="12" />
|
||||
<field name="FileCreationfTime" type="metatag" value="time_now" layout="020106150400" width="12" />
|
||||
<field name="Version" type="constant" value="01" width="2" />
|
||||
<field name="Filler2" type="filler" width="105" />
|
||||
</fields>
|
||||
</header>
|
||||
<content>
|
||||
<fields>
|
||||
<field name="TypeOfRecord" type="constant" value="20" width="2" />
|
||||
<field name="Account" type="cdrfield" value="cgrid" width="12" mandatory="true" />
|
||||
<field name="Subject" type="cdrfield" value="subject" strip="left" padding="left" width="5" />
|
||||
<field name="CLI" type="cdrfield" value="cli" strip="xright" width="15" />
|
||||
<field name="Destination" type="cdrfield" value="destination" strip="xright" width="24" />
|
||||
<field name="TOR" type="constant" value="02" width="2" />
|
||||
<field name="SubtypeTOR" type="constant" value="11" width="4" />
|
||||
<field name="SetupTime" type="cdrfield" value="start_time" layout="020106150400" width="12" />
|
||||
<field name="Duration" type="cdrfield" value="duration" width="6" multiply_factor_voice="1000" />
|
||||
<field name="DataVolume" type="filler" width="6" />
|
||||
<field name="TaxCode" type="constant" value="1" width="1" />
|
||||
<field name="OperatorCode" type="cdrfield" value="operator" width="2" />
|
||||
<field name="ProductId" type="cdrfield" value="productid" width="5" />
|
||||
<field name="NetworkId" type="constant" value="3" width="1" />
|
||||
<field name="CallId" type="cdrfield" value="accid" width="16" />
|
||||
<field name="Filler" type="filler" width="8" />
|
||||
<field name="Filler" type="filler" width="8" />
|
||||
<field name="TerminationCode" type="cdrfield" value="~cost_details:s/"MatchedDestId":".+_(\s\s\s\s\s)"/$1/" width="5" />
|
||||
<field name="Cost" type="cdrfield" value="cost" padding="zeroleft" width="9" />
|
||||
<field name="CalledMask" type="cdrfield" value="calledmask" width="1" />
|
||||
</fields>
|
||||
</content>
|
||||
<trailer>
|
||||
<fields>
|
||||
<field name="TypeOfRecord" type="constant" value="90" width="2" />
|
||||
<field name="Filler1" type="filler" width="3" />
|
||||
<field name="DistributorCode" type="constant" value="VOI" width="3" />
|
||||
<field name="FileSeqNr" type="metatag" value="export_id" padding="zeroleft" width="5" />
|
||||
<field name="NumberOfRecords" type="metatag" value="cdrs_number" padding="zeroleft" width="6" />
|
||||
<field name="CdrsDuration" type="metatag" value="cdrs_duration" padding="zeroleft" width="8" />
|
||||
<field name="FirstCdrTime" type="metatag" value="first_cdr_time" layout="020106150400" width="12" />
|
||||
<field name="LastCdrTime" type="metatag" value="last_cdr_time" layout="020106150400" width="12" />
|
||||
<field name="Filler1" type="filler" width="93" />
|
||||
</fields>
|
||||
</trailer>
|
||||
</export_template>
|
||||
</configuration>
|
||||
</document>`
|
||||
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 := `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="cgrates/xml">
|
||||
<configuration section="cdre" type="fixed_width" id="CDRE-FW2">
|
||||
<cdr_format>fwv</cdr_format>
|
||||
<data_usage_multiply_factor>1024.0</data_usage_multiply_factor>
|
||||
<cost_multiply_factor>1.19</cost_multiply_factor>
|
||||
<cost_rounding_decimals>-1</cost_rounding_decimals>
|
||||
<cost_shift_digits>-3</cost_shift_digits>
|
||||
<mask_destination_id>MASKED_DESTINATIONS</mask_destination_id>
|
||||
<mask_length>1</mask_length>
|
||||
<export_dir>/var/log/cgrates/cdre</export_dir>
|
||||
<export_template>
|
||||
<header>
|
||||
<fields>
|
||||
<field name="TypeOfRecord" type="constant" value="10" width="2" />
|
||||
<field name="LastCdr" type="metatag" value="last_cdr_time" layout="020106150400" width="12" />
|
||||
</fields>
|
||||
</header>
|
||||
<content>
|
||||
<fields>
|
||||
<field name="OperatorCode" type="cdrfield" value="operator" width="2" />
|
||||
<field name="ProductId" type="cdrfield" value="productid" width="5" />
|
||||
<field name="NetworkId" type="constant" value="3" width="1" />
|
||||
</fields>
|
||||
</content>
|
||||
<trailer>
|
||||
<fields>
|
||||
<field name="DistributorCode" type="constant" value="VOI" width="3" />
|
||||
<field name="FileSeqNr" type="metatag" value="export_id" padding="zeroleft" width="5" />
|
||||
</fields>
|
||||
</trailer>
|
||||
</export_template>
|
||||
</configuration>
|
||||
</document>`
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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("<element>"), cfgInst.RawConfig...) // Encapsulate the rawConfig in one element so we can Unmarshall into one struct
|
||||
rawConfig = append(rawConfig, []byte("</element>")...)
|
||||
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
|
||||
|
||||
@@ -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 <csv>
|
||||
# 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>
|
||||
|
||||
@@ -1,20 +1,29 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="cgrates/xml">
|
||||
<configuration section="cdre" type="fixed_width" id="CDREFW-A">
|
||||
<header>
|
||||
<fields>
|
||||
<field name="Filler1" type="filler" width="4"/>
|
||||
</fields>
|
||||
</header>
|
||||
<content>
|
||||
<fields>
|
||||
<field name="TypeOfRecord" type="constant" value="call"/>
|
||||
</fields>
|
||||
</content>
|
||||
<trailer>
|
||||
<fields>
|
||||
<field name="Filler1" type="filler" width="3"/>
|
||||
</fields>
|
||||
</trailer>
|
||||
<cdr_format>fwv</cdr_format>
|
||||
<data_usage_multiply_factor>0.0</data_usage_multiply_factor>
|
||||
<cost_multiply_factor>0.0</cost_multiply_factor>
|
||||
<cost_shift_digits>0</cost_shift_digits>
|
||||
<mask_destination_id>MASKED_DESTINATIONS</mask_destination_id>
|
||||
<mask_length>0</mask_length>
|
||||
<export_dir>/var/log/cgrates/cdre</export_dir>
|
||||
<export_template>
|
||||
<header>
|
||||
<fields>
|
||||
<field name="Filler1" type="filler" width="4" />
|
||||
</fields>
|
||||
</header>
|
||||
<content>
|
||||
<fields>
|
||||
<field name="TypeOfRecord" type="constant" value="call" />
|
||||
</fields>
|
||||
</content>
|
||||
<trailer>
|
||||
<fields>
|
||||
<field name="Filler1" type="filler" width="3" />
|
||||
</fields>
|
||||
</trailer>
|
||||
</export_template>
|
||||
</configuration>
|
||||
</document>
|
||||
|
||||
@@ -49,54 +49,63 @@
|
||||
</fields>
|
||||
</configuration>
|
||||
<configuration section="cdre" type="fwv" id="CDRE-FW1">
|
||||
<header>
|
||||
<fields>
|
||||
<field name="ToR" type="constant" value="10" width="2" />
|
||||
<field name="Filler1" type="filler" width="3" />
|
||||
<field name="FileType" type="constant" value="SIP" width="3" />
|
||||
<field name="FileSeqNr" type="metatag" value="export_id" padding="zeroleft" width="5" />
|
||||
<field name="LastCdr" type="metatag" value="last_cdr_atime" layout="020106150405" width="12" />
|
||||
<field name="FileCreationfTime" type="metatag" value="time_now" layout="020106150405" width="12" />
|
||||
<field name="FileVersion" type="constant" value="01" width="2" />
|
||||
<field name="Filler2" type="filler" width="105" />
|
||||
</fields>
|
||||
</header>
|
||||
<content>
|
||||
<fields>
|
||||
<field name="ToR" type="constant" value="20" width="2" />
|
||||
<field name="Subject" type="cdrfield" value="subject" width="12" padding="right" mandatory="true" />
|
||||
<field name="ConnectionNumber" type="constant" value="00000" width="5" />
|
||||
<field name="CallerId" type="cdrfield" value="~callerid:s/\+(\d+)/00$1/" strip="xright" width="15" padding="right" />
|
||||
<field name="Destination" type="cdrfield" value="~destination:s/^\+311400(\d+)/$1/:s/^\+311412\d\d112/112/:s/^\+31(\d+)/0$1/:s/^\+(\d+)/00$1/" strip="xright" width="24" padding="right" mandatory="true" />
|
||||
<field name="TypeOfService" type="constant" value="00" width="2" />
|
||||
<field name="ServiceId" type="constant" value="11" width="4" padding="right" />
|
||||
<field name="AnswerTime" type="cdrfield" value="answer_time" layout="020106150405" width="12" mandatory="true" />
|
||||
<field name="Usage" type="cdrfield" value="usage" layout="seconds" width="6" padding="right" mandatory="true" />
|
||||
<field name="DataCounter" type="filler" width="6" />
|
||||
<field name="VatCode" type="constant" value="1" width="1" />
|
||||
<field name="NetworkId" type="constant" value="S1" width="2" />
|
||||
<field name="DestinationSubId" type="cdrfield" value="~cost_details:s/"MatchedDestId":".+_(\w{5})"/$1/:s/(\w{6})/$1/" width="5" />
|
||||
<field name="NetworkSubtype" type="constant" value="3" width="1" padding="left" />
|
||||
<field name="CgrId" type="cdrfield" value="cgrid" strip="xleft" width="16" paddingi="right" mandatory="true" />
|
||||
<field name="FillerVolume1" type="filler" width="8" />
|
||||
<field name="FillerVolume2" type="filler" width="8" />
|
||||
<field name="DestinationSubId" type="cdrfield" value="~cost_details:s/"MatchedDestId":".+_(\w{5})"/$1/:s/(\w{6})/$1/" width="5" />
|
||||
<field name="Cost" type="cdrfield" value="cost" padding="zeroleft" width="9" />
|
||||
<field name="MaskDestination" type="metatag" value="mask_destination" width="1" />
|
||||
</fields>
|
||||
</content>
|
||||
<trailer>
|
||||
<fields>
|
||||
<field name="ToR" type="constant" value="90" width="2" />
|
||||
<field name="Filler1" type="filler" width="3" />
|
||||
<field name="FileType" type="constant" value="SIP" width="3" />
|
||||
<field name="FileSeqNr" type="metatag" value="export_id" padding="zeroleft" width="5" />
|
||||
<field name="TotalRecords" type="metatag" value="cdrs_number" padding="zeroleft" width="6" />
|
||||
<field name="TotalDuration" type="metatag" value="cdrs_duration" padding="zeroleft" width="8" />
|
||||
<field name="FirstCdrTime" type="metatag" value="first_cdr_atime" layout="020106150405" width="12" />
|
||||
<field name="LastCdrTime" type="metatag" value="last_cdr_atime" layout="020106150405" width="12" />
|
||||
<field name="Filler1" type="filler" width="93" />
|
||||
</fields>
|
||||
</trailer>
|
||||
<cdr_format>fwv</cdr_format>
|
||||
<data_usage_multiply_factor>0.0</data_usage_multiply_factor>
|
||||
<cost_multiply_factor>0.0</cost_multiply_factor>
|
||||
<cost_shift_digits>0</cost_shift_digits>
|
||||
<mask_destination_id>MASKED_DESTINATIONS</mask_destination_id>
|
||||
<mask_length>0</mask_length>
|
||||
<export_dir>/var/log/cgrates/cdre</export_dir>
|
||||
<export_template>
|
||||
<header>
|
||||
<fields>
|
||||
<field name="ToR" type="constant" value="10" width="2" />
|
||||
<field name="Filler1" type="filler" width="3" />
|
||||
<field name="FileType" type="constant" value="SIP" width="3" />
|
||||
<field name="FileSeqNr" type="metatag" value="export_id" padding="zeroleft" width="5" />
|
||||
<field name="LastCdr" type="metatag" value="last_cdr_atime" layout="020106150405" width="12" />
|
||||
<field name="FileCreationfTime" type="metatag" value="time_now" layout="020106150405" width="12" />
|
||||
<field name="FileVersion" type="constant" value="01" width="2" />
|
||||
<field name="Filler2" type="filler" width="105" />
|
||||
</fields>
|
||||
</header>
|
||||
<content>
|
||||
<fields>
|
||||
<field name="ToR" type="constant" value="20" width="2" />
|
||||
<field name="Subject" type="cdrfield" value="subject" width="12" padding="right" mandatory="true" />
|
||||
<field name="ConnectionNumber" type="constant" value="00000" width="5" />
|
||||
<field name="CallerId" type="cdrfield" value="~callerid:s/\+(\d+)/00$1/" strip="xright" width="15" padding="right" />
|
||||
<field name="Destination" type="cdrfield" value="~destination:s/^\+311400(\d+)/$1/:s/^\+311412\d\d112/112/:s/^\+31(\d+)/0$1/:s/^\+(\d+)/00$1/" strip="xright" width="24" padding="right" mandatory="true" />
|
||||
<field name="TypeOfService" type="constant" value="00" width="2" />
|
||||
<field name="ServiceId" type="constant" value="11" width="4" padding="right" />
|
||||
<field name="AnswerTime" type="cdrfield" value="answer_time" layout="020106150405" width="12" mandatory="true" />
|
||||
<field name="Usage" type="cdrfield" value="usage" layout="seconds" width="6" padding="right" mandatory="true" />
|
||||
<field name="DataCounter" type="filler" width="6" />
|
||||
<field name="VatCode" type="constant" value="1" width="1" />
|
||||
<field name="NetworkId" type="constant" value="S1" width="2" />
|
||||
<field name="DestinationSubId" type="cdrfield" value="~cost_details:s/"MatchedDestId":".+_(\w{5})"/$1/:s/(\w{6})/$1/" width="5" />
|
||||
<field name="NetworkSubtype" type="constant" value="3" width="1" padding="left" />
|
||||
<field name="CgrId" type="cdrfield" value="cgrid" strip="xleft" width="16" paddingi="right" mandatory="true" />
|
||||
<field name="FillerVolume1" type="filler" width="8" />
|
||||
<field name="FillerVolume2" type="filler" width="8" />
|
||||
<field name="DestinationSubId" type="cdrfield" value="~cost_details:s/"MatchedDestId":".+_(\w{5})"/$1/:s/(\w{6})/$1/" width="5" />
|
||||
<field name="Cost" type="cdrfield" value="cost" padding="zeroleft" width="9" />
|
||||
<field name="MaskDestination" type="metatag" value="mask_destination" width="1" />
|
||||
</fields>
|
||||
</content>
|
||||
<trailer>
|
||||
<fields>
|
||||
<field name="ToR" type="constant" value="90" width="2" />
|
||||
<field name="Filler1" type="filler" width="3" />
|
||||
<field name="FileType" type="constant" value="SIP" width="3" />
|
||||
<field name="FileSeqNr" type="metatag" value="export_id" padding="zeroleft" width="5" />
|
||||
<field name="TotalRecords" type="metatag" value="cdrs_number" padding="zeroleft" width="6" />
|
||||
<field name="TotalDuration" type="metatag" value="cdrs_duration" padding="zeroleft" width="8" />
|
||||
<field name="FirstCdrTime" type="metatag" value="first_cdr_atime" layout="020106150405" width="12" />
|
||||
<field name="LastCdrTime" type="metatag" value="last_cdr_atime" layout="020106150405" width="12" />
|
||||
<field name="Filler1" type="filler" width="93" />
|
||||
</fields>
|
||||
</trailer>
|
||||
</export_template>
|
||||
</configuration>
|
||||
</document>
|
||||
@@ -327,33 +327,35 @@ type CachedItemAge struct {
|
||||
}
|
||||
|
||||
type AttrExpFileCdrs struct {
|
||||
CdrFormat string // Cdr output file format <utils.CdreCdrFormats>
|
||||
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 <utils.CdreCdrFormats>
|
||||
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 {
|
||||
|
||||
@@ -119,6 +119,7 @@ const (
|
||||
OUT = "*out"
|
||||
CDR_IMPORT = "cdr_import"
|
||||
CDR_EXPORT = "cdr_export"
|
||||
CDRFIELD = "cdrfield"
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user