diff --git a/.golangci.yaml b/.golangci.yaml index 441a14b3..2696243f 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -37,7 +37,7 @@ linters-settings: gocyclo: # minimal code complexity to report, 30 by default (but we recommend 10-20) - min-complexity: 45 + min-complexity: 48 goconst: min-len: 3 min-occurrences: 4 diff --git a/plugins/ptp_operator/metrics/logparser.go b/plugins/ptp_operator/metrics/logparser.go index 894ea137..79be0c37 100644 --- a/plugins/ptp_operator/metrics/logparser.go +++ b/plugins/ptp_operator/metrics/logparser.go @@ -180,6 +180,53 @@ func extractRegularMetrics(processName, output string) (interfaceName string, pt return } +func extractExternalSourceMetrics(source, processName, output string) (interfaceName string, status, ptpOffset float64) { + // ts2phc[1699929121]:[ts2phc.0.config] ens2f0 nmea_status 0 offset 999999 s0 + // ts2phc[1700086474]:[ts2phc.0.config] ens7fx offset 0 pps_status 1 s2 + var err error + index := FindInLogForCfgFileIndex(output) + if index == -1 { + log.Errorf("config name is not found in log outpt") + return + } + + output = strings.Replace(output, "path", "", 1) + replacer := strings.NewReplacer("[", " ", "]", " ", ":", " ", "phc", "", "sys", "") + output = replacer.Replace(output) + + output = output[index:] + fields := strings.Fields(output) + + // 0 1 2 3 4 5 6 + // ts2phc.0.config ens2f0 nmea_status 0 offset 999999 s0 + // ts2phc.0.config ens7fx offset 0 pps_status 1 s2 + if len(fields) < 7 { + return + } + + interfaceName = fields[1] + + var fieldStatus, fieldOffset int + + if source == NMEA { + fieldStatus = 3 + fieldOffset = 5 + } else { + fieldStatus = 5 + fieldOffset = 3 + } + + status, err = strconv.ParseFloat(fields[fieldStatus], 64) + if err != nil { + log.Errorf("%s failed to parse %s status from master output %s error %v", processName, source, fields[fieldStatus], err) + } + ptpOffset, err = strconv.ParseFloat(fields[fieldOffset], 64) + if err != nil { + log.Errorf("%s failed to parse %s offset from master output %s error %v", processName, source, fields[fieldOffset], err) + } + return +} + // ExtractPTP4lEvent ... extract event form ptp4l logs // // "INITIALIZING to LISTENING on INIT_COMPLETE" diff --git a/plugins/ptp_operator/metrics/manager.go b/plugins/ptp_operator/metrics/manager.go index 0f6b139d..2769be02 100644 --- a/plugins/ptp_operator/metrics/manager.go +++ b/plugins/ptp_operator/metrics/manager.go @@ -226,17 +226,18 @@ func (p *PTPEventManager) PublishEvent(state ptp.SyncState, ptpOffset int64, sou } func (p *PTPEventManager) publish(data ceevent.Data, eventSource string, eventType ptp.EventType) { + if p.mock { + return + } if pubs, ok := p.publisherTypes[eventType]; ok { e, err := common.CreateEvent(pubs.PubID, string(eventType), eventSource, data) if err != nil { log.Errorf("failed to create ptp event, %s", err) return } - if !p.mock { - if err = common.PublishEventViaAPI(p.scConfig, e); err != nil { - log.Errorf("failed to publish ptp event %v, %s", e, err) - return - } + if err = common.PublishEventViaAPI(p.scConfig, e); err != nil { + log.Errorf("failed to publish ptp event %v, %s", e, err) + return } } else { log.Errorf("failed to publish ptp event due to missing publisher for type %s", string(eventType)) @@ -283,7 +284,7 @@ func (p *PTPEventManager) GenPTPEvent(ptpProfileName string, oStats *stats.Stats case ptp.HOLDOVER: // do nothing, the timeout will switch holdover to FREE-RUN default: // not yet used states - log.Warnf("unknown %s sync state %s ,has last ptp state %s", eventResourceName, clockState, lastClockState) + log.Warnf("%s sync state %s, last ptp state is unknown: %s", eventResourceName, clockState, lastClockState) if !isOffsetInRange(ptpOffset, threshold.MaxOffsetThreshold, threshold.MinOffsetThreshold) { clockState = ptp.FREERUN } diff --git a/plugins/ptp_operator/metrics/metrics.go b/plugins/ptp_operator/metrics/metrics.go index edd98872..793c8f57 100644 --- a/plugins/ptp_operator/metrics/metrics.go +++ b/plugins/ptp_operator/metrics/metrics.go @@ -65,6 +65,15 @@ const ( PtpProcessDown int64 = 0 // PtpProcessUp process is up PtpProcessUp int64 = 1 + + // NMEA ... + NMEA = "NMEA" + // PPS ... + PPS = "PPS" + // UNAVAILABLE Nmea and Pps status + UNAVAILABLE float64 = 0 + // AVAILABLE Nmea and Pps status + AVAILABLE float64 = 1 ) // ExtractMetrics ... extract metrics from ptp logs. @@ -167,6 +176,38 @@ func (p *PTPEventManager) ExtractMetrics(msg string) { UpdatePTPMetrics(master, processName, alias, ptpOffset, maxPtpOffset, frequencyAdjustment, delay) } } + } else if (strings.Contains(output, "nmea_status") || strings.Contains(output, "pps_status")) && + processName == ts2phcProcessName { + // ts2phc[1699929121]:[ts2phc.0.config] ens2f0 nmea_status 0 offset 999999 s0 + // ts2phc[1700086474]:[ts2phc.0.config] ens7fx offset 9999 pps_status 0 s2 + var source string + if strings.Contains(output, "nmea_status") { + source = NMEA + } else { + source = PPS + } + interfaceName, status, ptpOffset := extractExternalSourceMetrics(source, processName, output) + + offsetSource := master + interfaceType := types.IFace(master) + // ts2phc return actual interface name unlike ptp4l + ptpInterface = ptp4lconf.PTPInterface{Name: interfaceName} + + var alias string + r := []rune(interfaceName) + alias = string(r[:len(r)-1]) + "x" + ptpStats[master].SetAlias(alias) + masterResource := fmt.Sprintf("%s/%s", alias, MasterClockType) + if status == UNAVAILABLE { + p.GenPTPEvent(profileName, ptpStats[interfaceType], masterResource, int64(ptpOffset), ptp.FREERUN, ptp.PtpStateChange) + } else { + UpdatePTPOffsetMetrics(offsetSource, processName, alias, ptpOffset) + } + if source == NMEA { + UpdateNmeaStatusMetrics(processName, alias, status) + } else { + UpdatePpsStatusMetrics(processName, alias, status) + } } else if strings.Contains(output, " offset ") && (processName != gnssProcessName && processName != dpllProcessName && processName != gmProcessName) { // ptp4l[5196819.100]: [ptp4l.0.config] master offset -2162130 s2 freq +22451884 path delay 374976 diff --git a/plugins/ptp_operator/metrics/metrics_test.go b/plugins/ptp_operator/metrics/metrics_test.go new file mode 100644 index 00000000..ecdced8c --- /dev/null +++ b/plugins/ptp_operator/metrics/metrics_test.go @@ -0,0 +1,128 @@ +// Copyright 2023 The Cloud Native Events Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build unittests +// +build unittests + +package metrics_test + +import ( + "os" + "testing" + + "github.com/redhat-cne/cloud-event-proxy/pkg/common" + "github.com/redhat-cne/cloud-event-proxy/plugins/ptp_operator/config" + "github.com/redhat-cne/cloud-event-proxy/plugins/ptp_operator/metrics" + "github.com/redhat-cne/cloud-event-proxy/plugins/ptp_operator/ptp4lconf" + "github.com/redhat-cne/cloud-event-proxy/plugins/ptp_operator/stats" + "github.com/redhat-cne/cloud-event-proxy/plugins/ptp_operator/types" + "github.com/redhat-cne/sdk-go/pkg/event/ptp" +) + +type metricsTestCase struct { + ptpLog string +} + +var ptp4lConfig = &ptp4lconf.PTP4lConfig{ + Name: "ptp4l.0.config", + Profile: "grandmaster", + Interfaces: []*ptp4lconf.PTPInterface{ + { + Name: "ens2f0", + PortID: 1, + PortName: "port 1", + Role: 2, + }, + { + Name: "ens7f0", + PortID: 2, + PortName: "port 3", + Role: 2, + }, + }, +} + +var ptpEventManager *metrics.PTPEventManager +var scConfig *common.SCConfiguration +var resourcePrefix = "" + +// InitPubSubTypes ... initialize types of publishers for ptp operator +func InitPubSubTypes() map[ptp.EventType]*types.EventPublisherType { + InitPubs := make(map[ptp.EventType]*types.EventPublisherType) + InitPubs[ptp.OsClockSyncStateChange] = &types.EventPublisherType{ + EventType: ptp.OsClockSyncStateChange, + Resource: ptp.OsClockSyncState, + } + InitPubs[ptp.PtpClockClassChange] = &types.EventPublisherType{ + EventType: ptp.PtpClockClassChange, + Resource: ptp.PtpClockClass, + } + InitPubs[ptp.PtpStateChange] = &types.EventPublisherType{ + EventType: ptp.PtpStateChange, + Resource: ptp.PtpLockState, + } + InitPubs[ptp.GnssStateChange] = &types.EventPublisherType{ + EventType: ptp.GnssStateChange, + Resource: ptp.GnssSyncStatus, + } + return InitPubs +} + +var testCase = []string{ + "GM[1699929086]:[ts2phc.0.config] ens2f0 T-GM-STATUS s0", + "ts2phc[1699929121]:[ts2phc.0.config] ens2f0 nmea_status 0 offset 999999 s0", + "ts2phc[1699929171]:[ts2phc.0.config] ens2fx nmea_status 1 offset 0 s2", + "ts2phc[1700086474]:[ts2phc.0.config] ens7fx offset 9999 pps_status 0 s2", + "ts2phc[1700086474]:[ts2phc.0.config] ens7fx offset 0 pps_status 1 s2", + "ts2phc[441664.291]: [ts2phc.0.config] ens2f0 master offset 0 s2 freq -0", + "ts2phc[441664.304]: [ts2phc.0.config] ens7f0 master offset 0 s2 freq +0", +} + +func setup() { + ptpEventManager = metrics.NewPTPEventManager(resourcePrefix, InitPubSubTypes(), "tetsnode", scConfig) + ptpEventManager.MockTest(true) + + ptpEventManager.AddPTPConfig(types.ConfigName(ptp4lConfig.Name), ptp4lConfig) + + stats_master := stats.NewStats(ptp4lConfig.Name) + stats_master.SetOffsetSource("master") + stats_master.SetProcessName("ts2phc") + stats_master.SetAlias("ens2fx") + + stats_slave := stats.NewStats(ptp4lConfig.Name) + stats_slave.SetOffsetSource("phc") + stats_slave.SetProcessName("phc2sys") + stats_slave.SetLastSyncState("LOCKED") + stats_slave.SetClockClass(0) + + ptpEventManager.Stats[types.ConfigName(ptp4lConfig.Name)] = make(map[types.IFace]*stats.Stats) + ptpEventManager.Stats[types.ConfigName(ptp4lConfig.Name)][types.IFace("master")] = stats_master + ptpEventManager.Stats[types.ConfigName(ptp4lConfig.Name)][types.IFace("CLOCK_REALTIME")] = stats_slave + ptpEventManager.PtpConfigMapUpdates = config.NewLinuxPTPConfUpdate() +} + +func teardown() { +} + +func TestMain(m *testing.M) { + setup() + code := m.Run() + teardown() + os.Exit(code) +} +func Test_ExtractMetrics(t *testing.T) { + for _, tc := range testCase { + ptpEventManager.ExtractMetrics(tc) + } +} diff --git a/plugins/ptp_operator/metrics/registry.go b/plugins/ptp_operator/metrics/registry.go index 90dd5794..4e147bae 100644 --- a/plugins/ptp_operator/metrics/registry.go +++ b/plugins/ptp_operator/metrics/registry.go @@ -57,6 +57,24 @@ var ( Help: "0 = FREERUN, 1 = LOCKED, 2 = HOLDOVER", }, []string{"process", "node", "iface"}) + // NmeaStatus metrics to show current nmea status + NmeaStatus = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Namespace: ptpNamespace, + Subsystem: ptpSubsystem, + Name: "nmea_status", + Help: "0 = UNAVAILABLE, 1 = AVAILABLE", + }, []string{"process", "node", "iface"}) + + // PpsStatus metrics to show current pps status + PpsStatus = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Namespace: ptpNamespace, + Subsystem: ptpSubsystem, + Name: "pps_status", + Help: "0 = UNAVAILABLE, 1 = AVAILABLE", + }, []string{"process", "node", "iface"}) + // Threshold metrics to show current ptp threshold Threshold = prometheus.NewGaugeVec( prometheus.GaugeOpts{ @@ -113,6 +131,8 @@ func RegisterMetrics(nodeName string) { prometheus.MustRegister(PtpFrequencyAdjustment) prometheus.MustRegister(PtpDelay) prometheus.MustRegister(SyncState) + prometheus.MustRegister(NmeaStatus) + prometheus.MustRegister(PpsStatus) prometheus.MustRegister(Threshold) prometheus.MustRegister(InterfaceRole) prometheus.MustRegister(ClockClassMetrics) @@ -142,6 +162,12 @@ func UpdatePTPMetrics(metricsType, process, eventResourceName string, offset, ma "process": process, "node": ptpNodeName, "iface": eventResourceName}).Set(delay) } +// UpdatePTPOffsetMetrics ... update ptp offset metrics +func UpdatePTPOffsetMetrics(metricsType, process, eventResourceName string, offset float64) { + PtpOffset.With(prometheus.Labels{"from": metricsType, + "process": process, "node": ptpNodeName, "iface": eventResourceName}).Set(offset) +} + // DeletedPTPMetrics ... update metrics for deleted ptp config func DeletedPTPMetrics(clockType, processName, eventResourceName string) { PtpOffset.Delete(prometheus.Labels{"from": clockType, @@ -178,13 +204,25 @@ func UpdateSyncStateMetrics(process, iface string, state ptp.SyncState) { } // prevent reporting wrong metrics if iface == master && process == phc2sysProcessName { - log.Errorf("wrong meterics are processed, ignoring interface %s with process %s", iface, process) + log.Errorf("wrong metrics are processed, ignoring interface %s with process %s", iface, process) return } SyncState.With(prometheus.Labels{ "process": process, "node": ptpNodeName, "iface": iface}).Set(clockState) } +// UpdateNmeaStatusMetrics ... update nmea status metrics +func UpdateNmeaStatusMetrics(process, iface string, status float64) { + NmeaStatus.With(prometheus.Labels{ + "process": process, "node": ptpNodeName, "iface": iface}).Set(status) +} + +// UpdatePpsStatusMetrics ... update pps status metrics +func UpdatePpsStatusMetrics(process, iface string, status float64) { + PpsStatus.With(prometheus.Labels{ + "process": process, "node": ptpNodeName, "iface": iface}).Set(status) +} + // UpdateInterfaceRoleMetrics ... update interface role metrics func UpdateInterfaceRoleMetrics(process, ptpInterface string, role types.PtpPortRole) { InterfaceRole.With(prometheus.Labels{