Enhance logging in FdsSqlOptions and related classes

- Updated FdsSqlOptions to accept an optional ILogger parameter for improved error logging.
- Modified FdsMfr and FdsMfrClient classes to pass the logger instance to FdsSqlOptions.
- Added detailed error logging in various methods to capture SQL execution issues and file handling errors.
- Improved documentation for FdsSqlOptions to clarify logging behavior.
- Updated Archive class to log compression errors, enhancing traceability of failures.
- Adjusted project configuration to suppress specific warnings related to transitive dependencies.
- Added NuGet.config to define package sources for dependency management.
- Updated submodule references for OCORE and related projects.
This commit is contained in:
Stefan
2026-07-03 20:22:05 +02:00
parent 1a3bf30442
commit 882e97509a
57 changed files with 2121 additions and 106 deletions
+42 -14
View File
@@ -13,15 +13,26 @@ using static OCORE.commons;
namespace fds;
/// <summary>
/// Logs every SQL error both to the worker's structured <see cref="ILogger"/> (so it reaches
/// whatever sink/OTel export is configured) and to <see cref="FdsDebug.DebugLog"/> (SQL/local-file
/// debug trail). Passing <paramref name="logger"/> is optional for call sites that don't have one
/// in scope, but every caller that does have one in scope should pass it — otherwise a failing
/// stored procedure only ever shows up in the debug trail, never in the worker's own logs.
/// </summary>
public class FdsSqlOptions : sqloptions
{
private Dictionary<string, object>? _baseInfo;
public FdsSqlOptions(Dictionary<string, object>? dic = null)
public FdsSqlOptions(Dictionary<string, object>? dic = null, ILogger? logger = null)
{
_baseInfo = dic;
OnError = (procedure, ex, data) =>
{
logger?.LogError(ex, "SQL error in {Procedure}: {Message} — ctrl_nfo={CtrlInfo} sql={Data}",
procedure, ex.Message, dic, data);
FdsDebug.DebugLog(procedure, exc: ex, data: $"ctrl_nfo={dic}, sql={data}");
};
}
}
@@ -88,7 +99,7 @@ public class FdsMfr : IFdsMfr
if (debugDetails) FdsDebug.DebugToFile("GetInvoiceFiles_async - start", filename: "DebugDetail.txt");
var dtbl = await getSQLDatatable_async(
"EXECUTE [dbo].[fds__fn_getMFRInvoicesWithoutfiles];",
FdsShared.FDSConnectionString(), SqlParameterList: null, options: new FdsSqlOptions());
FdsShared.FDSConnectionString(), SqlParameterList: null, options: new FdsSqlOptions(logger: _logger));
if (dtbl.Count > 0)
{
var rows = dtbl.DataTable.Rows.Cast<DataRow>()
@@ -115,7 +126,7 @@ public class FdsMfr : IFdsMfr
SQL_VarChar("@Id", r.id),
SQL_VarChar("@filename", r.docName),
new SqlParameter("@file", fl) { SqlDbType = SqlDbType.VarBinary }),
options: new FdsSqlOptions());
options: new FdsSqlOptions(logger: _logger));
Interlocked.Increment(ref downloaded);
}
}
@@ -142,7 +153,7 @@ public class FdsMfr : IFdsMfr
var pl = new List<SqlParameter> { SQL_VarChar("@reportid", reportid) };
var sqldt = Task.Run(async () => await getSQLDatatable_async(
"EXECUTE [dbo].[fds__getReportDocument] @reportid;",
FdsShared.FDSConnectionString(), SqlParameterList: pl, options: new FdsSqlOptions())).Result;
FdsShared.FDSConnectionString(), SqlParameterList: pl, options: new FdsSqlOptions(logger: _logger))).Result;
if (sqldt.Count > 0)
{
@@ -166,7 +177,7 @@ public class FdsMfr : IFdsMfr
SQL_VarChar("@Id", reportid),
SQL_VarChar("@filename", fln),
new SqlParameter("@file", fl) { SqlDbType = SqlDbType.VarBinary }),
options: new FdsSqlOptions())).Wait();
options: new FdsSqlOptions(logger: _logger))).Wait();
}
catch (Exception fsex) { FdsDebug.DebugLog("getReportDoc - mfr storefile", exc: fsex); }
}
@@ -197,7 +208,7 @@ public class FdsMfr : IFdsMfr
var pl = new List<SqlParameter> { SQL_VarChar("@type", type), SQL_VarChar("@reportid", reportid) };
var sqldt = Task.Run(async () => await getSQLDatatable_async(
"EXECUTE [dbo].[fds__getFDSDocument] @type, @reportid;",
FdsShared.FDSConnectionString(), SqlParameterList: pl, options: new FdsSqlOptions())).Result;
FdsShared.FDSConnectionString(), SqlParameterList: pl, options: new FdsSqlOptions(logger: _logger))).Result;
if (sqldt.Count > 0)
{
@@ -236,14 +247,14 @@ public class FdsMfr : IFdsMfr
SQL_Bit("@files", includeFiles), SQL_VarChar("@authuser", authUser)
},
tablenames: new[] { "admin", "inv", "buchungen", "debitoren" },
options: new FdsSqlOptions())).Result;
options: new FdsSqlOptions(logger: _logger))).Result;
var bediFiles = new List<DatevDocument>();
if (dset.Count >= 4)
{
var admin = dset.Tables("admin").Rows[0].toObjectDictionary();
DateTime startdate = (DateTime)admin["startdate"], enddate = (DateTime)admin["enddate"];
DateTime startdate = (DateTime)admin["startdate"]!, enddate = (DateTime)admin["enddate"]!;
string register = tgtdate.ToString("yyyy\\/MM") + (mode.ToLower() == "w" ? "_w" + tgtdate.ToString("dd") : "");
var fls = new Dictionary<string, byte[]>();
@@ -251,10 +262,10 @@ public class FdsMfr : IFdsMfr
{
Formatkategorie = DatevFormatkategorie.Buchungsstapel,
Formatversion = DatevFormatversion.Buchungsstapel_9,
Beraternummer = (int)admin["beraternummer"],
Mandantennummer = (int)admin["mandantennummer"],
WJBeginn = (DateTime)admin["WJ-Beginn"],
Sachkontenlänge = (int)admin["Sachkontenlänge"],
Beraternummer = (int)admin["beraternummer"]!,
Mandantennummer = (int)admin["mandantennummer"]!,
WJBeginn = (DateTime)admin["WJ-Beginn"]!,
Sachkontenlänge = (int)admin["Sachkontenlänge"]!,
DatumVon = startdate,
DatumBis = enddate,
Bezeichnung = "fds_" + mode + tgtdate.ToString("yyMMdd")
@@ -284,7 +295,14 @@ public class FdsMfr : IFdsMfr
else
fl = irow.no("file", null) as byte[];
}
catch { }
catch (Exception ex)
{
// Silently omitting this file from the DATEV export previously left no
// trace at all — the export "succeeded" while quietly missing a document.
_logger.LogWarning(ex,
"getDatevZip: failed to fetch invoice file — invoiceId={InvoiceId} fileName={FileName}",
irow.nz("InvoiceId"), fln);
}
if (fl != null && !string.IsNullOrEmpty(fln))
{
fls.Add(fln, fl);
@@ -308,8 +326,18 @@ public class FdsMfr : IFdsMfr
stream!.Position = 0;
return archiveFile;
}
_logger.LogError(
"getDatevZip: CompressToStream returned false — archiveFile={ArchiveFile} fileCount={FileCount}",
archiveFile.FullName, fls.Count);
}
catch (Exception ex)
{
// Previously swallowed entirely: a DATEV export could fail at the archive
// step with zero trace anywhere, not even the DB debug log.
_logger.LogError(ex,
"getDatevZip: archive creation failed — archiveFile={ArchiveFile} fileCount={FileCount}",
archiveFile.FullName, fls.Count);
}
catch { }
}
}
}
+7 -7
View File
@@ -64,7 +64,7 @@ public class FdsMfrClient : IDisposable
public Dictionary<string, Dictionary<string, string>> NavProperties { get; } = new();
private readonly DataSet _tableSet;
public DatabaseSchema(EntityTypes et)
public DatabaseSchema(EntityTypes et, ILogger? logger = null)
{
_et = et;
var dset = Task.Run(async () => await getSQLDataSet_async(
@@ -72,7 +72,7 @@ public class FdsMfrClient : IDisposable
FdsShared.FDSConnectionString(),
SqlParameterList: new List<SqlParameter> { new("@tgttype", ThisEntityName) },
tablenames: new[] { "entity", "complex_types", "navigation_properties", "tables" },
options: new FdsSqlOptions())).Result;
options: new FdsSqlOptions(logger: logger))).Result;
IsValid = dset.Count > 0;
HasEntity = dset.Tables("entity").Rows.Count == 1;
@@ -98,7 +98,7 @@ public class FdsMfrClient : IDisposable
string.Join(Environment.NewLine, sqlList),
FdsShared.FDSConnectionString(),
tablenames: tableNames.ToArray(),
options: new FdsSqlOptions())).Result.DataSet;
options: new FdsSqlOptions(logger: logger))).Result.DataSet;
}
public DataSet TgtDataset(string setId)
@@ -147,7 +147,7 @@ public class FdsMfrClient : IDisposable
{
try
{
var schema = schemaDic != null && schemaDic.TryGetValue(thisEntityName, out var s) ? s : new DatabaseSchema(et);
var schema = schemaDic != null && schemaDic.TryGetValue(thisEntityName, out var s) ? s : new DatabaseSchema(et, _logger);
if (!schema.IsValid)
{
dlg("Schema not found", "", "");
@@ -179,7 +179,7 @@ public class FdsMfrClient : IDisposable
{
var lastDate = await getSQLValue_async(
schema.EntityConfig.nz("DateSQL").ne($"SELECT MAX([{schema.EntityConfig["DateColumn"]}]) FROM [dbo].[{schema.EntityTableName}];"),
FdsShared.FDSConnectionString(), options: new FdsSqlOptions());
FdsShared.FDSConnectionString(), options: new FdsSqlOptions(logger: _logger));
filter.Add(
updateNeed == FdsMfr.UpdateNeed.Short && lastDate.Result is DateTime dt
? $"$filter={schema.EntityConfig["DateColumn"]} gt DateTime'{dt:yyyy-MM-dd}T00:00:00'"
@@ -395,7 +395,7 @@ public class FdsMfrClient : IDisposable
{
string sql = "SELECT * FROM [dbo].[fds__getUpdateableTables]()"
+ (tgtEntityType.HasValue ? $" WHERE [entity_name] = '{tgtEntityType.Value}'" : "") + ";";
var updateableTables = await getSQLDatatable_async(sql, FdsShared.FDSConnectionString(), options: new FdsSqlOptions());
var updateableTables = await getSQLDatatable_async(sql, FdsShared.FDSConnectionString(), options: new FdsSqlOptions(logger: _logger));
dtf("UpdateableTables", updateableTables.Exception ?? "", $"({(updateableTables.Count > 0 ? updateableTables.DataTable.Rows.Count.ToString() : " no")} Rows)", null);
if (updateableTables.Count > 0 && updateableTables.DataTable.Columns.Contains("updateneed"))
@@ -446,7 +446,7 @@ public class FdsMfrClient : IDisposable
{
var updateableRequests = await getSQLDatatable_async(
"SELECT * FROM [dbo].[fds__getUpdateableRequests]();",
FdsShared.FDSConnectionString(), options: new FdsSqlOptions());
FdsShared.FDSConnectionString(), options: new FdsSqlOptions(logger: _logger));
dtf("UpdateableRequests", updateableRequests.Exception ?? "", $"({(updateableRequests.Count > 0 ? updateableRequests.DataTable.Rows.Count.ToString() : " no")} Rows)", null);
if (updateableRequests.Count > 0)
+1 -1
View File
@@ -9,7 +9,7 @@ namespace fds;
/// <summary>
/// Holds the application <see cref="IConfiguration"/> built from appsettings.json.
/// Call <see cref="Initialize"/> once at startup before accessing <see cref="Current"/>.
/// Call <see cref="Initialize()"/> once at startup before accessing <see cref="Current"/>.
/// </summary>
public static class FdsConfig
{
+9 -3
View File
@@ -5,7 +5,6 @@ namespace fds;
public class Archive : IDisposable
{
public event Action? Saving;
public event Action? FileSaved;
public event Action? FileStreamCreated;
@@ -140,8 +139,12 @@ public class Archive : IDisposable
_zipIn = null;
ZipInOK = false;
}
catch
catch (Exception ex)
{
// Previously silent: callers (e.g. HandleDatevZip) saw only a bare failed result
// with no way to tell a compression error from any other reason ExitOK is false.
_logger.LogError(ex, "Archive.Compress failed — archiveFile={ArchiveFile} fileCount={FileCount}",
archiveFile.FullName, files.Count);
ExitOK = false;
}
archiveFile.Refresh();
@@ -179,8 +182,11 @@ public class Archive : IDisposable
_zipIn = null;
ZipInOK = false;
}
catch
catch (Exception ex)
{
// Previously silent: the DATEV export ZIP-to-stream path failed with no
// trace anywhere, only a bare false return.
_logger.LogError(ex, "Archive.CompressToStream failed — fileCount={FileCount}", files.Count);
ExitOK = false;
}
return ExitOK;
+4 -2
View File
@@ -10,11 +10,13 @@
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<PlatformTarget>x64</PlatformTarget>
<DocumentationFile>Fuchs_DataService.xml</DocumentationFile>
<NoWarn>1591</NoWarn>
<!-- NU1608: transitive AngleSharp.Css/AngleSharp conflict inherited from OCORE_web -->
<NoWarn>1591;NU1608</NoWarn>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DocumentationFile>Fuchs_DataService.xml</DocumentationFile>
<NoWarn>1591</NoWarn>
<!-- NU1608: transitive AngleSharp.Css/AngleSharp conflict inherited from OCORE_web -->
<NoWarn>1591;NU1608</NoWarn>
</PropertyGroup>
<ItemGroup>
<Content Include="appsettings.json">
+9
View File
@@ -4,6 +4,15 @@
<name>Fuchs_DataService</name>
</assembly>
<members>
<member name="T:fds.FdsSqlOptions">
<summary>
Logs every SQL error both to the worker's structured <see cref="T:Microsoft.Extensions.Logging.ILogger"/> (so it reaches
whatever sink/OTel export is configured) and to <see cref="M:fds.FdsDebug.DebugLog(System.String,Microsoft.Data.SqlClient.SqlConnection,System.Exception,System.String,System.Object)"/> (SQL/local-file
debug trail). Passing <paramref name="logger"/> is optional for call sites that don't have one
in scope, but every caller that does have one in scope should pass it — otherwise a failing
stored procedure only ever shows up in the debug trail, never in the worker's own logs.
</summary>
</member>
<member name="F:fds.FdsMfr.InvoiceFileDownloadConcurrency">
<summary>Max parallel invoice-file downloads (independent per file).</summary>
</member>