Skip to content

Commit 2b06d4b

Browse files
authored
Log enhancements (#6)
1 parent 0257919 commit 2b06d4b

File tree

2 files changed

+200
-155
lines changed

2 files changed

+200
-155
lines changed

client.go

Lines changed: 190 additions & 148 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,21 @@ const (
3030
filterTypeXPath = "xpath"
3131
)
3232

33+
// NETCONF operation names
34+
const (
35+
opGet = "get"
36+
opGetConfig = "get-config"
37+
opEditConfig = "edit-config"
38+
opCopyConfig = "copy-config"
39+
opDeleteConfig = "delete-config"
40+
opLock = "lock"
41+
opUnlock = "unlock"
42+
opCommit = "commit"
43+
opDiscard = "discard"
44+
opValidate = "validate"
45+
opRPC = "rpc"
46+
)
47+
3348
// Default client configuration values
3449
const (
3550
DefaultPort = 830
@@ -198,6 +213,7 @@ func NewClient(host string, opts ...func(*Client)) (*Client, error) {
198213
options.WithAuthPassword(client.password),
199214
options.WithPort(client.Port),
200215
options.WithTimeoutSocket(client.ConnectTimeout),
216+
options.WithTimeoutOps(client.OperationTimeout),
201217
}
202218

203219
// Only disable host key verification if explicitly requested
@@ -1094,6 +1110,7 @@ func (c *Client) reconnect() error {
10941110
options.WithAuthPassword(c.password),
10951111
options.WithPort(c.Port),
10961112
options.WithTimeoutSocket(c.ConnectTimeout),
1113+
options.WithTimeoutOps(c.OperationTimeout),
10971114
}
10981115

10991116
if c.InsecureSkipVerify {
@@ -1280,12 +1297,21 @@ func (c *Client) sendRPC(ctx context.Context, req *Req) (Res, error) {
12801297

12811298
// Not transient or max retries reached
12821299
if !isTransient || attempt >= c.MaxRetries {
1283-
// Log operation failure
1284-
c.logger.Error(ctx, "NETCONF operation failed",
1285-
"operation", req.Operation,
1286-
"retries", attempt,
1287-
"transient", isTransient,
1288-
"errorCount", len(res.Errors))
1300+
// Log operation failure with error details
1301+
if err != nil {
1302+
c.logger.Error(ctx, "NETCONF operation failed",
1303+
"operation", req.Operation,
1304+
"retries", attempt,
1305+
"transient", isTransient,
1306+
"errorCount", len(res.Errors),
1307+
"error", err.Error())
1308+
} else {
1309+
c.logger.Error(ctx, "NETCONF operation failed",
1310+
"operation", req.Operation,
1311+
"retries", attempt,
1312+
"transient", isTransient,
1313+
"errorCount", len(res.Errors))
1314+
}
12891315

12901316
// Log each RPC error
12911317
for i, rpcErr := range res.Errors {
@@ -1361,171 +1387,187 @@ func (c *Client) sendRPC(ctx context.Context, req *Req) (Res, error) {
13611387
func (c *Client) executeRPC(ctx context.Context, req *Req) (Res, error) {
13621388
_ = ctx // Accepted for future compatibility, timeout enforced by caller
13631389

1364-
// Delegate to scrapligo driver based on operation
1365-
var scrapligoRes *response.NetconfResponse
1366-
var err error
1367-
13681390
// Check for nil driver before operation
13691391
if c.driver == nil {
13701392
return Res{}, fmt.Errorf("operation %s failed: driver is nil (connection closed)", req.Operation)
13711393
}
13721394

1395+
// Log request XML before sending
1396+
c.logRequestXML(ctx, req)
1397+
1398+
// Dispatch operation to scrapligo driver
1399+
scrapligoRes, err := c.dispatchOperation(req)
1400+
1401+
// Handle operation error
1402+
if err != nil {
1403+
return Res{}, c.formatOperationError(req, err)
1404+
}
1405+
1406+
// Check for nil response
1407+
if scrapligoRes == nil {
1408+
return Res{}, fmt.Errorf("operation %s: received nil response from driver", req.Operation)
1409+
}
1410+
1411+
// Log response XML
1412+
c.logResponseXML(ctx, req.Operation, scrapligoRes.Result)
1413+
1414+
// Parse response XML
1415+
return c.parseResponse(scrapligoRes)
1416+
}
1417+
1418+
// getRequestXMLForLogging determines what XML content to log based on operation type.
1419+
func (c *Client) getRequestXMLForLogging(req *Req) string {
13731420
switch req.Operation {
1374-
case "get":
1375-
// Get method signature: Get(filter string, opts ...util.Option)
1376-
// The filter string is the XML subtree for subtree filters, or empty string
1377-
// For XPath filters, the XPath expression is passed as the filter string
1378-
// Note: scrapligo driver/netconf may require filter type to be indicated via options
1379-
// for XPath filters (this is validated but needs integration testing)
1380-
filterStr := req.Filter.Content
1381-
scrapligoRes, err = c.driver.Get(filterStr)
1382-
1383-
case "get-config":
1384-
// GetConfig method signature: GetConfig(source string, opts ...util.Option)
1385-
//
1386-
// Filter options are provided via driver/opoptions package:
1387-
// - opoptions.WithFilter(content) sets the filter XML or XPath expression
1388-
// - opoptions.WithFilterType(type) sets "subtree" or "xpath" (default is "subtree")
1389-
var opts []util.Option
1390-
if req.Filter.Content != "" {
1391-
// Add filter content
1392-
opts = append(opts, opoptions.WithFilter(req.Filter.Content))
1393-
1394-
// Set filter type if XPath (subtree is default)
1395-
if req.Filter.Type == filterTypeXPath {
1396-
opts = append(opts, opoptions.WithFilterType(filterTypeXPath))
1397-
}
1398-
}
1399-
scrapligoRes, err = c.driver.GetConfig(req.Target, opts...)
1400-
1401-
case "edit-config":
1402-
// EditConfig method signature: EditConfig(target, config string)
1403-
// If advanced edit-config options are set, build custom XML
1404-
if req.DefaultOperation != "" || req.TestOption != "" || req.ErrorOption != "" {
1405-
rpcXML := c.buildEditConfigXML(req)
1406-
scrapligoRes, err = c.driver.RPC(opoptions.WithFilter(rpcXML))
1407-
} else {
1408-
// For standard edit-config, ensure config has <config> wrapper
1409-
// scrapligo's EditConfig expects the caller to provide the config element
1410-
configContent := req.Config
1411-
result := xmldot.Get(req.Config, "config")
1412-
if !result.Exists() {
1413-
// Config doesn't have <config> wrapper, add it
1414-
configContent = "<config>" + req.Config + "</config>"
1415-
}
1416-
scrapligoRes, err = c.driver.EditConfig(req.Target, configContent)
1421+
case opEditConfig:
1422+
// For edit-config, ensure <config> wrapper is present (matching what will be sent)
1423+
if !xmldot.Get(req.Config, "config").Exists() {
1424+
return "<config>" + req.Config + "</config>"
14171425
}
1426+
return req.Config
1427+
case opGetConfig, opGet:
1428+
return req.Filter.Content
1429+
case opLock, opUnlock, opCommit, opDiscard, opValidate:
1430+
return "" // Simple operations have no XML content
1431+
default:
1432+
return req.Config // For other operations (rpc, copy-config, etc.)
1433+
}
1434+
}
14181435

1419-
case "copy-config":
1420-
// CopyConfig method signature: CopyConfig(source, target string)
1421-
scrapligoRes, err = c.driver.CopyConfig(req.Config, req.Target)
1422-
1423-
case "delete-config":
1424-
// DeleteConfig method signature: DeleteConfig(target string)
1425-
scrapligoRes, err = c.driver.DeleteConfig(req.Target)
1426-
1427-
case "lock":
1428-
// Lock method signature: Lock(target string)
1429-
scrapligoRes, err = c.driver.Lock(req.Target)
1430-
1431-
case "unlock":
1432-
// Unlock method signature: Unlock(target string)
1433-
scrapligoRes, err = c.driver.Unlock(req.Target)
1434-
1435-
case "commit":
1436-
// Commit method signature: Commit(opts ...util.Option)
1437-
// Support confirmed commit parameters via scrapligo options
1438-
var opts []util.Option
1439-
if req.ConfirmTimeout > 0 {
1440-
// Confirmed commit with timeout
1441-
opts = append(opts, opoptions.WithCommitConfirmed())
1442-
opts = append(opts, opoptions.WithCommitConfirmTimeout(uint(req.ConfirmTimeout)))
1443-
}
1444-
if req.PersistID != "" {
1445-
// Persist ID for commit operations
1446-
opts = append(opts, opoptions.WithCommitConfirmedPersistID(req.PersistID))
1447-
}
1448-
scrapligoRes, err = c.driver.Commit(opts...)
1436+
// logRequestXML logs the request XML content if present.
1437+
func (c *Client) logRequestXML(ctx context.Context, req *Req) {
1438+
xmlToLog := c.getRequestXMLForLogging(req)
1439+
if xmlToLog == "" {
1440+
return
1441+
}
14491442

1450-
case "discard":
1451-
// Discard method signature: Discard()
1452-
scrapligoRes, err = c.driver.Discard()
1443+
if len(xmlToLog) > MaxXMLSizeForLogging {
1444+
c.logger.Debug(ctx, "NETCONF RPC request XML (truncated)",
1445+
"operation", req.Operation,
1446+
"size", len(xmlToLog),
1447+
"limit", MaxXMLSizeForLogging,
1448+
"xml", "[XML TOO LARGE FOR LOGGING]")
1449+
return
1450+
}
14531451

1454-
case "validate":
1455-
// Validate method signature: Validate(target string)
1456-
scrapligoRes, err = c.driver.Validate(req.Target)
1452+
if !utf8.ValidString(xmlToLog) {
1453+
c.logger.Warn(ctx, "Invalid UTF-8 in NETCONF request XML",
1454+
"operation", req.Operation,
1455+
"size", len(xmlToLog))
1456+
return
1457+
}
14571458

1458-
case "rpc":
1459-
// RPC method signature: RPC(opts ...util.Option)
1460-
// Pass the RPC XML content via WithFilter option
1461-
scrapligoRes, err = c.driver.RPC(opoptions.WithFilter(req.Config))
1459+
requestXML := c.prepareXMLForLogging(xmlToLog)
1460+
c.logger.Debug(ctx, "NETCONF RPC request XML",
1461+
"operation", req.Operation,
1462+
"xml", requestXML)
1463+
}
14621464

1463-
default:
1464-
return Res{}, fmt.Errorf("unsupported operation: %s", req.Operation)
1465+
// logResponseXML logs the response XML content.
1466+
func (c *Client) logResponseXML(ctx context.Context, operation string, result string) {
1467+
if result == "" {
1468+
return
14651469
}
14661470

1467-
if err != nil {
1468-
return Res{}, fmt.Errorf("operation %s failed: %w", req.Operation, err)
1471+
if len(result) > MaxXMLSizeForLogging {
1472+
c.logger.Debug(ctx, "NETCONF RPC response XML (truncated)",
1473+
"operation", operation,
1474+
"size", len(result),
1475+
"limit", MaxXMLSizeForLogging,
1476+
"xml", "[XML TOO LARGE FOR LOGGING]")
1477+
return
14691478
}
14701479

1471-
// Check for nil response
1472-
if scrapligoRes == nil {
1473-
return Res{}, fmt.Errorf("operation %s: received nil response from driver", req.Operation)
1480+
if !utf8.ValidString(result) {
1481+
c.logger.Warn(ctx, "Invalid UTF-8 in NETCONF response XML",
1482+
"operation", operation,
1483+
"size", len(result))
1484+
return
14741485
}
14751486

1476-
// Log request XML content (Debug level only)
1477-
// Pre-check size and level to avoid expensive processing when not needed
1478-
if len(scrapligoRes.Input) > 0 {
1479-
// Pre-check size limit before string conversion (avoid allocation)
1480-
if len(scrapligoRes.Input) <= MaxXMLSizeForLogging {
1481-
// Validate UTF-8 encoding
1482-
if !utf8.Valid(scrapligoRes.Input) {
1483-
c.logger.Warn(ctx, "Invalid UTF-8 in NETCONF request XML",
1484-
"operation", req.Operation,
1485-
"size", len(scrapligoRes.Input))
1486-
} else {
1487-
requestXML := c.prepareXMLForLogging(string(scrapligoRes.Input))
1488-
c.logger.Debug(ctx, "NETCONF RPC request XML",
1489-
"operation", req.Operation,
1490-
"xml", requestXML)
1491-
}
1492-
} else {
1493-
// Log truncation message only (cheap operation)
1494-
c.logger.Debug(ctx, "NETCONF RPC request XML (truncated)",
1495-
"operation", req.Operation,
1496-
"size", len(scrapligoRes.Input),
1497-
"limit", MaxXMLSizeForLogging,
1498-
"xml", "[XML TOO LARGE FOR LOGGING]")
1499-
}
1487+
responseXML := c.prepareXMLForLogging(result)
1488+
c.logger.Debug(ctx, "NETCONF RPC response XML",
1489+
"operation", operation,
1490+
"xml", responseXML)
1491+
}
1492+
1493+
// formatOperationError formats an operation error with context.
1494+
func (c *Client) formatOperationError(req *Req, err error) error {
1495+
if req.Operation == opEditConfig {
1496+
return fmt.Errorf("operation %s failed (target=%s, configSize=%d): %w",
1497+
req.Operation, req.Target, len(req.Config), err)
15001498
}
1499+
return fmt.Errorf("operation %s failed (target=%s): %w", req.Operation, req.Target, err)
1500+
}
15011501

1502-
// Log response XML content (Debug level only)
1503-
if scrapligoRes.Result != "" {
1504-
// Pre-check size limit before processing
1505-
if len(scrapligoRes.Result) <= MaxXMLSizeForLogging {
1506-
// Validate UTF-8 encoding
1507-
if !utf8.ValidString(scrapligoRes.Result) {
1508-
c.logger.Warn(ctx, "Invalid UTF-8 in NETCONF response XML",
1509-
"operation", req.Operation,
1510-
"size", len(scrapligoRes.Result))
1511-
} else {
1512-
responseXML := c.prepareXMLForLogging(scrapligoRes.Result)
1513-
c.logger.Debug(ctx, "NETCONF RPC response XML",
1514-
"operation", req.Operation,
1515-
"xml", responseXML)
1516-
}
1517-
} else {
1518-
// Log truncation message only (cheap operation)
1519-
c.logger.Debug(ctx, "NETCONF RPC response XML (truncated)",
1520-
"operation", req.Operation,
1521-
"size", len(scrapligoRes.Result),
1522-
"limit", MaxXMLSizeForLogging,
1523-
"xml", "[XML TOO LARGE FOR LOGGING]")
1502+
// dispatchOperation dispatches the operation to the appropriate scrapligo driver method.
1503+
func (c *Client) dispatchOperation(req *Req) (*response.NetconfResponse, error) {
1504+
switch req.Operation {
1505+
case opGet:
1506+
return c.driver.Get(req.Filter.Content)
1507+
case opGetConfig:
1508+
return c.executeGetConfig(req)
1509+
case opEditConfig:
1510+
return c.executeEditConfig(req)
1511+
case opCopyConfig:
1512+
return c.driver.CopyConfig(req.Config, req.Target)
1513+
case opDeleteConfig:
1514+
return c.driver.DeleteConfig(req.Target)
1515+
case opLock:
1516+
return c.driver.Lock(req.Target)
1517+
case opUnlock:
1518+
return c.driver.Unlock(req.Target)
1519+
case opCommit:
1520+
return c.executeCommit(req)
1521+
case opDiscard:
1522+
return c.driver.Discard()
1523+
case opValidate:
1524+
return c.driver.Validate(req.Target)
1525+
case opRPC:
1526+
return c.driver.RPC(opoptions.WithFilter(req.Config))
1527+
default:
1528+
return nil, fmt.Errorf("unsupported operation: %s", req.Operation)
1529+
}
1530+
}
1531+
1532+
// executeGetConfig executes a get-config operation with filter options.
1533+
func (c *Client) executeGetConfig(req *Req) (*response.NetconfResponse, error) {
1534+
var opts []util.Option
1535+
if req.Filter.Content != "" {
1536+
opts = append(opts, opoptions.WithFilter(req.Filter.Content))
1537+
if req.Filter.Type == filterTypeXPath {
1538+
opts = append(opts, opoptions.WithFilterType(filterTypeXPath))
15241539
}
15251540
}
1541+
return c.driver.GetConfig(req.Target, opts...)
1542+
}
15261543

1527-
// Parse response XML
1528-
return c.parseResponse(scrapligoRes)
1544+
// executeEditConfig executes an edit-config operation.
1545+
func (c *Client) executeEditConfig(req *Req) (*response.NetconfResponse, error) {
1546+
// If advanced edit-config options are set, build custom XML
1547+
if req.DefaultOperation != "" || req.TestOption != "" || req.ErrorOption != "" {
1548+
rpcXML := c.buildEditConfigXML(req)
1549+
return c.driver.RPC(opoptions.WithFilter(rpcXML))
1550+
}
1551+
1552+
// For standard edit-config, ensure config has <config> wrapper
1553+
configContent := req.Config
1554+
if !xmldot.Get(req.Config, "config").Exists() {
1555+
configContent = "<config>" + req.Config + "</config>"
1556+
}
1557+
return c.driver.EditConfig(req.Target, configContent)
1558+
}
1559+
1560+
// executeCommit executes a commit operation with optional confirmed commit.
1561+
func (c *Client) executeCommit(req *Req) (*response.NetconfResponse, error) {
1562+
var opts []util.Option
1563+
if req.ConfirmTimeout > 0 {
1564+
opts = append(opts, opoptions.WithCommitConfirmed())
1565+
opts = append(opts, opoptions.WithCommitConfirmTimeout(uint(req.ConfirmTimeout)))
1566+
}
1567+
if req.PersistID != "" {
1568+
opts = append(opts, opoptions.WithCommitConfirmedPersistID(req.PersistID))
1569+
}
1570+
return c.driver.Commit(opts...)
15291571
}
15301572

15311573
// parseResponse parses a NETCONF response from scrapligo

0 commit comments

Comments
 (0)