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