@@ -102,6 +102,14 @@ public partial class RemoteCollectorService
102102 private int _logInsertFailures ;
103103 private string ? _lastLogInsertError ;
104104
105+ /// <summary>
106+ /// Per-server flag indicating that master DB enumeration has failed with an access-denied
107+ /// error and should not be retried. Used on Azure SQL DB where per-database logins may not
108+ /// have master access (e.g. Microsoft Dynamics 365 FO). See issue #857.
109+ /// </summary>
110+ private readonly Dictionary < int , bool > _azureMasterInaccessible = new ( ) ;
111+ private readonly object _azureMasterInaccessibleLock = new ( ) ;
112+
105113 public RemoteCollectorService (
106114 DuckDbInitializer duckDb ,
107115 ServerManager serverManager ,
@@ -561,27 +569,94 @@ INSERT INTO collection_log (log_id, server_id, server_name, collector_name, coll
561569 /// Enumerates online databases on an Azure SQL DB logical server.
562570 /// HAS_DBACCESS() returns false for user databases from master on Azure SQL DB,
563571 /// so we skip that filter — inaccessible databases should be handled by callers via try/catch.
572+ ///
573+ /// On Azure SQL DB, logins are sometimes granted access only to a specific user database and
574+ /// not to master (e.g. Microsoft Dynamics 365 FO). In that case, master enumeration fails with
575+ /// an access/login error; we fall back to returning the connection's initial catalog as a
576+ /// single-database list, and cache that decision per server so we don't retry master each cycle.
577+ /// See issue #857.
564578 /// </summary>
565579 protected async Task < List < string > > GetAzureDatabaseListAsync ( ServerConnection server , CancellationToken cancellationToken )
566580 {
581+ var serverId = GetServerId ( server ) ;
567582 var baseConnStr = server . GetConnectionString ( _serverManager . CredentialService ) ;
583+ var targetDb = new SqlConnectionStringBuilder ( baseConnStr ) . InitialCatalog ;
584+
585+ bool knownInaccessible ;
586+ lock ( _azureMasterInaccessibleLock )
587+ {
588+ _azureMasterInaccessible . TryGetValue ( serverId , out knownInaccessible ) ;
589+ }
590+
591+ if ( knownInaccessible )
592+ {
593+ return SingleDbOrEmpty ( targetDb ) ;
594+ }
595+
568596 var connStr = new SqlConnectionStringBuilder ( baseConnStr )
569597 {
570598 ConnectTimeout = ConnectionTimeoutSeconds ,
571599 InitialCatalog = "master"
572600 } . ConnectionString ;
573601
574602 var databases = new List < string > ( ) ;
575- using var conn = new SqlConnection ( connStr ) ;
576- await conn . OpenAsync ( cancellationToken ) ;
577- using var cmd = new SqlCommand (
578- "SELECT name FROM sys.databases WHERE state_desc = N'ONLINE' AND database_id > 0 ORDER BY name;" ,
579- conn )
580- { CommandTimeout = CommandTimeoutSeconds } ;
581- using var reader = await cmd . ExecuteReaderAsync ( cancellationToken ) ;
582- while ( await reader . ReadAsync ( cancellationToken ) )
583- databases . Add ( reader . GetString ( 0 ) ) ;
584- return databases ;
603+ try
604+ {
605+ using var conn = new SqlConnection ( connStr ) ;
606+ await conn . OpenAsync ( cancellationToken ) ;
607+ using var cmd = new SqlCommand (
608+ "SELECT name FROM sys.databases WHERE state_desc = N'ONLINE' AND database_id > 0 ORDER BY name;" ,
609+ conn )
610+ { CommandTimeout = CommandTimeoutSeconds } ;
611+ using var reader = await cmd . ExecuteReaderAsync ( cancellationToken ) ;
612+ while ( await reader . ReadAsync ( cancellationToken ) )
613+ databases . Add ( reader . GetString ( 0 ) ) ;
614+ return databases ;
615+ }
616+ catch ( SqlException ex ) when ( IsMasterAccessDeniedError ( ex ) )
617+ {
618+ lock ( _azureMasterInaccessibleLock )
619+ {
620+ _azureMasterInaccessible [ serverId ] = true ;
621+ }
622+
623+ var fallback = SingleDbOrEmpty ( targetDb ) ;
624+ if ( fallback . Count > 0 )
625+ {
626+ AppLogger . Info ( "Collector" , $ " [{ server . DisplayName } ] master DB inaccessible (SQL error { ex . Number } ) — collecting from '{ targetDb } ' only.") ;
627+ }
628+ else
629+ {
630+ AppLogger . Warn ( "Collector" , $ " [{ server . DisplayName } ] master DB inaccessible (SQL error { ex . Number } ) and no target database in connection string — no data will be collected for database-scoped collectors.") ;
631+ }
632+ return fallback ;
633+ }
634+ }
635+
636+ private static List < string > SingleDbOrEmpty ( string ? targetDb )
637+ {
638+ if ( string . IsNullOrEmpty ( targetDb ) || string . Equals ( targetDb , "master" , StringComparison . OrdinalIgnoreCase ) )
639+ return new List < string > ( ) ;
640+ return new List < string > { targetDb } ;
641+ }
642+
643+ /// <summary>
644+ /// Error numbers indicating the login cannot open or read from master on Azure SQL DB.
645+ /// Trigger a fallback to single-database mode when we see one of these.
646+ /// </summary>
647+ private static bool IsMasterAccessDeniedError ( SqlException ex )
648+ {
649+ return ex . Number switch
650+ {
651+ 229 => true , // Permission denied on object
652+ 230 => true , // Permission denied on column
653+ 916 => true , // Server principal is not able to access the database under the current security context
654+ 4060 => true , // Cannot open database requested by the login
655+ 18456 => true , // Login failed for user
656+ 40613 => true , // Database 'master' on server is not currently available
657+ 40615 => true , // Cannot open server — login denied (firewall/auth)
658+ _ => false
659+ } ;
585660 }
586661
587662 /// <summary>
0 commit comments