Diameter ccr_fields and cca_fields inside processors

This commit is contained in:
DanB
2015-12-17 19:25:42 +01:00
parent d11d9eadb7
commit 8676b0951e
8 changed files with 187 additions and 147 deletions

View File

@@ -68,14 +68,14 @@ func (self *DiameterAgent) handlers() diam.Handler {
func (self DiameterAgent) processCCR(ccr *CCR, reqProcessor *config.DARequestProcessor) (*CCA, error) {
passesAllFilters := true
for _, fldFilter := range reqProcessor.RequestFilter {
if passes, _ := ccr.passesFieldFilter(fldFilter); !passes {
if passes, _ := passesFieldFilter(ccr.diamMessage, fldFilter); !passes {
passesAllFilters = false
}
}
if !passesAllFilters { // Not going with this processor further
return nil, nil
}
smgEv, err := ccr.AsSMGenericEvent(reqProcessor.ContentFields)
smgEv, err := ccr.AsSMGenericEvent(reqProcessor.CCRFields)
if err != nil {
return nil, err
}
@@ -98,8 +98,8 @@ func (self DiameterAgent) processCCR(ccr *CCR, reqProcessor *config.DARequestPro
cca := NewCCAFromCCR(ccr)
cca.OriginHost = self.cgrCfg.DiameterAgentCfg().OriginHost
cca.OriginRealm = self.cgrCfg.DiameterAgentCfg().OriginRealm
cca.GrantedServiceUnit.CCTime = int(maxUsage)
cca.ResultCode = diam.Success
cca.GrantedServiceUnit.CCTime = int(maxUsage)
return cca, nil
}
@@ -124,7 +124,7 @@ func (self *DiameterAgent) handleCCR(c diam.Conn, m *diam.Message) {
utils.Logger.Err(fmt.Sprintf("<DiameterAgent> No request processor enabled for CCR: %+v, ignoring request", ccr))
return
}
if dmtA, err := cca.AsDiameterMessage(); err != nil {
if dmtA, err := cca.AsBareDiameterMessage(); err != nil {
utils.Logger.Err(fmt.Sprintf("<DiameterAgent> Failed to convert cca as diameter message, error: %s", err.Error()))
return
} else if _, err := dmtA.WriteTo(c); err != nil {

View File

@@ -139,11 +139,11 @@ func TestDmtAgentCCRAsSMGenericEvent(t *testing.T) {
if ccr.diamMessage, err = ccr.AsDiameterMessage(); err != nil {
t.Error(err)
}
eSMGE := sessionmanager.SMGenericEvent{"EventName": "DIAMETER_CCR", "AccId": "routinga;1442095190;1476802709",
eSMGE := sessionmanager.SMGenericEvent{"EventName": DIAMETER_CCR, "AccId": "routinga;1442095190;1476802709",
"Account": "*users", "AnswerTime": "2015-11-23 12:22:24 +0000 UTC", "Category": "call",
"Destination": "4986517174964", "Direction": "*out", "ReqType": "*users", "SetupTime": "2015-11-23 12:22:24 +0000 UTC",
"Subject": "*users", "SubscriberId": "4986517174963", "TOR": "*voice", "Tenant": "*users", "Usage": "300"}
if smge, err := ccr.AsSMGenericEvent(cfgDefaults.DiameterAgentCfg().RequestProcessors[0].ContentFields); err != nil {
if smge, err := ccr.AsSMGenericEvent(cfgDefaults.DiameterAgentCfg().RequestProcessors[0].CCRFields); err != nil {
t.Error(err)
} else if !reflect.DeepEqual(eSMGE, smge) {
t.Errorf("Expecting: %+v, received: %+v", eSMGE, smge)

View File

@@ -23,6 +23,7 @@ Build various type of packets here
*/
import (
"errors"
"fmt"
"math"
"math/rand"
@@ -47,9 +48,8 @@ func init() {
}
const (
META_CCR_USAGE = "*ccr_usage"
META_CCR_SMG_EVENT_NAME = "*ccr_smg_event_name"
DIAMETER_CCR = "DIAMETER_CCR"
META_CCR_USAGE = "*ccr_usage"
DIAMETER_CCR = "DIAMETER_CCR"
)
func loadDictionaries(dictsDir, componentId string) error {
@@ -155,6 +155,148 @@ func storedCdrToCCR(cdr *engine.StoredCdr, originHost, originRealm string, vendo
return ccr
}
// Not the cleanest but most efficient way to retrieve a string from AVP since there are string methods on all datatypes
// and the output is always in teh form "DataType{real_string}Padding:x"
func avpValAsString(a *diam.AVP) string {
dataVal := a.Data.String()
startIdx := strings.Index(dataVal, "{")
endIdx := strings.Index(dataVal, "}")
if startIdx == 0 || endIdx == 0 {
return ""
}
return dataVal[startIdx+1 : endIdx]
}
// Handler for meta functions
func metaHandler(m *diam.Message, tag, arg string, debitInterval time.Duration) (string, error) {
switch tag {
case META_CCR_USAGE:
ccReqTypeAvp, err := m.FindAVP("CC-Request-Type", dict.UndefinedVendorID)
if err != nil {
return "", err
} else if ccReqTypeAvp == nil {
return "", errors.New("CC-Request-Type not found")
}
ccReqNrAvp, err := m.FindAVP("CC-Request-Number", dict.UndefinedVendorID)
if err != nil {
return "", err
} else if ccReqNrAvp == nil {
return "", errors.New("CC-Request-Number not found")
}
reqUnitAVPs, err := m.FindAVPsWithPath([]interface{}{"Requested-Service-Unit", "CC-Time"}, dict.UndefinedVendorID)
if err != nil {
return "", err
} else if len(reqUnitAVPs) == 0 {
return "", errors.New("Requested-Service-Unit/CC-Time not found")
}
usedUnitAVPs, err := m.FindAVPsWithPath([]interface{}{"Used-Service-Unit", "CC-Time"}, dict.UndefinedVendorID)
if err != nil {
return "", err
} else if len(usedUnitAVPs) == 0 {
return "", errors.New("Used-Service-Unit/CC-Time not found")
}
usage := usageFromCCR(int(ccReqTypeAvp.Data.(datatype.Enumerated)),
int(ccReqNrAvp.Data.(datatype.Enumerated)),
int(reqUnitAVPs[0].Data.(datatype.Unsigned32)),
int(usedUnitAVPs[0].Data.(datatype.Unsigned32)), debitInterval)
return strconv.FormatFloat(usage.Seconds(), 'f', -1, 64), nil
}
return "", nil
}
func avpsWithPath(m *diam.Message, rsrFld *utils.RSRField) ([]*diam.AVP, error) {
hierarchyPath := strings.Split(rsrFld.Id, utils.HIERARCHY_SEP)
hpIf := make([]interface{}, len(hierarchyPath))
for i, val := range hierarchyPath {
hpIf[i] = val
}
return m.FindAVPsWithPath(hpIf, dict.UndefinedVendorID)
}
// Follows the implementation in the StorCdr
func passesFieldFilter(m *diam.Message, fieldFilter *utils.RSRField) (bool, int) {
if fieldFilter == nil {
return true, 0
}
avps, err := avpsWithPath(m, fieldFilter)
if err != nil {
return false, 0
} else if len(avps) == 0 {
return true, 0
}
for avpIdx, avpVal := range avps {
if fieldFilter.FilterPasses(avpValAsString(avpVal)) {
return true, avpIdx
}
}
return false, 0
}
func composedFieldvalue(m *diam.Message, outTpl utils.RSRFields, avpIdx int) string {
var outVal string
for _, rsrTpl := range outTpl {
if rsrTpl.IsStatic() {
outVal += rsrTpl.ParseValue("")
} else {
matchingAvps, err := avpsWithPath(m, rsrTpl)
if err != nil || len(matchingAvps) == 0 {
utils.Logger.Warning(fmt.Sprintf("<Diameter> Cannot find AVP for field template with id: %s, ignoring.", rsrTpl.Id))
continue // Filter not matching
}
if len(matchingAvps) <= avpIdx {
utils.Logger.Warning(fmt.Sprintf("<Diameter> Cannot retrieve AVP with index %d for field template with id: %s", avpIdx, rsrTpl.Id))
continue // Not convertible, ignore
}
if matchingAvps[0].Data.Type() == diam.GroupedAVPType {
utils.Logger.Warning(fmt.Sprintf("<Diameter> Value for field template with id: %s is matching a group AVP, ignoring.", rsrTpl.Id))
continue // Not convertible, ignore
}
outVal += avpValAsString(matchingAvps[avpIdx])
}
}
return outVal
}
func fieldOutVal(m *diam.Message, cfgFld *config.CfgCdrField, debitInterval time.Duration) (fmtValOut string, err error) {
var outVal string
switch cfgFld.Type {
case utils.META_FILLER:
outVal = cfgFld.Value.Id()
cfgFld.Padding = "right"
case utils.META_CONSTANT:
outVal = cfgFld.Value.Id()
case utils.META_HANDLER:
outVal, err = metaHandler(m, cfgFld.HandlerId, cfgFld.Layout, debitInterval)
if err != nil {
utils.Logger.Warning(fmt.Sprintf("<Diameter> Ignoring processing of metafunction: %s, error: %s", cfgFld.HandlerId, err.Error()))
}
case utils.META_COMPOSED:
outVal = composedFieldvalue(m, cfgFld.Value, 0)
case utils.MetaGrouped: // GroupedAVP
passAtIndex := -1
matchedAllFilters := true
for _, fldFilter := range cfgFld.FieldFilter {
var pass bool
if pass, passAtIndex = passesFieldFilter(m, fldFilter); !pass {
matchedAllFilters = false
break
}
}
if !matchedAllFilters {
return "", nil // Not matching field filters, will have it empty
}
if passAtIndex == -1 {
passAtIndex = 0 // No filter
}
outVal = composedFieldvalue(m, cfgFld.Value, passAtIndex)
}
if fmtValOut, err = utils.FmtFieldWidth(outVal, cfgFld.Width, cfgFld.Strip, cfgFld.Padding, cfgFld.Mandatory); err != nil {
utils.Logger.Warning(fmt.Sprintf("<Diameter> Error when processing field template with tag: %s, error: %s", cfgFld.Tag, err.Error()))
return "", err
}
return fmtValOut, nil
}
// debitInterval is the configured debitInterval, in sync with the diameter client one
func NewCCRFromDiameterMessage(m *diam.Message, debitInterval time.Duration) (*CCR, error) {
var ccr CCR
@@ -288,127 +430,12 @@ func (self *CCR) AsDiameterMessage() (*diam.Message, error) {
return m, nil
}
// Not the cleanest but most efficient way to retrieve a string from AVP since there are string methods on all datatypes
// and the output is always in teh form "DataType{real_string}Padding:x"
func avpValAsString(a *diam.AVP) string {
dataVal := a.Data.String()
startIdx := strings.Index(dataVal, "{")
endIdx := strings.Index(dataVal, "}")
if startIdx == 0 || endIdx == 0 {
return ""
}
return dataVal[startIdx+1 : endIdx]
}
// Handler for meta functions
func (self *CCR) metaHandler(tag, arg string) (string, error) {
switch tag {
case META_CCR_USAGE:
usage := usageFromCCR(self.CCRequestType, self.CCRequestNumber, self.RequestedServiceUnit.CCTime, self.UsedServiceUnit.CCTime, self.debitInterval)
return strconv.FormatFloat(usage.Seconds(), 'f', -1, 64), nil
}
return "", nil
}
func (self *CCR) avpsWithPath(rsrFld *utils.RSRField) ([]*diam.AVP, error) {
hierarchyPath := strings.Split(rsrFld.Id, utils.HIERARCHY_SEP)
hpIf := make([]interface{}, len(hierarchyPath))
for i, val := range hierarchyPath {
hpIf[i] = val
}
return self.diamMessage.FindAVPsWithPath(hpIf, dict.UndefinedVendorID)
}
// Follows the implementation in the StorCdr
func (self *CCR) passesFieldFilter(fieldFilter *utils.RSRField) (bool, int) {
if fieldFilter == nil {
return true, 0
}
avps, err := self.avpsWithPath(fieldFilter)
if err != nil {
return false, 0
} else if len(avps) == 0 {
return true, 0
}
for avpIdx, avpVal := range avps {
if fieldFilter.FilterPasses(avpValAsString(avpVal)) {
return true, avpIdx
}
}
return false, 0
}
func (self *CCR) eventFieldValue(fldTpl utils.RSRFields, avpIdx int) string {
var outVal string
for _, rsrTpl := range fldTpl {
if rsrTpl.IsStatic() {
outVal += rsrTpl.ParseValue("")
} else {
matchingAvps, err := self.avpsWithPath(rsrTpl)
if err != nil || len(matchingAvps) == 0 {
utils.Logger.Warning(fmt.Sprintf("<Diameter> Cannot find AVP for field template with id: %s, ignoring.", rsrTpl.Id))
continue // Filter not matching
}
if len(matchingAvps) <= avpIdx {
utils.Logger.Warning(fmt.Sprintf("<Diameter> Cannot retrieve AVP with index %d for field template with id: %s", avpIdx, rsrTpl.Id))
continue // Not convertible, ignore
}
if matchingAvps[0].Data.Type() == diam.GroupedAVPType {
utils.Logger.Warning(fmt.Sprintf("<Diameter> Value for field template with id: %s is matching a group AVP, ignoring.", rsrTpl.Id))
continue // Not convertible, ignore
}
outVal += avpValAsString(matchingAvps[avpIdx])
}
}
return outVal
}
func (self *CCR) fieldOutVal(cfgFld *config.CfgCdrField) (fmtValOut string, err error) {
var outVal string
switch cfgFld.Type {
case utils.META_FILLER:
outVal = cfgFld.Value.Id()
cfgFld.Padding = "right"
case utils.META_CONSTANT:
outVal = cfgFld.Value.Id()
case utils.META_HANDLER:
outVal, err = self.metaHandler(cfgFld.HandlerId, cfgFld.Layout)
if err != nil {
utils.Logger.Warning(fmt.Sprintf("<Diameter> Ignoring processing of metafunction: %s, error: %s", cfgFld.HandlerId, err.Error()))
}
case utils.META_COMPOSED:
outVal = self.eventFieldValue(cfgFld.Value, 0)
case utils.MetaGrouped: // GroupedAVP
passAtIndex := -1
matchedAllFilters := true
for _, fldFilter := range cfgFld.FieldFilter {
var pass bool
if pass, passAtIndex = self.passesFieldFilter(fldFilter); !pass {
matchedAllFilters = false
break
}
}
if !matchedAllFilters {
return "", nil // Not matching field filters, will have it empty
}
if passAtIndex == -1 {
passAtIndex = 0 // No filter
}
outVal = self.eventFieldValue(cfgFld.Value, passAtIndex)
}
if fmtValOut, err = utils.FmtFieldWidth(outVal, cfgFld.Width, cfgFld.Strip, cfgFld.Padding, cfgFld.Mandatory); err != nil {
utils.Logger.Warning(fmt.Sprintf("<Diameter> Error when processing field template with tag: %s, error: %s", cfgFld.Tag, err.Error()))
return "", err
}
return fmtValOut, nil
}
// Extracts data out of CCR into a SMGenericEvent based on the configured template
func (self *CCR) AsSMGenericEvent(cfgFlds []*config.CfgCdrField) (sessionmanager.SMGenericEvent, error) {
outMap := make(map[string]string) // work with it so we can append values to keys
outMap[utils.EVENT_NAME] = DIAMETER_CCR
for _, cfgFld := range cfgFlds {
fmtOut, err := self.fieldOutVal(cfgFld)
fmtOut, err := fieldOutVal(self.diamMessage, cfgFld, self.debitInterval)
if err != nil {
return nil, err
}
@@ -444,7 +471,7 @@ type CCA struct {
}
// Converts itself into DiameterMessage
func (self *CCA) AsDiameterMessage() (*diam.Message, error) {
func (self *CCA) AsBareDiameterMessage() (*diam.Message, error) {
if _, err := self.diamMessage.NewAVP("Session-Id", avp.Mbit, 0, datatype.UTF8String(self.SessionId)); err != nil {
return nil, err
}
@@ -466,14 +493,16 @@ func (self *CCA) AsDiameterMessage() (*diam.Message, error) {
if _, err := self.diamMessage.NewAVP(avp.ResultCode, avp.Mbit, 0, datatype.Unsigned32(self.ResultCode)); err != nil {
return nil, err
}
ccTimeAvp, err := self.diamMessage.Dictionary().FindAVP(self.diamMessage.Header.ApplicationID, "CC-Time")
if err != nil {
return nil, err
}
if _, err := self.diamMessage.NewAVP("Granted-Service-Unit", avp.Mbit, 0, &diam.GroupedAVP{
AVP: []*diam.AVP{
diam.NewAVP(ccTimeAvp.Code, avp.Mbit, 0, datatype.Unsigned32(self.GrantedServiceUnit.CCTime))}}); err != nil {
return nil, err
}
/*
ccTimeAvp, err := self.diamMessage.Dictionary().FindAVP(self.diamMessage.Header.ApplicationID, "CC-Time")
if err != nil {
return nil, err
}
if _, err := self.diamMessage.NewAVP("Granted-Service-Unit", avp.Mbit, 0, &diam.GroupedAVP{
AVP: []*diam.AVP{
diam.NewAVP(ccTimeAvp.Code, avp.Mbit, 0, datatype.Unsigned32(self.GrantedServiceUnit.CCTime))}}); err != nil {
return nil, err
}
*/
return self.diamMessage, nil
}

View File

@@ -99,11 +99,10 @@ func TestFieldOutVal(t *testing.T) {
m.NewAVP("Requested-Service-Unit", avp.Mbit, 0, &diam.GroupedAVP{
AVP: []*diam.AVP{
diam.NewAVP(420, avp.Mbit, 0, datatype.Unsigned32(360))}}) // CC-Time
ccr := &CCR{diamMessage: m}
cfgFld := &config.CfgCdrField{Tag: "StaticTest", Type: utils.META_COMPOSED, FieldId: utils.TOR,
Value: utils.ParseRSRFieldsMustCompile("^*voice", utils.INFIELD_SEP), Mandatory: true}
eOut := "*voice"
if fldOut, err := ccr.fieldOutVal(cfgFld); err != nil {
if fldOut, err := fieldOutVal(m, cfgFld, time.Duration(0)); err != nil {
t.Error(err)
} else if fldOut != eOut {
t.Errorf("Expecting: %s, received: %s", eOut, fldOut)
@@ -111,7 +110,7 @@ func TestFieldOutVal(t *testing.T) {
cfgFld = &config.CfgCdrField{Tag: "ComposedTest", Type: utils.META_COMPOSED, FieldId: utils.DESTINATION,
Value: utils.ParseRSRFieldsMustCompile("Requested-Service-Unit>CC-Time", utils.INFIELD_SEP), Mandatory: true}
eOut = "360"
if fldOut, err := ccr.fieldOutVal(cfgFld); err != nil {
if fldOut, err := fieldOutVal(m, cfgFld, time.Duration(0)); err != nil {
t.Error(err)
} else if fldOut != eOut {
t.Errorf("Expecting: %s, received: %s", eOut, fldOut)
@@ -120,7 +119,7 @@ func TestFieldOutVal(t *testing.T) {
cfgFld = &config.CfgCdrField{Tag: "Grouped1", Type: utils.MetaGrouped, FieldId: "Account",
Value: utils.ParseRSRFieldsMustCompile("Subscription-Id>Subscription-Id-Data", utils.INFIELD_SEP), Mandatory: true}
eOut = "33708000003"
if fldOut, err := ccr.fieldOutVal(cfgFld); err != nil {
if fldOut, err := fieldOutVal(m, cfgFld, time.Duration(0)); err != nil {
t.Error(err)
} else if fldOut != eOut {
t.Errorf("Expecting: %s, received: %s", eOut, fldOut)
@@ -130,7 +129,7 @@ func TestFieldOutVal(t *testing.T) {
FieldFilter: utils.ParseRSRFieldsMustCompile("Subscription-Id>Subscription-Id-Type(1)", utils.INFIELD_SEP),
Value: utils.ParseRSRFieldsMustCompile("Subscription-Id>Subscription-Id-Data", utils.INFIELD_SEP), Mandatory: true}
eOut = "208708000003"
if fldOut, err := ccr.fieldOutVal(cfgFld); err != nil {
if fldOut, err := fieldOutVal(m, cfgFld, time.Duration(0)); err != nil {
t.Error(err)
} else if fldOut != eOut {
t.Errorf("Expecting: %s, received: %s", eOut, fldOut)

View File

@@ -277,7 +277,7 @@ const CGRATES_CFG_JSON = `
"dry_run": false, // do not send the CDRs to CDRS, just parse them
"request_filter": "Subscription-Id>Subscription-Id-Type(0)", // filter requests processed by this processor
"continue_on_success": false, // continue to the next template if executed
"content_fields":[ // import content_fields template, tag will match internally CDR field, in case of .csv value will be represented by index of the field value
"ccr_fields":[ // fields taken out of CCR and used in internal requests
{"tag": "tor", "field_id": "TOR", "type": "*composed", "value": "^*voice", "mandatory": true},
{"tag": "accid", "field_id": "AccId", "type": "*composed", "value": "Session-Id", "mandatory": true},
{"tag": "reqtype", "field_id": "ReqType", "type": "*composed", "value": "^*users", "mandatory": true},
@@ -292,6 +292,9 @@ const CGRATES_CFG_JSON = `
{"tag": "usage", "field_id": "Usage", "type": "*handler", "handler_id": "*ccr_usage", "mandatory": true},
{"tag": "subscriber_id", "field_id": "SubscriberId", "type": "*composed", "value": "Subscription-Id>Subscription-Id-Data", "mandatory": true},
],
"cca_fields":[ // fields returned in CCA
//{"tag": "tor", "field_id": "TOR", "type": "*composed", "value": "^*voice", "mandatory": true},
],
},
],
},

View File

@@ -433,7 +433,7 @@ func TestDiameterAgentJsonCfg(t *testing.T) {
Dry_run: utils.BoolPointer(false),
Request_filter: utils.StringPointer("Subscription-Id>Subscription-Id-Type(0)"),
Continue_on_success: utils.BoolPointer(false),
Content_fields: &[]*CdrFieldJsonCfg{
CCR_fields: &[]*CdrFieldJsonCfg{
&CdrFieldJsonCfg{Tag: utils.StringPointer("tor"), Field_id: utils.StringPointer(utils.TOR), Type: utils.StringPointer(utils.META_COMPOSED),
Value: utils.StringPointer("^*voice"), Mandatory: utils.BoolPointer(true)},
&CdrFieldJsonCfg{Tag: utils.StringPointer("accid"), Field_id: utils.StringPointer(utils.ACCID), Type: utils.StringPointer(utils.META_COMPOSED),
@@ -461,13 +461,15 @@ func TestDiameterAgentJsonCfg(t *testing.T) {
&CdrFieldJsonCfg{Tag: utils.StringPointer("subscriber_id"), Field_id: utils.StringPointer("SubscriberId"), Type: utils.StringPointer(utils.META_COMPOSED),
Value: utils.StringPointer("Subscription-Id>Subscription-Id-Data"), Mandatory: utils.BoolPointer(true)},
},
CCA_fields: &[]*CdrFieldJsonCfg{},
},
},
}
if cfg, err := dfCgrJsonCfg.DiameterAgentJsonCfg(); err != nil {
t.Error(err)
} else if !reflect.DeepEqual(eCfg, cfg) {
t.Error("Received: ", cfg)
rcv := *cfg.Request_processors
t.Errorf("Received: %+v", rcv[0].CCA_fields)
}
}

View File

@@ -100,7 +100,8 @@ type DARequestProcessor struct {
DryRun bool
RequestFilter utils.RSRFields
ContinueOnSuccess bool
ContentFields []*CfgCdrField
CCRFields []*CfgCdrField
CCAFields []*CfgCdrField
}
func (self *DARequestProcessor) loadFromJsonCfg(jsnCfg *DARequestProcessorJsnCfg) error {
@@ -119,8 +120,13 @@ func (self *DARequestProcessor) loadFromJsonCfg(jsnCfg *DARequestProcessorJsnCfg
return err
}
}
if jsnCfg.Content_fields != nil {
if self.ContentFields, err = CfgCdrFieldsFromCdrFieldsJsonCfg(*jsnCfg.Content_fields); err != nil {
if jsnCfg.CCR_fields != nil {
if self.CCRFields, err = CfgCdrFieldsFromCdrFieldsJsonCfg(*jsnCfg.CCR_fields); err != nil {
return err
}
}
if jsnCfg.CCA_fields != nil {
if self.CCAFields, err = CfgCdrFieldsFromCdrFieldsJsonCfg(*jsnCfg.CCA_fields); err != nil {
return err
}
}

View File

@@ -257,7 +257,8 @@ type DARequestProcessorJsnCfg struct {
Dry_run *bool
Request_filter *string
Continue_on_success *bool
Content_fields *[]*CdrFieldJsonCfg
CCR_fields *[]*CdrFieldJsonCfg
CCA_fields *[]*CdrFieldJsonCfg
}
// History server config section