diff --git a/common.props b/common.props
index e7a9f994b3..d4d950fa63 100644
--- a/common.props
+++ b/common.props
@@ -42,8 +42,8 @@
-
-
+
+
diff --git a/deploy/docker/docker-compose.yaml b/deploy/docker/docker-compose.yaml
index df131936c7..ecfc4024be 100644
--- a/deploy/docker/docker-compose.yaml
+++ b/deploy/docker/docker-compose.yaml
@@ -5,20 +5,20 @@ services:
############################
opcplc:
container_name: opcplc
- image: mcr.microsoft.com/iotedge/opc-plc:${OPC_PLC_TAG:-2.9.10}
+ image: mcr.microsoft.com/iotedge/opc-plc:${OPC_PLC_TAG:-latest}
ports:
- "50000:50000"
command: [
- "--sph",
+ "--sph=True",
"--spf=/shared/pn.json",
"--pn=50000",
- "--alm",
- "--ses",
+ "--alm=True",
+ "--ses=True",
"--ei=${EVENT_NODES:-100}",
"--gn=${GUID_NODES:-100}",
"--fn=${FAST_NODES:-99900}",
"--sn=${SLOW_NODES:-99900}",
- "--aa"
+ "--aa=True"
]
volumes:
- shared:/shared:rw
@@ -41,6 +41,7 @@ services:
"--cl=5",
"--rs",
"--dm=True",
+ "--lfm=syslog",
"--pki=/shared/pki",
"--pf=/shared/pn.json",
"--npd=${NODES_PER_DATASET:-10000}"
diff --git a/deploy/docker/with-monitor.yaml b/deploy/docker/with-monitor.yaml
index d7d7c71854..51490bbc54 100644
--- a/deploy/docker/with-monitor.yaml
+++ b/deploy/docker/with-monitor.yaml
@@ -13,7 +13,7 @@ services:
environment:
DOTNET_DiagnosticPorts: /shared/diag/dotnet-monitor.sock
############################
- # Optional dotnet-monitor
+ # dotnet-monitor
############################
monitor:
container_name: monitor
diff --git a/deploy/docker/with-pcap-capture.yaml b/deploy/docker/with-pcap-capture.yaml
new file mode 100644
index 0000000000..3f4f975905
--- /dev/null
+++ b/deploy/docker/with-pcap-capture.yaml
@@ -0,0 +1,34 @@
+version: "3.9"
+services:
+ ############################
+ # OPC PLC Simulation
+ ############################
+ opcplc:
+ command: [
+ "--sph=True",
+ "--spf=/shared/pn.json",
+ "--pn=50000",
+ "--fn=8000",
+ "--aa=True",
+ "--ut=True"
+ ]
+ ############################
+ # Network capture
+ ############################
+ pcap:
+ container_name: pcap
+ image: travelping/pcap
+ cap_add:
+ - NET_ADMIN
+ network_mode: host
+ volumes:
+ - shared:/data:rw
+ environment:
+ IFACE: any
+ FORMAT: pcapng
+ MAXFILENUM: 10
+ MAXFILESIZE: 200
+ FILENAME: dump.pcap
+ FILTER: "src or dst host 192.168.80.2"
+volumes:
+ shared:
diff --git a/docs/opc-publisher/commandline.md b/docs/opc-publisher/commandline.md
index cc1f6ceb0d..b09adfbad3 100644
--- a/docs/opc-publisher/commandline.md
+++ b/docs/opc-publisher/commandline.md
@@ -18,8 +18,8 @@ When both environment variable and CLI argument are provided, the command line o
██║ ██║██╔═══╝ ██║ ██╔═══╝ ██║ ██║██╔══██╗██║ ██║╚════██║██╔══██║██╔══╝ ██╔══██╗
╚██████╔╝██║ ╚██████╗ ██║ ╚██████╔╝██████╔╝███████╗██║███████║██║ ██║███████╗██║ ██║
╚═════╝ ╚═╝ ╚═════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚══════╝╚═╝╚══════╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝
+ 2.9.4 (.NET 8.0.1/win-x64/OPC Stack 1.4.372.116)
- 2.9.4 (.NET 8.0.0/win-x64/OPC Stack 1.4.372.106)
General
-------
@@ -806,6 +806,14 @@ Diagnostic options
`Critical`
`None`
Default: `Information`.
+ --lfm, --logformat, --LogFormat=VALUE
+ The logging format to use when writing to the
+ console.
+ Allowed values:
+ `simple`
+ `syslog`
+ `systemd`
+ Default: `simple`.
--di, --diagnosticsinterval, --DiagnosticsInterval=VALUE
Produce publisher diagnostic information at this
specified interval in seconds.
@@ -856,15 +864,6 @@ Diagnostic options
metrics directly on the standard path.
Default: `disabled` if Otlp collector is
configured, otherwise `enabled`.
- --cap, --capturedevice, --CaptureDevice=VALUE
- The capture device to use to capture network
- traffic.
- Network capture is not supported on this system.
- --cpf, --capturefile, --CaptureFileName=VALUE
- The file name to capture traffic to.
- A device must be selected using `--cd` if
- capture capability is supported on this system.
- Default: `opcua.pcap`.
```
Currently supported combinations of `--mm` snd `--me` can be found [here](./messageformats.md).
diff --git a/docs/release-announcement.md b/docs/release-announcement.md
index d6c2fb7635..7452b90088 100644
--- a/docs/release-announcement.md
+++ b/docs/release-announcement.md
@@ -65,6 +65,7 @@ We are pleased to announce the release of version 2.9.4 of OPC Publisher and the
- Recreate session if it expires on server (#2138)
- Log subscription keep alive error only when session is connected (#2137)
- Update OPC UA .net stack to latest version (1.4.372.116-preview) to enable fully async reconnect and fix several issues in previous versions.
+- Added the ability to switch publisher to emit logs in syslog or systemd format using --lfm command line option.
- Fix issue where certain publish errors cause reconnect state machine to fail (#2104, #2136)
## Azure Industrial IoT OPC Publisher 2.9.3
diff --git a/src/Azure.IIoT.OpcUa.Publisher.Models/tests/Azure.IIoT.OpcUa.Publisher.Models.Tests.csproj b/src/Azure.IIoT.OpcUa.Publisher.Models/tests/Azure.IIoT.OpcUa.Publisher.Models.Tests.csproj
index 5d2bceba34..1400924e0e 100644
--- a/src/Azure.IIoT.OpcUa.Publisher.Models/tests/Azure.IIoT.OpcUa.Publisher.Models.Tests.csproj
+++ b/src/Azure.IIoT.OpcUa.Publisher.Models/tests/Azure.IIoT.OpcUa.Publisher.Models.Tests.csproj
@@ -10,7 +10,7 @@
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
all
runtime; build; native; contentfiles; analyzers
diff --git a/src/Azure.IIoT.OpcUa.Publisher.Module/cli/Program.cs b/src/Azure.IIoT.OpcUa.Publisher.Module/cli/Program.cs
index 60f13872d3..22d33ce1b7 100644
--- a/src/Azure.IIoT.OpcUa.Publisher.Module/cli/Program.cs
+++ b/src/Azure.IIoT.OpcUa.Publisher.Module/cli/Program.cs
@@ -381,7 +381,7 @@ private static async Task LoadPnJson(string publishProfile,
}
if (publishedNodesFile != null && File.Exists(publishedNodesFile))
{
- var publishedNodesFilePath = Path.GetTempFileName();
+ const string publishedNodesFilePath = "profile.json";
await File.WriteAllTextAsync(publishedNodesFilePath,
(await File.ReadAllTextAsync(publishedNodesFile, ct).ConfigureAwait(false))
diff --git a/src/Azure.IIoT.OpcUa.Publisher.Module/src/Runtime/CommandLine.cs b/src/Azure.IIoT.OpcUa.Publisher.Module/src/Runtime/CommandLine.cs
index 40bbdb9f8d..015ff002b9 100644
--- a/src/Azure.IIoT.OpcUa.Publisher.Module/src/Runtime/CommandLine.cs
+++ b/src/Azure.IIoT.OpcUa.Publisher.Module/src/Runtime/CommandLine.cs
@@ -448,9 +448,12 @@ public CommandLine(string[] args, CommandLineLogger? logger = null)
"------------------",
"",
- { $"ll|loglevel=|{Configuration.Logging.LogLevelKey}=",
+ { $"ll|loglevel=|{Configuration.LoggingLevel.LogLevelKey}=",
$"The loglevel to use.\nAllowed values:\n `{string.Join("`\n `", Enum.GetNames(typeof(LogLevel)))}`\nDefault: `{LogLevel.Information}`.\n",
- (LogLevel l) => this[Configuration.Logging.LogLevelKey] = l.ToString() },
+ (LogLevel l) => this[Configuration.LoggingLevel.LogLevelKey] = l.ToString() },
+ { $"lfm|logformat=|{Configuration.LoggingFormat.LogFormatKey}=",
+ $"The log format to use when writing to the console.\nAllowed values:\n `{string.Join("`\n `", Configuration.LoggingFormat.LogFormatsSupported)}`\nDefault: `{Configuration.LoggingFormat.LogFormatDefault}`.\n",
+ (string s) => this[Configuration.LoggingFormat.LogFormatKey] = s },
{ $"di|diagnosticsinterval=|{PublisherConfig.DiagnosticsIntervalKey}=",
"Produce publisher diagnostic information at this specified interval in seconds.\nBy default diagnostics are written to the OPC Publisher logger (which requires at least --loglevel `information`) unless configured differently using `--pd`.\n`0` disables diagnostic output.\nDefault:60000 (60 seconds).\nAlso can be set using `DiagnosticsInterval` environment variable in the form of a duration string in the form `[d.]hh:mm:ss[.fffffff]`\".\n",
(int i) => this[PublisherConfig.DiagnosticsIntervalKey] = TimeSpan.FromSeconds(i).ToString() },
@@ -475,12 +478,6 @@ public CommandLine(string[] args, CommandLineLogger? logger = null)
{ $"em|enableprometheusendpoint=|{Configuration.Otlp.EnableMetricsKey}=",
"Explicitly enable or disable exporting prometheus metrics directly on the standard path.\nDefault: `disabled` if Otlp collector is configured, otherwise `enabled`.\n",
(bool? b) => this[Configuration.Otlp.EnableMetricsKey] = b?.ToString() ?? "True" },
- { $"cap|capturedevice=|{OpcUaClientConfig.CaptureDeviceKey}=",
- $"The capture device to use to capture network traffic.\n{SupportsCapture(OpcUaClientCapture.AvailableDevices)}\n",
- (string s) => this[OpcUaClientConfig.CaptureDeviceKey] = s },
- { $"cpf|capturefile=|{OpcUaClientConfig.CaptureFileNameKey}=",
- $"The file name to capture traffic to.\nA device must be selected using `--cd` if capture capability is supported on this system.\nDefault: `{OpcUaClientConfig.CaptureFileNameDefault}`.\n",
- (string s) => this[OpcUaClientConfig.CaptureFileNameKey] = s },
// testing purposes
@@ -618,15 +615,6 @@ void SetStoreType(string s, string storeTypeKey, string optionName)
}
}
- private static string SupportsCapture(IReadOnlyList devices)
- {
- if (devices.Count == 0)
- {
- return "Network capture is not supported on this system.";
- }
- return $"Available devices on your system:\n `{string.Join("`\n `", devices)}`\nDefault: `null` (disabled).";
- }
-
private readonly CommandLineLogger _logger;
}
diff --git a/src/Azure.IIoT.OpcUa.Publisher.Module/src/Runtime/Configuration.cs b/src/Azure.IIoT.OpcUa.Publisher.Module/src/Runtime/Configuration.cs
index 689976f0ec..4c5e2c3f72 100644
--- a/src/Azure.IIoT.OpcUa.Publisher.Module/src/Runtime/Configuration.cs
+++ b/src/Azure.IIoT.OpcUa.Publisher.Module/src/Runtime/Configuration.cs
@@ -33,6 +33,8 @@ namespace Azure.IIoT.OpcUa.Publisher.Module.Runtime
using System.Linq;
using System.Net;
using System.Text.RegularExpressions;
+ using Microsoft.Extensions.Logging.Console;
+ using static Azure.IIoT.OpcUa.Publisher.Module.Runtime.Configuration;
///
/// Configuration extensions
@@ -54,8 +56,16 @@ public static void AddPublisherServices(this ContainerBuilder builder)
.AsImplementedInterfaces().SingleInstance();
builder.RegisterType()
.AsImplementedInterfaces().AsSelf().SingleInstance();
- builder.RegisterType()
+ builder.RegisterType()
.AsImplementedInterfaces();
+ builder.RegisterType>()
+ .AsImplementedInterfaces();
+ builder.RegisterType>()
+ .AsImplementedInterfaces();
+ builder.RegisterType>()
+ .AsImplementedInterfaces();
+ builder.RegisterType()
+ .AsImplementedInterfaces().AsSelf().SingleInstance();
builder.RegisterType()
.AsImplementedInterfaces();
@@ -445,9 +455,9 @@ public override void Configure(string? name, KestrelServerOptions options)
}
///
- /// Configure logger factory
+ /// Configure logger filter
///
- internal sealed class Logging : ConfigureOptionBase
+ internal sealed class LoggingLevel : ConfigureOptionBase
{
///
/// Configuration
@@ -457,9 +467,80 @@ internal sealed class Logging : ConfigureOptionBase
///
public override void Configure(string? name, LoggerFilterOptions options)
{
- if (Enum.TryParse(GetStringOrDefault(LogLevelKey), out var logLevel))
+ var levelString = GetStringOrDefault(LogLevelKey);
+ if (!string.IsNullOrEmpty(levelString))
+ {
+ if (Enum.TryParse(levelString, out var logLevel))
+ {
+ options.MinLevel = logLevel;
+ }
+ else
+ {
+ // Compatibilty with serilog
+ switch (levelString)
+ {
+ case "Verbose":
+ options.MinLevel = LogLevel.Trace;
+ break;
+ case "Fatal":
+ options.MinLevel = LogLevel.Critical;
+ break;
+ }
+ }
+ }
+ }
+
+ ///
+ /// Create logging configurator
+ ///
+ ///
+ public LoggingLevel(IConfiguration configuration) : base(configuration)
+ {
+ }
+ }
+
+ ///
+ /// Logging format
+ ///
+ internal class LoggingFormat : PostConfigureOptionBase
+ {
+ ///
+ /// Supported formats
+ ///
+ public static readonly string[] LogFormatsSupported = new[]
+ {
+ ConsoleFormatterNames.Simple,
+ Syslog.FormatterName,
+ ConsoleFormatterNames.Systemd
+ };
+
+ ///
+ /// Configuration
+ ///
+ public const string LogFormatKey = "LogFormat";
+
+ ///
+ /// Default format
+ ///
+ public const string LogFormatDefault = ConsoleFormatterNames.Simple;
+
+ ///
+ public override void PostConfigure(string? name, ConsoleLoggerOptions options)
+ {
+ switch (GetStringOrDefault(LogFormatKey))
{
- options.MinLevel = logLevel;
+ case Syslog.FormatterName:
+ options.FormatterName = Syslog.FormatterName;
+ break;
+ case ConsoleFormatterNames.Systemd:
+ options.FormatterName = ConsoleFormatterNames.Systemd;
+ break;
+ case ConsoleFormatterNames.Simple:
+ options.FormatterName = ConsoleFormatterNames.Simple;
+ break;
+ default:
+ options.FormatterName = LogFormatDefault;
+ break;
}
}
@@ -467,7 +548,37 @@ public override void Configure(string? name, LoggerFilterOptions options)
/// Create logging configurator
///
///
- public Logging(IConfiguration configuration) : base(configuration)
+ public LoggingFormat(IConfiguration configuration) : base(configuration)
+ {
+ }
+ }
+
+ ///
+ /// Logging format
+ ///
+ ///
+ internal sealed class ConsoleLogging : LoggingFormat,
+ IConfigureOptions, IConfigureNamedOptions where T : ConsoleFormatterOptions
+ {
+ ///
+ public void Configure(string? name, T options)
+ {
+ options.TimestampFormat = "[yy-MM-dd HH:mm:ss.ffff] ";
+ options.IncludeScopes = true;
+ options.UseUtcTimestamp = true;
+ }
+
+ ///
+ public void Configure(T options)
+ {
+ Configure(null, options);
+ }
+
+ ///
+ /// Create logging configurator
+ ///
+ ///
+ public ConsoleLogging(IConfiguration configuration) : base(configuration)
{
}
}
diff --git a/src/Azure.IIoT.OpcUa.Publisher.Module/src/Runtime/Syslog.cs b/src/Azure.IIoT.OpcUa.Publisher.Module/src/Runtime/Syslog.cs
new file mode 100644
index 0000000000..225444c3a3
--- /dev/null
+++ b/src/Azure.IIoT.OpcUa.Publisher.Module/src/Runtime/Syslog.cs
@@ -0,0 +1,108 @@
+// ------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License (MIT). See License.txt in the repo root for license information.
+// ------------------------------------------------------------
+
+namespace Azure.IIoT.OpcUa.Publisher.Module.Runtime
+{
+ using Microsoft.Extensions.Logging;
+ using Microsoft.Extensions.Options;
+ using System;
+ using System.Globalization;
+ using Microsoft.Extensions.Logging.Abstractions;
+ using Microsoft.Extensions.Logging.Console;
+ using System.IO;
+ using System.Text;
+
+ ///
+ /// Logging formatter compatible with syslogs format.
+ ///
+ public sealed class Syslog : ConsoleFormatter, IDisposable
+ {
+ ///
+ /// The default timestamp format for all IoT compatible logging events..
+ ///
+ public const string DefaultTimestampFormat = "yyyy-MM-ddTHH:mm:ss.fffZ ";
+
+ ///
+ /// Name of the formatter
+ ///
+ public const string FormatterName = "syslog";
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ ///
+ public Syslog(IOptionsMonitor options)
+ : base(FormatterName)
+ {
+ _optionsReloadToken = options.OnChange(opt =>
+ {
+ _options = opt;
+ _includeScopes = opt.IncludeScopes;
+ });
+ _options = options.CurrentValue;
+ _serviceId = "opcpublisher@311";
+ _timestampFormat = DefaultTimestampFormat;
+ _includeScopes = _options.IncludeScopes;
+ }
+
+ ///
+ public override void Write(in LogEntry logEntry,
+ IExternalScopeProvider? scopeProvider, TextWriter textWriter)
+ {
+ string? message = logEntry.Formatter?.Invoke(logEntry.State, logEntry.Exception);
+ if (message is null)
+ {
+ return;
+ }
+ var messageBuilder = new StringBuilder(_initialLength)
+ .Append(_syslogMap[(int)logEntry.LogLevel])
+ .Append(DateTime.UtcNow.ToString(_timestampFormat, CultureInfo.InvariantCulture));
+ if (_includeScopes && scopeProvider != null && !string.IsNullOrEmpty(_serviceId))
+ {
+ messageBuilder.Append('[').Append(_serviceId);
+ scopeProvider.ForEachScope((scope, state) =>
+ {
+ StringBuilder builder = state;
+ builder.Append(' ').Append(scope);
+ }, messageBuilder);
+ messageBuilder.Append("] ");
+ }
+ messageBuilder.Append("- ").AppendLine(message);
+ if (logEntry.Exception != null)
+ {
+ // TODO: syslog format does not support stack traces
+ messageBuilder.AppendLine(logEntry.Exception.ToString());
+ }
+ textWriter.Write(messageBuilder.ToString());
+ }
+
+ ///
+ public void Dispose()
+ {
+ _optionsReloadToken?.Dispose();
+ }
+
+ private const int _initialLength = 256;
+
+ ///
+ /// Map of to syslog severity.
+ ///
+ private static readonly string[] _syslogMap = new[]
+ {
+ /* Trace */ "<7>",
+ /* Debug */ "<7>",
+ /* Info */ "<6>",
+ /* Warn */ "<4>",
+ /* Error */ "<3>",
+ /* Crit */ "<3>"
+ };
+
+ private readonly IDisposable? _optionsReloadToken;
+ private readonly string _timestampFormat;
+ private readonly string _serviceId;
+ private bool _includeScopes;
+ private ConsoleFormatterOptions _options;
+ }
+}
diff --git a/src/Azure.IIoT.OpcUa.Publisher.Module/src/Startup.cs b/src/Azure.IIoT.OpcUa.Publisher.Module/src/Startup.cs
index 6ce46d3a30..11da21969a 100644
--- a/src/Azure.IIoT.OpcUa.Publisher.Module/src/Startup.cs
+++ b/src/Azure.IIoT.OpcUa.Publisher.Module/src/Startup.cs
@@ -20,6 +20,7 @@ namespace Azure.IIoT.OpcUa.Publisher.Module
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
using System;
+ using Microsoft.Extensions.Logging.Console;
///
/// Webservice startup
@@ -60,13 +61,8 @@ public void ConfigureServices(IServiceCollection services)
{
services.AddLogging(options => options
.AddFilter(typeof(IAwaitable).Namespace, LogLevel.Warning)
- .AddSimpleConsole(options =>
- {
- // options.SingleLine = true;
- options.IncludeScopes = true;
- options.UseUtcTimestamp = true;
- options.TimestampFormat = "[yy-MM-dd HH:mm:ss.ffff] ";
- })
+ .AddConsole()
+ .AddConsoleFormatter()
.AddOpenTelemetry(Configuration, options =>
{
options.IncludeScopes = true;
diff --git a/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Azure.IIoT.OpcUa.Publisher.Module.Tests.csproj b/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Azure.IIoT.OpcUa.Publisher.Module.Tests.csproj
index 9e1295d8ca..2ada11f497 100644
--- a/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Azure.IIoT.OpcUa.Publisher.Module.Tests.csproj
+++ b/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Azure.IIoT.OpcUa.Publisher.Module.Tests.csproj
@@ -9,7 +9,7 @@
-
+
all
runtime; build; native; contentfiles; analyzers
diff --git a/src/Azure.IIoT.OpcUa.Publisher.Service.WebApi/tests/Azure.IIoT.OpcUa.Publisher.Service.WebApi.Tests.csproj b/src/Azure.IIoT.OpcUa.Publisher.Service.WebApi/tests/Azure.IIoT.OpcUa.Publisher.Service.WebApi.Tests.csproj
index 8fbdf06243..694090077c 100644
--- a/src/Azure.IIoT.OpcUa.Publisher.Service.WebApi/tests/Azure.IIoT.OpcUa.Publisher.Service.WebApi.Tests.csproj
+++ b/src/Azure.IIoT.OpcUa.Publisher.Service.WebApi/tests/Azure.IIoT.OpcUa.Publisher.Service.WebApi.Tests.csproj
@@ -10,7 +10,7 @@
-
+
all
runtime; build; native; contentfiles; analyzers
diff --git a/src/Azure.IIoT.OpcUa.Publisher.Service/tests/Azure.IIoT.OpcUa.Publisher.Service.Tests.csproj b/src/Azure.IIoT.OpcUa.Publisher.Service/tests/Azure.IIoT.OpcUa.Publisher.Service.Tests.csproj
index c4f7373e7a..915f662eaa 100644
--- a/src/Azure.IIoT.OpcUa.Publisher.Service/tests/Azure.IIoT.OpcUa.Publisher.Service.Tests.csproj
+++ b/src/Azure.IIoT.OpcUa.Publisher.Service/tests/Azure.IIoT.OpcUa.Publisher.Service.Tests.csproj
@@ -12,7 +12,7 @@
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
all
runtime; build; native; contentfiles; analyzers
diff --git a/src/Azure.IIoT.OpcUa.Publisher.Testing/tests/Azure.IIoT.OpcUa.Publisher.Testing.csproj b/src/Azure.IIoT.OpcUa.Publisher.Testing/tests/Azure.IIoT.OpcUa.Publisher.Testing.csproj
index 90b61398ff..7443fce93c 100644
--- a/src/Azure.IIoT.OpcUa.Publisher.Testing/tests/Azure.IIoT.OpcUa.Publisher.Testing.csproj
+++ b/src/Azure.IIoT.OpcUa.Publisher.Testing/tests/Azure.IIoT.OpcUa.Publisher.Testing.csproj
@@ -12,7 +12,7 @@
-
+
diff --git a/src/Azure.IIoT.OpcUa.Publisher/src/Azure.IIoT.OpcUa.Publisher.csproj b/src/Azure.IIoT.OpcUa.Publisher/src/Azure.IIoT.OpcUa.Publisher.csproj
index ac081a3a42..fb5dbf4155 100644
--- a/src/Azure.IIoT.OpcUa.Publisher/src/Azure.IIoT.OpcUa.Publisher.csproj
+++ b/src/Azure.IIoT.OpcUa.Publisher/src/Azure.IIoT.OpcUa.Publisher.csproj
@@ -4,12 +4,6 @@
true
enable
-
- SHARPPCAP
-
-
-
-
diff --git a/src/Azure.IIoT.OpcUa.Publisher/src/Services/PublisherDiagnosticCollector.cs b/src/Azure.IIoT.OpcUa.Publisher/src/Services/PublisherDiagnosticCollector.cs
index 9676cbcd4c..6073ca5f22 100644
--- a/src/Azure.IIoT.OpcUa.Publisher/src/Services/PublisherDiagnosticCollector.cs
+++ b/src/Azure.IIoT.OpcUa.Publisher/src/Services/PublisherDiagnosticCollector.cs
@@ -195,14 +195,7 @@ internal WriterGroupDiagnosticModel AggregateModel
writers.Sum(w => w.MonitoredOpcNodesFailedCount),
MonitoredOpcNodesSucceededCount = MonitoredOpcNodesSucceededCount +
writers.Sum(w => w.MonitoredOpcNodesSucceededCount),
- ConnectionRetries = ConnectionRetries +
- writers.Sum(w => w.ConnectionRetries),
- NumberOfDisconnectedEndpoints = NumberOfDisconnectedEndpoints +
- writers.Sum(w => w.NumberOfDisconnectedEndpoints),
- NumberOfConnectedEndpoints = NumberOfConnectedEndpoints +
- writers.Sum(w => w.NumberOfConnectedEndpoints),
- OpcEndpointConnected = NumberOfConnectedEndpoints != 0 ||
- writers.Any(w => w.NumberOfConnectedEndpoints != 0),
+ OpcEndpointConnected = NumberOfConnectedEndpoints != 0,
PublishRequestsRatio = PublishRequestsRatio +
writers.Sum(w => w.PublishRequestsRatio),
BadPublishRequestsRatio = BadPublishRequestsRatio +
diff --git a/src/Azure.IIoT.OpcUa.Publisher/src/Services/WriterGroupDataSource.cs b/src/Azure.IIoT.OpcUa.Publisher/src/Services/WriterGroupDataSource.cs
index de0692f04a..c8e0cff5c8 100644
--- a/src/Azure.IIoT.OpcUa.Publisher/src/Services/WriterGroupDataSource.cs
+++ b/src/Azure.IIoT.OpcUa.Publisher/src/Services/WriterGroupDataSource.cs
@@ -886,6 +886,20 @@ private long DataChangesCount
}
}
+ private IEnumerable UsedClients => _subscriptions.Values
+ .Select(s => s.Subscription?.State!)
+ .Where(s => s != null)
+ .Distinct();
+
+ private int ReconnectCount => UsedClients
+ .Sum(s => s.ReconnectCount);
+
+ private int ConnectedClients => UsedClients
+ .Count(s => s.State == EndpointConnectivityState.Ready);
+
+ private int DisconnectedClients => UsedClients
+ .Count(s => s.State != EndpointConnectivityState.Ready);
+
///
/// Create observable metrics
///
@@ -927,6 +941,15 @@ private void InitializeMetrics()
_meter.CreateObservableCounter("iiot_edge_publisher_keep_alive_notifications",
() => new Measurement(_keepAliveCount, _metrics.TagList), "Notifications",
"Total Opc keep alive notifications delivered for processing.");
+ _meter.CreateObservableUpDownCounter("iiot_edge_publisher_connection_retries",
+ () => new Measurement(ReconnectCount,
+ _metrics.TagList), "Attempts", "OPC UA connect retries.");
+ _meter.CreateObservableGauge("iiot_edge_publisher_is_connection_ok",
+ () => new Measurement(ConnectedClients,
+ _metrics.TagList), "Endpoints", "OPC UA endpoints that are successfully connected.");
+ _meter.CreateObservableGauge("iiot_edge_publisher_is_disconnected",
+ () => new Measurement(DisconnectedClients,
+ _metrics.TagList), "Endpoints", "OPC UA endpoints that are disconnected.");
}
private const long kNumberOfInvokedMessagesResetThreshold = long.MaxValue - 10000;
diff --git a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Extensions/ContainerBuilderEx.cs b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Extensions/ContainerBuilderEx.cs
index 06e4f28953..d9d83718e2 100644
--- a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Extensions/ContainerBuilderEx.cs
+++ b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Extensions/ContainerBuilderEx.cs
@@ -31,8 +31,6 @@ public static void AddOpcUaStack(this ContainerBuilder builder)
.AsImplementedInterfaces().SingleInstance();
builder.RegisterType()
.AsImplementedInterfaces().SingleInstance();
- builder.RegisterType()
- .AsImplementedInterfaces().SingleInstance();
builder.RegisterType()
.AsImplementedInterfaces();
diff --git a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/IOpcUaClient.cs b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/IOpcUaClient.cs
index 21f162f239..9a3e483598 100644
--- a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/IOpcUaClient.cs
+++ b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/IOpcUaClient.cs
@@ -39,6 +39,8 @@ ValueTask GetSessionHandleAsync(
/// is not connected.
///
///
- void ManageSubscription(IOpcUaSubscription subscription);
+ ///
+ void ManageSubscription(IOpcUaSubscription subscription,
+ bool closeSubscription = false);
}
}
diff --git a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/IOpcUaClientDiagnostics.cs b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/IOpcUaClientDiagnostics.cs
index 379cc59292..fa6f6acf61 100644
--- a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/IOpcUaClientDiagnostics.cs
+++ b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/IOpcUaClientDiagnostics.cs
@@ -10,7 +10,7 @@ namespace Azure.IIoT.OpcUa.Publisher.Stack
///
/// Safely access client state for diagnostics
///
- internal interface IOpcUaClientDiagnostics
+ public interface IOpcUaClientDiagnostics
{
///
/// Bad publish requests tracked by this client
diff --git a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/IOpcUaSubscription.cs b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/IOpcUaSubscription.cs
index 88ee0dec6c..ad967a4081 100644
--- a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/IOpcUaSubscription.cs
+++ b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/IOpcUaSubscription.cs
@@ -17,8 +17,8 @@ namespace Azure.IIoT.OpcUa.Publisher.Stack
internal interface IOpcUaSubscription
{
///
- /// Apply the current subscription configuration to
- /// the session.
+ /// Create or update the subscription now using the
+ /// currently configured subscription configuration.
///
///
///
@@ -34,5 +34,17 @@ ValueTask SyncWithSessionAsync(ISession session,
///
bool TryGetCurrentPosition(out uint subscriptionId,
out uint sequenceNumber);
+
+ ///
+ /// Notifies the subscription that should remove
+ /// itself from the session. If the session is null
+ /// then there is no session and the subscription
+ /// should clean up.
+ ///
+ ///
+ ///
+ ///
+ ValueTask CloseInSessionAsync(ISession? session,
+ CancellationToken ct = default);
}
}
diff --git a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/ISubscriptionHandle.cs b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/ISubscriptionHandle.cs
index e46a8ad582..53515cb125 100644
--- a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/ISubscriptionHandle.cs
+++ b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/ISubscriptionHandle.cs
@@ -6,8 +6,6 @@
namespace Azure.IIoT.OpcUa.Publisher.Stack
{
using Azure.IIoT.OpcUa.Publisher.Stack.Models;
- using System.Threading;
- using System.Threading.Tasks;
///
/// Subscription handle is a safe abstraction that allows the owner of the
@@ -26,6 +24,11 @@ public interface ISubscriptionHandle
///
ushort LocalIndex { get; }
+ ///
+ /// State of the underlying client
+ ///
+ IOpcUaClientDiagnostics State { get; }
+
///
/// Create a keep alive notification
///
diff --git a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaClient.cs b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaClient.cs
index 0f16c33641..9c1c946d5a 100644
--- a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaClient.cs
+++ b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaClient.cs
@@ -217,9 +217,10 @@ public bool TryGetSession([NotNullWhen(true)] out ISession? session)
}
///
- public void ManageSubscription(IOpcUaSubscription subscription)
+ public void ManageSubscription(IOpcUaSubscription subscription, bool closeSubscription)
{
- TriggerConnectionEvent(ConnectionEvent.SubscriptionManage, subscription);
+ TriggerConnectionEvent(closeSubscription ?
+ ConnectionEvent.SubscriptionClose : ConnectionEvent.SubscriptionManage, subscription);
}
///
@@ -709,6 +710,13 @@ await ApplySubscriptionAsync(new[] { item }, queuedSubscriptions,
}
break;
+ case ConnectionEvent.SubscriptionClose:
+ var sub = context as IOpcUaSubscription;
+ Debug.Assert(sub != null);
+ queuedSubscriptions.Remove(sub);
+ await sub.CloseInSessionAsync(_session, ct).ConfigureAwait(false);
+ break;
+
case ConnectionEvent.StartReconnect: // sent by the keep alive timeout path
switch (currentSessionState)
{
@@ -1270,6 +1278,7 @@ private async ValueTask UpdateSessionAsync(ISession session)
if (ReferenceEquals(_session, session))
{
// Not a new session
+ NotifyConnectivityStateChange(EndpointConnectivityState.Ready);
return false;
}
@@ -1567,7 +1576,8 @@ private enum ConnectionEvent
Disconnect,
StartReconnect,
ReconnectComplete,
- SubscriptionManage
+ SubscriptionManage,
+ SubscriptionClose
}
private enum SessionState
diff --git a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaClientCapture.cs b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaClientCapture.cs
deleted file mode 100644
index 73604430c6..0000000000
--- a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaClientCapture.cs
+++ /dev/null
@@ -1,189 +0,0 @@
-// ------------------------------------------------------------
-// Copyright (c) Microsoft Corporation. All rights reserved.
-// Licensed under the MIT License (MIT). See License.txt in the repo root for license information.
-// ------------------------------------------------------------
-
-namespace Azure.IIoT.OpcUa.Publisher.Stack.Services
-{
- using Microsoft.Extensions.Logging;
- using Microsoft.Extensions.Options;
- using System;
-#if SHARPPCAP
- using SharpPcap;
- using SharpPcap.LibPcap;
-#endif
- using System.Collections.Generic;
- using Autofac;
-
- ///
- /// Opc Ua traffic capture capabilities
- ///
- public sealed class OpcUaClientCapture : IStartable, IDisposable
- {
- ///
- /// Get the devices that can be used to capture
- ///
- public static IReadOnlyList AvailableDevices
- {
- get
- {
-#if SHARPPCAP
- try
- {
- return LibPcapLiveDeviceList.Instance
- .Select(d => d.Interface.FriendlyName ?? d.Name)
- .ToArray();
- }
- catch
-#endif
- {
- return Array.Empty();
- }
- }
- }
-
- ///
- /// Create capture service
- ///
- ///
- ///
- public OpcUaClientCapture(IOptions options,
- ILogger logger)
- {
- _options = options;
- _logger = logger;
- }
-
- ///
- public void Start()
- {
- // Find device
- var deviceName = _options.Value.CaptureDevice;
- if (string.IsNullOrEmpty(deviceName))
- {
- return;
- }
-
-#if SHARPPCAP
- _logger.LogInformation("Using SharpPcap {Version}", Pcap.SharpPcapVersion);
- var device = FindDeviceByName(deviceName);
- if (device == null)
- {
- _logger.LogError("Could not find a capture device with name {Name}! " +
- "Not capturing traffic...", deviceName);
- return;
- }
-
- _device?.Dispose();
- _device = new CaptureDevice(this, device, _options.Value.CaptureFileName);
-#else
- _logger.LogWarning("SharpPcap is not included in this build.");
-#endif
- }
-
- ///
- public void Dispose()
- {
-#if SHARPPCAP
- _device?.Dispose();
- _device = null;
-#endif
- }
-
-#if SHARPPCAP
- ///
- /// Find device by name
- ///
- ///
- ///
- private static LibPcapLiveDevice? FindDeviceByName(string deviceName)
- {
- var device = LibPcapLiveDeviceList.Instance
- .FirstOrDefault(d => d.Interface.FriendlyName == deviceName);
- if (device == null)
- {
- device = LibPcapLiveDeviceList.Instance
- .FirstOrDefault(d => d.Name == deviceName);
- if (device == null && deviceName == "loopback")
- {
- device = LibPcapLiveDeviceList.Instance
- .FirstOrDefault(d => d.Loopback);
- }
- }
- return device;
- }
-
- ///
- /// Capture device
- ///
- private sealed class CaptureDevice : IDisposable
- {
- ///
- /// Create capture device
- ///
- ///
- ///
- ///
- public CaptureDevice(OpcUaClientCapture outer,
- LibPcapLiveDevice device, string? fileName = null)
- {
- _outer = outer;
- _device = device;
-
- if (string.IsNullOrEmpty(fileName))
- {
- fileName = "opcua.pcap";
- }
-
- if (File.Exists(fileName))
- {
- File.Delete(fileName);
- }
-
- _outer._logger.LogInformation(
- "Start capturing {Device} ({Description}) to {FileName}.",
- device.Name, device.Description, fileName);
-
- // Open the device for capturing
- _device.Open(mode: DeviceModes.NoCaptureLocal, 1000);
- // _device.Filter = "ip and tcp and not port 80 and not port 25";
-
- _writer = new CaptureFileWriterDevice(fileName);
- _writer.Open(_device);
- _device.OnPacketArrival += (_, e) => _writer.Write(e.GetPacket());
-
- // Start the capturing process
- _device.StartCapture();
- }
-
- ///
- public void Dispose()
- {
- try
- {
- _device.StopCapture();
-
- _outer._logger.LogInformation(
- "Stopped capturing {Device} ({Description}) to file ({Statistics}).",
- _device.Name, _device.Description, _device.Statistics.ToString());
-
- _writer.Close();
- }
- finally
- {
- _writer.Dispose();
- _device.Dispose();
- }
- }
-
- private readonly LibPcapLiveDevice _device;
- private readonly CaptureFileWriterDevice _writer;
- private readonly OpcUaClientCapture _outer;
- }
-
- private CaptureDevice? _device;
-#endif
- private readonly IOptions _options;
- private readonly ILogger _logger;
- }
-}
diff --git a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaSubscription.cs b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaSubscription.cs
index 47b0c4d8c9..0d906b0528 100644
--- a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaSubscription.cs
+++ b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaSubscription.cs
@@ -46,6 +46,10 @@ internal sealed class OpcUaSubscription : Subscription, ISubscriptionHandle,
///
public ushort LocalIndex { get; }
+ ///
+ public IOpcUaClientDiagnostics State
+ => (_client as IOpcUaClientDiagnostics) ?? OpcUaClient.Disconnected;
+
///
/// Current metadata
///
@@ -58,12 +62,6 @@ internal sealed class OpcUaSubscription : Subscription, ISubscriptionHandle,
internal bool IsOnline
=> Handle != null && Session?.Connected == true && !_closed;
- ///
- /// Client state
- ///
- internal IOpcUaClientDiagnostics State
- => (_client as IOpcUaClientDiagnostics) ?? OpcUaClient.Disconnected;
-
///
/// Subscription
///
@@ -96,6 +94,7 @@ internal OpcUaSubscription(IClientAccessor clients,
InitializeMetrics();
TriggerManageSubscription(true);
+ Debug.Assert(_client != null);
}
///
@@ -240,10 +239,8 @@ public void Update(SubscriptionModel subscription)
_forceRecreate = true;
// ... release client handle to cause closing of session if last reference.
- var client = _client;
+ _client?.Dispose();
_client = null;
-
- client?.Dispose();
}
TriggerManageSubscription(true);
@@ -263,31 +260,40 @@ public void Close()
Debug.Assert(!_closed);
_closed = true;
+
TriggerManageSubscription(true);
}
}
+ ///
+ public async ValueTask CloseInSessionAsync(ISession? session, CancellationToken ct)
+ {
+ Debug.Assert(_closed);
+
+ // Finalize closing the subscription
+ ResetKeepAliveTimer();
+
+ _callbacks.OnSubscriptionUpdated(null);
+
+ // Does not throw
+ await CloseCurrentSubscriptionAsync().ConfigureAwait(false);
+
+ lock (_lock)
+ {
+ _client?.Dispose();
+ _client = null;
+ }
+ }
+
///
public async ValueTask SyncWithSessionAsync(ISession session, CancellationToken ct)
{
- if (_disposed)
+ if (_disposed || _closed)
{
return;
}
try
{
- if (_closed) // Finalize closing the subscription
- {
- _callbacks.OnSubscriptionUpdated(null);
-
- // Does not throw
- await CloseCurrentSubscriptionAsync().ConfigureAwait(false);
-
- _client?.Dispose();
- _client = null;
- return;
- }
-
await SyncWithSessionInternalAsync(session, ct).ConfigureAwait(false);
}
catch (Exception e)
@@ -314,6 +320,11 @@ protected override void Dispose(bool disposing)
if (!_disposed)
{
_disposed = true;
+ if (_closed)
+ {
+ _client?.Dispose();
+ _client = null;
+ }
FastDataChangeCallback = null;
FastEventCallback = null;
@@ -1233,7 +1244,10 @@ private void TriggerSubscriptionManagementCallbackIn(TimeSpan? delay,
///
private void OnSubscriptionManagementTriggered(object? state)
{
- TriggerManageSubscription(false);
+ lock (_lock)
+ {
+ TriggerManageSubscription(false);
+ }
}
///
@@ -1263,7 +1277,7 @@ private void TriggerManageSubscription(bool ensureClientExists)
_logger.LogInformation("Trigger management of subscription {Subscription}...",
this);
- _client.ManageSubscription(this);
+ _client.ManageSubscription(this, _closed);
}
///
@@ -2136,15 +2150,6 @@ public void InitializeMetrics()
_meter.CreateObservableUpDownCounter("iiot_edge_publisher_monitored_items",
() => new Measurement(_currentlyMonitored.Count,
_metrics.TagList), "Monitored items", "Monitored item count.");
- _meter.CreateObservableUpDownCounter("iiot_edge_publisher_connection_retries",
- () => new Measurement(State.ReconnectCount,
- _metrics.TagList), "Attempts", "OPC UA connect retries.");
- _meter.CreateObservableGauge("iiot_edge_publisher_is_connection_ok",
- () => new Measurement(State.State == EndpointConnectivityState.Ready ? 1 : 0,
- _metrics.TagList), "Online", "OPC UA connection success flag.");
- _meter.CreateObservableGauge("iiot_edge_publisher_is_disconnected",
- () => new Measurement(State.State != EndpointConnectivityState.Ready ? 1 : 0,
- _metrics.TagList), "Online", "OPC UA connection success flag.");
_meter.CreateObservableUpDownCounter("iiot_edge_publisher_publish_requests_per_subscription",
() => new Measurement(Ratio(State.OutstandingRequestCount, State.SubscriptionCount),
_metrics.TagList), "Requests per Subscription", "Good publish requests per subsciption.");
@@ -2164,9 +2169,7 @@ public void InitializeMetrics()
private static readonly TimeSpan kDefaultErrorRetryDelay = TimeSpan.FromSeconds(2);
private FrozenDictionary _currentlyMonitored;
private SubscriptionModel _template;
-#pragma warning disable CA2213 // Disposable fields should be disposed
private IOpcUaClient? _client;
-#pragma warning restore CA2213 // Disposable fields should be disposed
private uint _previousSequenceNumber;
private bool _useDeferredAcknoledge;
private uint _sequenceNumber;
diff --git a/src/Azure.IIoT.OpcUa.Publisher/tests/Azure.IIoT.OpcUa.Publisher.Tests.csproj b/src/Azure.IIoT.OpcUa.Publisher/tests/Azure.IIoT.OpcUa.Publisher.Tests.csproj
index 5368644c1c..2b616c8815 100644
--- a/src/Azure.IIoT.OpcUa.Publisher/tests/Azure.IIoT.OpcUa.Publisher.Tests.csproj
+++ b/src/Azure.IIoT.OpcUa.Publisher/tests/Azure.IIoT.OpcUa.Publisher.Tests.csproj
@@ -12,7 +12,7 @@
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
all
runtime; build; native; contentfiles; analyzers
diff --git a/src/Azure.IIoT.OpcUa/tests/Azure.IIoT.OpcUa.Tests.csproj b/src/Azure.IIoT.OpcUa/tests/Azure.IIoT.OpcUa.Tests.csproj
index 807b8fac37..f4d29e8929 100644
--- a/src/Azure.IIoT.OpcUa/tests/Azure.IIoT.OpcUa.Tests.csproj
+++ b/src/Azure.IIoT.OpcUa/tests/Azure.IIoT.OpcUa.Tests.csproj
@@ -9,7 +9,7 @@
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
all
runtime; build; native; contentfiles; analyzers