method error handling
introduction
This tutorial focuses on comprehensive error handling strategies for OPC UA method calls. Proper error handling is crucial for building robust industrial automation systems that can gracefully handle failures, provide meaningful diagnostics, and maintain system stability.
Error handling in OPC UA method calls involves understanding status codes, implementing recovery strategies, logging diagnostic information, and providing appropriate user feedback.
prerequisites
- Completed Advanced Method Patterns tutorial
- Understanding of OPC UA status codes and error types
- Knowledge of industrial automation error handling requirements
- Experience with Node-RED error handling patterns
understanding OPC UA status codes
1. common status codes for method Calls
// Define comprehensive status code handling
const StatusCodeHandler = {
statusCodes: {
// Success codes
"Good": {
category: "success",
description: "Method executed successfully",
action: "continue",
logLevel: "info"
},
// Input/Parameter errors
"BadInvalidArgument": {
category: "parameter_error",
description: "Invalid method argument provided",
action: "validate_parameters",
logLevel: "error"
},
"BadArgumentsMissing": {
category: "parameter_error",
description: "Required method arguments missing",
action: "provide_missing_arguments",
logLevel: "error"
},
"BadTooManyArguments": {
category: "parameter_error",
description: "Too many arguments provided to method",
action: "remove_extra_arguments",
logLevel: "error"
},
"BadTypeMismatch": {
category: "parameter_error",
description: "Argument type does not match expected type",
action: "convert_argument_types",
logLevel: "error"
},
"BadOutOfRange": {
category: "parameter_error",
description: "Argument value is outside valid range",
action: "adjust_argument_values",
logLevel: "error"
},
// Method availability errors
"BadMethodInvalid": {
category: "method_error",
description: "Method does not exist on the object",
action: "verify_method_exists",
logLevel: "error"
},
"BadNoValidCertificate": {
category: "security_error",
description: "No valid certificate for method execution",
action: "check_security_configuration",
logLevel: "error"
},
"BadUserAccessDenied": {
category: "security_error",
description: "User does not have permission to execute method",
action: "check_user_permissions",
logLevel: "error"
},
// Communication errors
"BadCommunicationError": {
category: "communication_error",
description: "Communication error during method call",
action: "retry_with_backoff",
logLevel: "warn"
},
"BadTimeout": {
category: "communication_error",
description: "Method call timed out",
action: "increase_timeout_or_retry",
logLevel: "warn"
},
"BadServerNotConnected": {
category: "communication_error",
description: "Server connection lost",
action: "reconnect_and_retry",
logLevel: "error"
},
"BadTooManyOps": {
category: "communication_error",
description: "Server overloaded with requests",
action: "throttle_requests",
logLevel: "warn"
},
// Server/Object state errors
"BadStateNotActive": {
category: "state_error",
description: "Object not in correct state for method execution",
action: "check_object_state",
logLevel: "error"
},
"BadShutdown": {
category: "state_error",
description: "Server is shutting down",
action: "wait_for_server_restart",
logLevel: "error"
},
"BadNotExecutable": {
category: "state_error",
description: "Method cannot be executed in current context",
action: "check_execution_context",
logLevel: "error"
},
// Resource errors
"BadOutOfMemory": {
category: "resource_error",
description: "Server out of memory",
action: "wait_and_retry",
logLevel: "error"
},
"BadResourceUnavailable": {
category: "resource_error",
description: "Required resource is unavailable",
action: "wait_for_resource",
logLevel: "warn"
}
},
getStatusInfo: function(statusCode) {
return this.statusCodes[statusCode] || {
category: "unknown_error",
description: `Unknown status code: ${statusCode}`,
action: "log_and_escalate",
logLevel: "error"
};
},
isRetryableError: function(statusCode) {
const retryableCategories = [
"communication_error",
"resource_error"
];
const statusInfo = this.getStatusInfo(statusCode);
return retryableCategories.includes(statusInfo.category);
},
getErrorSeverity: function(statusCode) {
const statusInfo = this.getStatusInfo(statusCode);
switch (statusInfo.category) {
case "parameter_error":
return "medium";
case "security_error":
return "high";
case "communication_error":
return "low";
case "state_error":
return "medium";
case "resource_error":
return "low";
default:
return "high";
}
}
};
// Process method result with comprehensive status handling
const statusCode = msg.statusCode || "Unknown";
const statusInfo = StatusCodeHandler.getStatusInfo(statusCode);
msg.statusInfo = statusInfo;
msg.errorSeverity = StatusCodeHandler.getErrorSeverity(statusCode);
msg.isRetryable = StatusCodeHandler.isRetryableError(statusCode);
// Log based on severity
switch (statusInfo.logLevel) {
case "info":
node.log(`Method success: ${statusInfo.description}`);
break;
case "warn":
node.warn(`Method warning: ${statusInfo.description} (${statusCode})`);
break;
case "error":
node.error(`Method error: ${statusInfo.description} (${statusCode})`);
break;
}
return msg;
2. detailed error analysis
// Analyze errors and provide detailed diagnostics
const ErrorAnalyzer = {
analyzeParameterError: function(statusCode, methodSignature, providedParams) {
const analysis = {
errorType: "parameter_error",
statusCode: statusCode,
issues: [],
suggestions: []
};
if (!methodSignature) {
analysis.issues.push("Method signature not available for validation");
analysis.suggestions.push("Retrieve method signature using browsing");
return analysis;
}
const expectedParams = methodSignature.inputArguments || [];
const providedKeys = Object.keys(providedParams || {});
switch (statusCode) {
case "BadArgumentsMissing":
const requiredParams = expectedParams.filter(p => !p.optional);
const missingParams = requiredParams.filter(p =>
!providedKeys.includes(p.name)
);
analysis.issues.push(
`Missing required parameters: ${missingParams.map(p => p.name).join(", ")}`
);
analysis.suggestions.push(
"Provide all required parameters",
"Check method documentation for parameter requirements"
);
break;
case "BadTooManyArguments":
const expectedNames = expectedParams.map(p => p.name);
const extraParams = providedKeys.filter(k =>
!expectedNames.includes(k)
);
analysis.issues.push(
`Unexpected parameters: ${extraParams.join(", ")}`
);
analysis.suggestions.push(
"Remove extra parameters",
"Verify parameter names match method signature"
);
break;
case "BadTypeMismatch":
expectedParams.forEach(param => {
const providedValue = providedParams[param.name];
if (providedValue !== undefined) {
const expectedType = param.dataType;
const providedType = typeof providedValue;
if (!this.isTypeCompatible(expectedType, providedType, providedValue)) {
analysis.issues.push(
`Parameter '${param.name}' type mismatch: expected ${expectedType}, got ${providedType}`
);
analysis.suggestions.push(
`Convert '${param.name}' to ${expectedType} type`
);
}
}
});
break;
case "BadOutOfRange":
expectedParams.forEach(param => {
const providedValue = providedParams[param.name];
if (providedValue !== undefined && param.constraints) {
const violations = this.checkConstraints(providedValue, param.constraints);
if (violations.length > 0) {
analysis.issues.push(
`Parameter '${param.name}' constraint violations: ${violations.join(", ")}`
);
analysis.suggestions.push(
`Adjust '${param.name}' to meet constraints: ${JSON.stringify(param.constraints)}`
);
}
}
});
break;
}
return analysis;
},
isTypeCompatible: function(expectedType, providedType, value) {
// Simplified type compatibility check
const compatibilityMap = {
"Double": ["number"],
"Float": ["number"],
"Int32": ["number"],
"UInt32": ["number"],
"String": ["string"],
"Boolean": ["boolean"],
"DateTime": ["string", "object"] // Date objects or ISO strings
};
const compatibleTypes = compatibilityMap[expectedType] || [expectedType.toLowerCase()];
return compatibleTypes.includes(providedType);
},
checkConstraints: function(value, constraints) {
const violations = [];
if (constraints.min !== undefined && value < constraints.min) {
violations.push(`below minimum (${constraints.min})`);
}
if (constraints.max !== undefined && value > constraints.max) {
violations.push(`above maximum (${constraints.max})`);
}
if (constraints.enum && !constraints.enum.includes(value)) {
violations.push(`not in allowed values (${constraints.enum.join(", ")})`);
}
return violations;
},
analyzeSecurityError: function(statusCode, userContext, methodInfo) {
const analysis = {
errorType: "security_error",
statusCode: statusCode,
issues: [],
suggestions: []
};
switch (statusCode) {
case "BadUserAccessDenied":
analysis.issues.push(
`User '${userContext?.username || "anonymous"}' lacks permission for method '${methodInfo?.name}'`
);
analysis.suggestions.push(
"Verify user has execute permission on the method",
"Check role-based access control configuration",
"Ensure user is properly authenticated"
);
break;
case "BadNoValidCertificate":
analysis.issues.push("No valid certificate for secure method execution");
analysis.suggestions.push(
"Install valid client certificate",
"Check certificate trust configuration",
"Verify certificate has not expired"
);
break;
}
return analysis;
}
};
// Perform detailed error analysis
const statusCode = msg.statusCode;
const methodSignature = msg.methodSignature; // Should be provided from browsing
const providedParams = msg.payload;
let analysis = {};
if (statusCode !== "Good") {
const statusInfo = StatusCodeHandler.getStatusInfo(statusCode);
switch (statusInfo.category) {
case "parameter_error":
analysis = ErrorAnalyzer.analyzeParameterError(
statusCode,
methodSignature,
providedParams
);
break;
case "security_error":
analysis = ErrorAnalyzer.analyzeSecurityError(
statusCode,
msg.userContext,
msg.methodInfo
);
break;
default:
analysis = {
errorType: statusInfo.category,
statusCode: statusCode,
description: statusInfo.description,
recommendedAction: statusInfo.action
};
}
msg.errorAnalysis = analysis;
// Log detailed analysis
node.error(`Method call failed - ${analysis.errorType}: ${JSON.stringify(analysis, null, 2)}`);
}
return msg;
recovery strategies
1. automatic error recovery
// Implement automatic recovery strategies
const RecoveryManager = {
strategies: {
"parameter_error": {
automatic: true,
actions: [
"validate_and_fix_parameters",
"apply_default_values",
"retry_with_corrected_parameters"
]
},
"communication_error": {
automatic: true,
actions: [
"wait_and_retry",
"reconnect_if_needed",
"use_alternative_endpoint"
]
},
"state_error": {
automatic: false,
actions: [
"check_object_state",
"wait_for_state_change",
"manual_intervention_required"
]
},
"security_error": {
automatic: false,
actions: [
"log_security_violation",
"notify_administrators",
"require_manual_authorization"
]
}
},
executeRecovery: function(errorType, errorDetails, context) {
const strategy = this.strategies[errorType];
if (!strategy || !strategy.automatic) {
return {
success: false,
reason: "Manual intervention required",
strategy: strategy
};
}
const recoveryActions = [];
strategy.actions.forEach(action => {
const result = this.executeAction(action, errorDetails, context);
recoveryActions.push(result);
});
const allSuccessful = recoveryActions.every(action => action.success);
return {
success: allSuccessful,
actions: recoveryActions,
recoveredPayload: allSuccessful ? context.recoveredPayload : null
};
},
executeAction: function(actionName, errorDetails, context) {
switch (actionName) {
case "validate_and_fix_parameters":
return this.fixParameters(errorDetails, context);
case "apply_default_values":
return this.applyDefaults(errorDetails, context);
case "wait_and_retry":
return this.scheduleRetry(errorDetails, context);
case "reconnect_if_needed":
return this.checkReconnection(errorDetails, context);
default:
return {
success: false,
action: actionName,
reason: "Unknown action"
};
}
},
fixParameters: function(errorDetails, context) {
if (errorDetails.errorType !== "parameter_error") {
return { success: false, reason: "Not a parameter error" };
}
const originalParams = context.originalPayload || {};
const fixedParams = { ...originalParams };
let hasChanges = false;
// Fix based on analysis
if (errorDetails.analysis && errorDetails.analysis.issues) {
errorDetails.analysis.issues.forEach(issue => {
if (issue.includes("type mismatch")) {
// Attempt type conversion
const paramMatch = issue.match(/Parameter '(\w+)'/);
if (paramMatch) {
const paramName = paramMatch[1];
const currentValue = fixedParams[paramName];
if (issue.includes("expected Double") && typeof currentValue === "string") {
const numValue = parseFloat(currentValue);
if (!isNaN(numValue)) {
fixedParams[paramName] = numValue;
hasChanges = true;
}
} else if (issue.includes("expected String") && typeof currentValue === "number") {
fixedParams[paramName] = currentValue.toString();
hasChanges = true;
}
}
}
});
}
if (hasChanges) {
context.recoveredPayload = fixedParams;
return {
success: true,
action: "validate_and_fix_parameters",
changes: "Applied automatic type conversions"
};
}
return {
success: false,
action: "validate_and_fix_parameters",
reason: "No automatic fixes available"
};
},
applyDefaults: function(errorDetails, context) {
const methodSignature = context.methodSignature;
if (!methodSignature || !methodSignature.inputArguments) {
return { success: false, reason: "No method signature available" };
}
const currentParams = context.recoveredPayload || context.originalPayload || {};
const paramsWithDefaults = { ...currentParams };
let hasDefaults = false;
methodSignature.inputArguments.forEach(param => {
if (param.defaultValue !== undefined && paramsWithDefaults[param.name] === undefined) {
paramsWithDefaults[param.name] = param.defaultValue;
hasDefaults = true;
}
});
if (hasDefaults) {
context.recoveredPayload = paramsWithDefaults;
return {
success: true,
action: "apply_default_values",
changes: "Applied missing default values"
};
}
return {
success: false,
action: "apply_default_values",
reason: "No default values to apply"
};
},
scheduleRetry: function(errorDetails, context) {
const retryDelay = context.retryDelay || 1000;
const maxRetries = context.maxRetries || 3;
const currentRetry = context.retryCount || 0;
if (currentRetry >= maxRetries) {
return {
success: false,
action: "wait_and_retry",
reason: "Maximum retries exceeded"
};
}
// Schedule retry with exponential backoff
const delay = retryDelay * Math.pow(2, currentRetry);
setTimeout(() => {
node.send({
...context.originalMessage,
payload: context.recoveredPayload || context.originalPayload,
retryCount: currentRetry + 1,
isRetry: true
});
}, delay);
return {
success: true,
action: "wait_and_retry",
delay: delay,
retryNumber: currentRetry + 1
};
},
checkReconnection: function(errorDetails, context) {
// This would typically trigger a reconnection flow
return {
success: true,
action: "reconnect_if_needed",
note: "Reconnection check initiated"
};
}
};
// Execute recovery if error occurred
const statusCode = msg.statusCode;
if (statusCode !== "Good") {
const errorAnalysis = msg.errorAnalysis || {};
const recoveryContext = {
originalMessage: msg,
originalPayload: msg.payload,
methodSignature: msg.methodSignature,
retryCount: msg.retryCount || 0,
maxRetries: 3
};
const recoveryResult = RecoveryManager.executeRecovery(
errorAnalysis.errorType,
errorAnalysis,
recoveryContext
);
msg.recoveryResult = recoveryResult;
if (recoveryResult.success && recoveryResult.recoveredPayload) {
// Recovery successful, retry with corrected parameters
node.log("Automatic recovery successful, retrying method call");
msg.payload = recoveryResult.recoveredPayload;
msg.isRecovery = true;
msg.recoveryActions = recoveryResult.actions;
return msg;
} else {
// Recovery failed or not applicable
node.error(`Recovery failed: ${recoveryResult.reason || "Unknown error"}`);
msg.recoveryFailed = true;
return msg;
}
} else {
// Success case
return msg;
}
2. circuit breaker pattern
// Implement circuit breaker for failing methods
const CircuitBreaker = {
breakers: {},
// Circuit breaker states
states: {
CLOSED: "closed", // Normal operation
OPEN: "open", // Blocking calls due to failures
HALF_OPEN: "half_open" // Testing if service recovered
},
getBreaker: function(methodKey) {
if (!this.breakers[methodKey]) {
this.breakers[methodKey] = {
state: this.states.CLOSED,
failureCount: 0,
lastFailureTime: 0,
successCount: 0,
threshold: 5, // Open after 5 failures
timeout: 60000, // 1 minute timeout
halfOpenMaxCalls: 3 // Test with 3 calls in half-open
};
}
return this.breakers[methodKey];
},
canExecute: function(methodKey) {
const breaker = this.getBreaker(methodKey);
const now = Date.now();
switch (breaker.state) {
case this.states.CLOSED:
return { allowed: true, reason: "Circuit closed" };
case this.states.OPEN:
if (now - breaker.lastFailureTime > breaker.timeout) {
// Timeout elapsed, move to half-open
breaker.state = this.states.HALF_OPEN;
breaker.successCount = 0;
return { allowed: true, reason: "Testing service recovery" };
}
return {
allowed: false,
reason: `Circuit open, retry in ${Math.round((breaker.timeout - (now - breaker.lastFailureTime)) / 1000)}s`
};
case this.states.HALF_OPEN:
if (breaker.successCount < breaker.halfOpenMaxCalls) {
return { allowed: true, reason: "Testing in half-open state" };
}
return { allowed: false, reason: "Half-open call limit reached" };
}
},
recordSuccess: function(methodKey) {
const breaker = this.getBreaker(methodKey);
switch (breaker.state) {
case this.states.CLOSED:
breaker.failureCount = 0; // Reset failure count
break;
case this.states.HALF_OPEN:
breaker.successCount++;
if (breaker.successCount >= breaker.halfOpenMaxCalls) {
// Recovered, close circuit
breaker.state = this.states.CLOSED;
breaker.failureCount = 0;
breaker.successCount = 0;
node.log(`Circuit breaker closed for ${methodKey} - service recovered`);
}
break;
}
},
recordFailure: function(methodKey) {
const breaker = this.getBreaker(methodKey);
const now = Date.now();
breaker.failureCount++;
breaker.lastFailureTime = now;
switch (breaker.state) {
case this.states.CLOSED:
if (breaker.failureCount >= breaker.threshold) {
breaker.state = this.states.OPEN;
node.warn(`Circuit breaker opened for ${methodKey} - too many failures`);
}
break;
case this.states.HALF_OPEN:
// Failed during testing, go back to open
breaker.state = this.states.OPEN;
breaker.successCount = 0;
node.warn(`Circuit breaker reopened for ${methodKey} - test failed`);
break;
}
},
getStatus: function(methodKey) {
const breaker = this.getBreaker(methodKey);
return {
state: breaker.state,
failureCount: breaker.failureCount,
successCount: breaker.successCount,
lastFailureTime: breaker.lastFailureTime
};
}
};
// Check circuit breaker before method execution
const methodKey = `${msg.objectId}:${msg.methodId}`;
const execution = CircuitBreaker.canExecute(methodKey);
if (!execution.allowed) {
// Circuit breaker is open
node.warn(`Method call blocked by circuit breaker: ${execution.reason}`);
msg.payload = {
circuitBreakerBlocked: true,
reason: execution.reason,
breakerStatus: CircuitBreaker.getStatus(methodKey)
};
return msg;
}
// Method execution allowed, continue with call
msg.circuitBreakerStatus = CircuitBreaker.getStatus(methodKey);
return msg;
// After method execution (in a separate function node):
const statusCode = msg.statusCode;
const methodKey = `${msg.objectId}:${msg.methodId}`;
if (statusCode === "Good") {
CircuitBreaker.recordSuccess(methodKey);
msg.circuitBreakerAction = "success_recorded";
} else {
CircuitBreaker.recordFailure(methodKey);
msg.circuitBreakerAction = "failure_recorded";
}
msg.circuitBreakerStatus = CircuitBreaker.getStatus(methodKey);
return msg;
error logging and monitoring
1. structured error logging
// Comprehensive error logging system
const ErrorLogger = {
logLevels: {
ERROR: 0,
WARN: 1,
INFO: 2,
DEBUG: 3
},
currentLevel: 2, // INFO level
formatError: function(errorData) {
return {
timestamp: new Date().toISOString(),
nodeId: errorData.nodeId || "unknown",
flowId: errorData.flowId || "unknown",
level: errorData.level || "ERROR",
category: errorData.category || "method_call",
statusCode: errorData.statusCode,
objectId: errorData.objectId,
methodId: errorData.methodId,
message: errorData.message,
details: errorData.details || {},
context: errorData.context || {},
stackTrace: errorData.stackTrace,
correlationId: errorData.correlationId || this.generateCorrelationId()
};
},
generateCorrelationId: function() {
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
},
logError: function(errorData) {
const formattedError = this.formatError(errorData);
// Log to Node-RED
const logLevel = this.logLevels[formattedError.level.toUpperCase()] || 0;
if (logLevel <= this.currentLevel) {
const logMessage = `[${formattedError.level}] ${formattedError.category}: ${formattedError.message}`;
switch (formattedError.level.toUpperCase()) {
case "ERROR":
node.error(logMessage, formattedError);
break;
case "WARN":
node.warn(logMessage, formattedError);
break;
case "INFO":
node.log(logMessage, formattedError);
break;
case "DEBUG":
node.debug(logMessage, formattedError);
break;
}
}
// Send to external logging system if configured
this.sendToExternalLogger(formattedError);
return formattedError.correlationId;
},
sendToExternalLogger: function(errorData) {
// Implementation would send to external logging service
// For example: Elasticsearch, Splunk, Azure Monitor, etc.
if (global.get("external_logging_enabled")) {
const logEndpoint = global.get("log_endpoint");
if (logEndpoint) {
// Would implement HTTP request to logging service
node.trace(`Would send error to ${logEndpoint}: ${errorData.correlationId}`);
}
}
},
createErrorReport: function(methodCall, statusCode, additionalInfo = {}) {
const errorData = {
nodeId: node.id,
flowId: node.z,
level: statusCode === "Good" ? "INFO" : "ERROR",
category: "opcua_method_call",
statusCode: statusCode,
objectId: methodCall.objectId,
methodId: methodCall.methodId,
message: this.getErrorMessage(statusCode, methodCall),
details: {
parameters: methodCall.payload,
timeout: methodCall.timeout,
...additionalInfo
},
context: {
timestamp: methodCall.timestamp || Date.now(),
userAgent: methodCall.userAgent,
sessionId: methodCall.sessionId
}
};
return this.logError(errorData);
},
getErrorMessage: function(statusCode, methodCall) {
if (statusCode === "Good") {
return `Method call successful: ${methodCall.methodId}`;
}
const statusInfo = StatusCodeHandler.getStatusInfo(statusCode);
return `Method call failed: ${statusInfo.description} (${statusCode})`;
}
};
// Log method call result
const statusCode = msg.statusCode || "Unknown";
const methodCall = {
objectId: msg.objectId,
methodId: msg.methodId,
payload: msg.payload,
timeout: msg.timeout,
timestamp: msg.timestamp || Date.now()
};
const additionalInfo = {
executionTime: msg.executionTime,
retryCount: msg.retryCount || 0,
circuitBreakerStatus: msg.circuitBreakerStatus,
errorAnalysis: msg.errorAnalysis
};
const correlationId = ErrorLogger.createErrorReport(methodCall, statusCode, additionalInfo);
msg.correlationId = correlationId;
return msg;
2. error metrics and alerting
// Track error metrics and generate alerts
const ErrorMetrics = {
metrics: {
totalCalls: 0,
errorCalls: 0,
errorsByType: {},
errorsByMethod: {},
responseTimesMs: [],
lastErrorTime: null
},
recordCall: function(methodKey, statusCode, responseTime) {
this.metrics.totalCalls++;
if (responseTime) {
this.metrics.responseTimesMs.push(responseTime);
// Keep only last 1000 measurements
if (this.metrics.responseTimesMs.length > 1000) {
this.metrics.responseTimesMs.shift();
}
}
if (statusCode !== "Good") {
this.metrics.errorCalls++;
this.metrics.lastErrorTime = Date.now();
// Track by error type
if (!this.metrics.errorsByType[statusCode]) {
this.metrics.errorsByType[statusCode] = 0;
}
this.metrics.errorsByType[statusCode]++;
// Track by method
if (!this.metrics.errorsByMethod[methodKey]) {
this.metrics.errorsByMethod[methodKey] = 0;
}
this.metrics.errorsByMethod[methodKey]++;
}
},
getErrorRate: function(timeWindowMs = 3600000) { // 1 hour default
if (this.metrics.totalCalls === 0) return 0;
const now = Date.now();
const recentErrors = this.metrics.lastErrorTime &&
(now - this.metrics.lastErrorTime) < timeWindowMs;
return this.metrics.errorCalls / this.metrics.totalCalls;
},
getAverageResponseTime: function() {
if (this.metrics.responseTimesMs.length === 0) return 0;
const sum = this.metrics.responseTimesMs.reduce((a, b) => a + b, 0);
return sum / this.metrics.responseTimesMs.length;
},
checkAlerts: function() {
const alerts = [];
// High error rate alert
const errorRate = this.getErrorRate();
if (errorRate > 0.1) { // 10% error rate threshold
alerts.push({
type: "high_error_rate",
severity: "warning",
message: `High error rate detected: ${(errorRate * 100).toFixed(1)}%`,
value: errorRate,
threshold: 0.1
});
}
// High response time alert
const avgResponseTime = this.getAverageResponseTime();
if (avgResponseTime > 5000) { // 5 second threshold
alerts.push({
type: "high_response_time",
severity: "warning",
message: `High average response time: ${avgResponseTime.toFixed(0)}ms`,
value: avgResponseTime,
threshold: 5000
});
}
// Frequent specific error alert
Object.keys(this.metrics.errorsByType).forEach(errorType => {
const count = this.metrics.errorsByType[errorType];
if (count > 10) { // More than 10 of same error
alerts.push({
type: "frequent_error",
severity: "error",
message: `Frequent ${errorType} errors: ${count} occurrences`,
value: count,
errorType: errorType
});
}
});
return alerts;
},
generateReport: function() {
return {
summary: {
totalCalls: this.metrics.totalCalls,
errorCalls: this.metrics.errorCalls,
errorRate: this.getErrorRate(),
averageResponseTime: this.getAverageResponseTime()
},
errorsByType: this.metrics.errorsByType,
errorsByMethod: this.metrics.errorsByMethod,
alerts: this.checkAlerts(),
reportTime: new Date().toISOString()
};
}
};
// Record metrics for current call
const methodKey = `${msg.objectId}:${msg.methodId}`;
const statusCode = msg.statusCode;
const responseTime = msg.executionTime;
ErrorMetrics.recordCall(methodKey, statusCode, responseTime);
// Check for alerts
const alerts = ErrorMetrics.checkAlerts();
if (alerts.length > 0) {
msg.alerts = alerts;
// Log alerts
alerts.forEach(alert => {
const logLevel = alert.severity === "error" ? "error" : "warn";
node[logLevel](`ALERT: ${alert.message}`);
});
}
// Add metrics to message
msg.metrics = ErrorMetrics.generateReport();
return msg;
Best practices
- Comprehensive Status Code Handling: Handle all possible OPC UA status codes appropriately
- Automatic Recovery: Implement automatic recovery for common, recoverable errors
- Circuit Breaker Pattern: Protect against cascading failures with circuit breakers
- Structured Logging: Use structured, searchable error logs with correlation IDs
- Error Metrics: Track error rates and patterns for system health monitoring
- Graceful Degradation: Design fallback mechanisms for critical functionality
- User-Friendly Messages: Provide clear, actionable error messages to operators