Repository: syslog-ng/syslog-ng
Pull request #5338: Fix wildcard file regression
Author: login: HofiOne
Created: 2025-04-25T17:02:40Z | Merged: 2025-05-19T09:07:33Z

This PR 

- fixes issues of the new inotify-driven `file()` and `wildcard-file()` monitoring, even though inotify sends file change notifications correctly, but not fully processed changes (e.g. when there are more changes than a fetch cycle (fetch-limit) can handle) will not be notified again till there are no new changes that could lead to delayed or lost change processing
- adds `follow-method()` option to `file()` and `wildcard-file()` sources

Doc PR: https://github.com/syslog-ng/syslog-ng.github.io/pull/192

Signed-off-by: Hofi hofione@gmail.com


Diff:
diff --git a/lib/Makefile.am b/lib/Makefile.am
index ff63cc7d732..75339e79cc5 100644
--- a/lib/Makefile.am
+++ b/lib/Makefile.am
@@ -153,6 +153,7 @@ pkginclude_HEADERS			+= \
 	lib/ml-batched-timer.h		\
 	lib/msg-format.h		\
 	lib/msg-stats.h			\
+	lib/notified-fd-events.h		\
 	lib/on-error.h			\
 	lib/parse-number.h		\
 	lib/pathutils.h         \
diff --git a/lib/logpipe.h b/lib/logpipe.h
index 79e865c474f..35ac0d24e33 100644
--- a/lib/logpipe.h
+++ b/lib/logpipe.h
@@ -33,14 +33,15 @@
 #include "messages.h"
 
 /* notify code values */
-#define NC_CLOSE       1
-#define NC_READ_ERROR  2
-#define NC_WRITE_ERROR 3
-#define NC_FILE_MOVED  4
-#define NC_FILE_EOF    5
-#define NC_REOPEN_REQUIRED 6
-#define NC_FILE_DELETED 7
-#define NC_FILE_MODIFIED 8
+#define NC_CLOSE            1
+#define NC_READ_ERROR       2
+#define NC_WRITE_ERROR      3
+#define NC_FILE_MOVED       4
+#define NC_FILE_EOF         5
+#define NC_REOPEN_REQUIRED  6
+#define NC_FILE_DELETED     7
+#define NC_FILE_MODIFIED    8
+#define NC_AGAIN            9
 
 /* notify result mask values */
 #define NR_OK          0x0000
diff --git a/lib/logproto/logproto-text-server.c b/lib/logproto/logproto-text-server.c
index e2eaf592344..b00a37851ff 100644
--- a/lib/logproto/logproto-text-server.c
+++ b/lib/logproto/logproto-text-server.c
@@ -30,13 +30,12 @@ LogProtoPrepareAction
 log_proto_text_server_poll_prepare_method(LogProtoServer *s, GIOCondition *cond, gint *timeout)
 {
   LogProtoTextServer *self = (LogProtoTextServer *) s;
-  gboolean avail;
 
   LogProtoPrepareAction action = log_proto_buffered_server_poll_prepare(s, cond, timeout);
   if (action != LPPA_POLL_IO)
     return action;
 
-  avail = (self->cached_eol_pos != 0);
+  gboolean avail = (self->cached_eol_pos != 0);
   return avail ? LPPA_FORCE_SCHEDULE_FETCH : LPPA_POLL_IO;
 }
 
diff --git a/lib/logreader.c b/lib/logreader.c
index 30aae70b108..1303934220a 100644
--- a/lib/logreader.c
+++ b/lib/logreader.c
@@ -30,7 +30,7 @@
 #include "str-format.h"
 
 static void log_reader_io_handle_in(gpointer s);
-static gboolean log_reader_fetch_log(LogReader *self);
+static gint log_reader_fetch_log(LogReader *self);
 static void log_reader_update_watches(LogReader *self);
 
 /*****************************************************************************
@@ -398,7 +398,7 @@ log_reader_work_finished(void *s, gpointer arg)
       g_mutex_unlock(&self->pending_close_lock);
     }
 
-  if (self->notify_code)
+  if (self->notify_code && self->notify_code != NC_AGAIN)
     {
       gint notify_code = self->notify_code;
 
@@ -424,6 +424,11 @@ log_reader_work_finished(void *s, gpointer arg)
         }
       log_proto_server_reset_error(self->proto);
       log_reader_update_watches(self);
+
+      if (self->notify_code == NC_AGAIN && poll_events_system_notified(self->poll_events))
+        {
+          log_reader_force_check_in_next_poll(self);
+        }
     }
 }
 
@@ -547,41 +552,50 @@ log_reader_handle_line(LogReader *self, const guchar *line, gint length, LogTran
 static gint
 log_reader_fetch_log(LogReader *self)
 {
-  gint msg_count = 0;
-  gboolean may_read = TRUE;
   LogTransportAuxData aux_storage, *aux = &aux_storage;
-
   if ((self->options->flags & LR_IGNORE_AUX_DATA))
     aux = NULL;
-
   log_transport_aux_data_init(aux);
+
   if (self->handshake_in_progress)
     {
-      return log_reader_process_handshake(self);
+      gint result = log_reader_process_handshake(self);
+      /* TODO: usage of can_fetch_after_handshake might not needed at all here, just wanted to keep the original flow
+       *       to be surely backward compatible, my bet is that every successfull handshake can be followed by an
+       *       immediate fetch normally */
+      if (FALSE == self->can_fetch_after_handshake || result != 0 || self->handshake_in_progress)
+        {
+          log_transport_aux_data_destroy(aux);
+          return result;
+        }
     }
 
   /* NOTE: this loop is here to decrease the load on the main loop, we try
    * to fetch a couple of messages in a single run (but only up to
    * fetch_limit).
    */
-  while (msg_count < self->options->fetch_limit && !main_loop_worker_job_quit())
+  gint msg_count = 0;
+  gboolean may_read = TRUE;
+  gint result = 0;
+  while (!main_loop_worker_job_quit())
     {
-      Bookmark *bookmark;
-      const guchar *msg;
-      gsize msg_len;
-      LogProtoStatus status;
+      if (msg_count >= self->options->fetch_limit)
+        {
+          result = NC_AGAIN;
+          break;
+        }
 
-      msg = NULL;
+      log_transport_aux_data_reinit(aux);
+      Bookmark *bookmark = ack_tracker_request_bookmark(self->super.ack_tracker);
+      const guchar *msg = NULL;
+      gsize msg_len;
 
       /* NOTE: may_read is used to implement multi-read checking. It
        * is initialized to TRUE to indicate that the protocol is
        * allowed to issue a read(). If multi-read is disallowed in the
        * protocol, it resets may_read to FALSE after the first read was issued.
        */
-
-      log_transport_aux_data_reinit(aux);
-      bookmark = ack_tracker_request_bookmark(self->super.ack_tracker);
-      status = log_proto_server_fetch(self->proto, &msg, &msg_len, &may_read, aux, bookmark);
+      LogProtoStatus status = log_proto_server_fetch(self->proto, &msg, &msg_len, &may_read, aux, bookmark);
       switch (status)
         {
         case LPS_EOF:
@@ -617,7 +631,7 @@ log_reader_fetch_log(LogReader *self)
     }
   log_transport_aux_data_destroy(aux);
 
-  return 0;
+  return result;
 }
 
 static void
diff --git a/lib/logreader.h b/lib/logreader.h
index 61868794351..cd93e79d073 100644
--- a/lib/logreader.h
+++ b/lib/logreader.h
@@ -62,7 +62,6 @@ struct _LogReader
 {
   LogSource super;
   LogProtoServer *proto;
-  gboolean handshake_in_progress;
   LogPipe *control;
   LogReaderOptions *options;
   PollEvents *poll_events;
@@ -71,14 +70,14 @@ struct _LogReader
   StatsAggregator *max_message_size;
   StatsAggregator *average_messages_size;
   StatsAggregator *CPS;
-
-  /* NOTE: these used to be LogReaderWatch members, which were merged into
-   * LogReader with the multi-thread refactorization */
-
   struct iv_task restart_task;
   struct iv_event schedule_wakeup;
   MainLoopIOWorkerJob io_job;
-  guint watches_running:1, suspended:1, realloc_window_after_fetch:1;
+  guint watches_running:1,
+        suspended:1,
+        realloc_window_after_fetch:1,
+        handshake_in_progress:1,
+        can_fetch_after_handshake:1;
   gint notify_code;
 
   /* proto & poll_events pending to be applied. As long as the previous
diff --git a/lib/notified-fd-events.h b/lib/notified-fd-events.h
new file mode 100644
index 00000000000..9b00f5c67ea
--- /dev/null
+++ b/lib/notified-fd-events.h
@@ -0,0 +1,48 @@
+/*
+ * Copyright (c) 2025 One Identity LLC.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ * As an additional exemption you are allowed to compile & link against the
+ * OpenSSL libraries as published by the OpenSSL project. See the file
+ * COPYING for details.
+ *
+ */
+#ifndef NOTIFIED_FD_EVENTS_H_INCLUDED
+#define NOTIFIED_FD_EVENTS_H_INCLUDED
+
+#include "poll-fd-events.h"
+
+static inline void
+notified_fd_events_update_watches(PollEvents *s, GIOCondition cond)
+{
+  poll_events_check_watches(s);
+}
+
+static inline PollEvents *
+notified_fd_events_new(gint fd)
+{
+  PollFdEvents *self = (PollFdEvents *) poll_fd_events_new(fd);
+
+  self->super.type = FM_INOTIFY;
+  self->super.start_watches = NULL;
+  self->super.stop_watches = NULL;
+  self->super.suspend_watches = NULL;
+  self->super.update_watches = notified_fd_events_update_watches;
+
+  return &self->super;
+}
+
+#endif
diff --git a/lib/poll-events.h b/lib/poll-events.h
index 0ab30571eb8..eabf518d31d 100644
--- a/lib/poll-events.h
+++ b/lib/poll-events.h
@@ -26,13 +26,23 @@
 
 #include "syslog-ng.h"
 
+typedef enum
+{
+  FM_LEGACY,
+  FM_POLL,
+  FM_SYSTEM_POLL,
+  FM_INOTIFY,
+  FM_UNKNOWN
+} FollowMethod;
+
 typedef struct _PollEvents PollEvents;
 typedef void (*PollCallback)(gpointer user_data);
 typedef gboolean (*PollChecker)(PollEvents *self, gpointer user_data);
 
+/* TODO: Poll is just a legacy word here, rename to sg. more appropriate (e.g. FollowEvents) */
 struct _PollEvents
 {
-  gboolean system_polled;
+  FollowMethod type;
   PollCallback callback;
   gpointer callback_data;
   gpointer checker_data;
@@ -57,7 +67,13 @@ poll_events_get_fd(PollEvents *self)
 static inline gboolean
 poll_events_system_polled(PollEvents *self)
 {
-  return self->system_polled;
+  return self->type == FM_SYSTEM_POLL;
+}
+
+static inline gboolean
+poll_events_system_notified(PollEvents *self)
+{
+  return self->type == FM_INOTIFY;
 }
 
 static inline void
diff --git a/lib/poll-fd-events.c b/lib/poll-fd-events.c
index ed935703bac..be50599feeb 100644
--- a/lib/poll-fd-events.c
+++ b/lib/poll-fd-events.c
@@ -23,15 +23,6 @@
  */
 #include "poll-fd-events.h"
 
-#include <iv.h>
-
-typedef struct _PollFdEvents
-{
-  PollEvents super;
-  struct iv_fd fd_watch;
-  gboolean notified;
-} PollFdEvents;
-
 #define IV_FD_CALLBACK(x) ((void (*)(void *)) (x))
 
 static inline gint
@@ -76,7 +67,7 @@ poll_fd_events_update_watches(PollEvents *s, GIOCondition cond)
 
   poll_events_suspend_watches(s);
 
-  if (poll_events_check_watches(s) && FALSE == self->notified)
+  if (poll_events_check_watches(s) && FALSE == poll_events_system_notified(s))
     {
       if (cond & G_IO_IN)
         iv_fd_set_handler_in(&self->fd_watch, IV_FD_CALLBACK(poll_events_invoke_callback));
@@ -100,9 +91,8 @@ poll_fd_events_new(gint fd)
   self->super.stop_watches = poll_fd_events_stop_watches;
   self->super.update_watches = poll_fd_events_update_watches;
   self->super.suspend_watches = poll_fd_events_suspend_watches;
-  self->super.system_polled = TRUE;
+  self->super.type = FM_SYSTEM_POLL;
   self->super.get_fd = _get_fd;
-  self->notified = FALSE;
 
   IV_FD_INIT(&self->fd_watch);
   self->fd_watch.fd = fd;
@@ -110,16 +100,3 @@ poll_fd_events_new(gint fd)
 
   return &self->super;
 }
-
-PollEvents *
-notified_fd_events_new(gint fd)
-{
-  PollFdEvents *self = (PollFdEvents *)poll_fd_events_new(fd);
-
-  self->notified = TRUE;
-  self->super.start_watches = NULL;
-  self->super.stop_watches = NULL;
-  self->super.suspend_watches = NULL;
-
-  return &self->super;
-}
diff --git a/lib/poll-fd-events.h b/lib/poll-fd-events.h
index 36d3b2101ee..7c71e34fca9 100644
--- a/lib/poll-fd-events.h
+++ b/lib/poll-fd-events.h
@@ -25,8 +25,14 @@
 #define POLL_FD_EVENTS_H_INCLUDED
 
 #include "poll-events.h"
+#include <iv.h>
+
+typedef struct _PollFdEvents
+{
+  PollEvents super;
+  struct iv_fd fd_watch;
+} PollFdEvents;
 
 PollEvents *poll_fd_events_new(gint fd);
-PollEvents *notified_fd_events_new(gint fd);
 
 #endif
diff --git a/modules/affile/CMakeLists.txt b/modules/affile/CMakeLists.txt
index b77e7a25351..b034a36942b 100644
--- a/modules/affile/CMakeLists.txt
+++ b/modules/affile/CMakeLists.txt
@@ -1,4 +1,4 @@
-set (AFFILE_SOURCES
+set(AFFILE_SOURCES
   "affile-dest.h"
   "affile-dest-internal-queue-filter.h"
   "affile-parser.h"
@@ -10,6 +10,7 @@ set (AFFILE_SOURCES
   "directory-monitor-poll.h"
   "file-opener.h"
   "file-reader.h"
+  "file-monitor-factory.h"
   "file-specializations.h"
   "logproto-file-reader.h"
   "logproto-file-writer.h"
@@ -31,6 +32,7 @@ set (AFFILE_SOURCES
   "directory-monitor-factory.c"
   "directory-monitor-content-comparator.c"
   "directory-monitor-poll.c"
+  "file-monitor-factory.c"
   "file-opener.c"
   "file-reader.c"
   "linux-kmsg.c"
diff --git a/modules/affile/Makefile.am b/modules/affile/Makefile.am
index 8d93f3d130a..3bc00e88c30 100644
--- a/modules/affile/Makefile.am
+++ b/modules/affile/Makefile.am
@@ -12,6 +12,8 @@ modules_affile_libaffile_la_SOURCES	=			\
 	modules/affile/transport-prockmsg.h			\
 	modules/affile/file-reader.c				\
 	modules/affile/file-reader.h				\
+	modules/affile/file-monitor-factory.h			\
+	modules/affile/file-monitor-factory.c			\
 	modules/affile/wildcard-file-reader.c		\
 	modules/affile/wildcard-file-reader.h		\
 	modules/affile/wildcard-source.h			\
diff --git a/modules/affile/affile-grammar.ym b/modules/affile/affile-grammar.ym
index d2e4425f7b9..86743cd15d5 100644
--- a/modules/affile/affile-grammar.ym
+++ b/modules/affile/affile-grammar.ym
@@ -89,6 +89,7 @@ affile_grammar_set_wildcard_file_source_driver(WildcardSourceDriver *sd)
 
 %token KW_FSYNC
 %token KW_FOLLOW_FREQ
+%token KW_FOLLOW_METHOD
 %token KW_MONITOR_FREQ
 %token KW_OVERWRITE_IF_OLDER
 %token KW_SYMLINK_AS
@@ -106,88 +107,89 @@ affile_grammar_set_wildcard_file_source_driver(WildcardSourceDriver *sd)
 %token KW_STDIN
 %token KW_STDOUT
 
-%type	<ptr> source_affile
-%type	<ptr> source_affile_params
-%type	<ptr> source_afpipe_params
-%type	<ptr> source_stdin_params
-%type   <ptr> dest_affile
-%type	<ptr> dest_affile_params
-%type   <ptr> dest_afpipe_params
-%type   <ptr> dest_stdout_params
+%type  <ptr> source_affile
+%type  <ptr> source_affile_params
+%type  <ptr> source_afpipe_params
+%type  <ptr> source_stdin_params
+%type  <ptr> dest_affile
+%type  <ptr> dest_affile_params
+%type  <ptr> dest_afpipe_params
+%type  <ptr> dest_stdout_params
 
 %%
 
 start
-        : LL_CONTEXT_SOURCE source_affile                        { YYACCEPT; }
-	| LL_CONTEXT_DESTINATION dest_affile			 { YYACCEPT; }
-        ;
+  : LL_CONTEXT_SOURCE source_affile { YYACCEPT; }
+  | LL_CONTEXT_DESTINATION dest_affile { YYACCEPT; }
+  ;
 
 source_affile
-	: KW_FILE '(' _inner_src_context_push source_affile_params _inner_src_context_pop ')'	{ $$ = $4; }
-	| KW_PIPE '(' _inner_src_context_push source_afpipe_params _inner_src_context_pop ')'	{ $$ = $4; }
-	| KW_STDIN '(' _inner_src_context_push source_stdin_params _inner_src_context_pop ')'	{ $$ = $4; }
-	| KW_WILDCARD_FILE
-	{
-	  WildcardSourceDriver *sd = (WildcardSourceDriver *) wildcard_sd_new(configuration);;
-	  *instance = &sd->super.super;
-	  affile_grammar_set_wildcard_file_source_driver(sd);
-	} '(' _inner_src_context_push source_wildcard_params _inner_src_context_pop ')' { $$ = last_driver; }
-	;
+  : KW_FILE '(' _inner_src_context_push source_affile_params _inner_src_context_pop ')'  { $$ = $4; }
+  | KW_PIPE '(' _inner_src_context_push source_afpipe_params _inner_src_context_pop ')'  { $$ = $4; }
+  | KW_STDIN '(' _inner_src_context_push source_stdin_params _inner_src_context_pop ')'  { $$ = $4; }
+  | KW_WILDCARD_FILE
+  {
+    WildcardSourceDriver *sd = (WildcardSourceDriver *) wildcard_sd_new(configuration);;
+    *instance = &sd->super.super;
+    affile_grammar_set_wildcard_file_source_driver(sd);
+  } '(' _inner_src_context_push source_wildcard_params _inner_src_context_pop ')' { $$ = last_driver; }
+  ;
 
 source_wildcard_params
-    : source_wildcard_options
-    ;
+  : source_wildcard_options
+  ;
 
 source_wildcard_options
-    : source_wildcard_option source_wildcard_options
-    |
-    ;
+  : source_wildcard_option source_wildcard_options
+  |
+  ;
 
 source_stdin_params
-	:
-	  {
-	    AFFileSourceDriver *sd = (AFFileSourceDriver *) stdin_sd_new(configuration);
-	    *instance = &sd->super.super;
-	    affile_grammar_set_file_source_driver(sd);
-	  }
-          source_affile_options                 { $$ = last_driver; }
-	;
+  :
+    {
+      AFFileSourceDriver *sd = (AFFileSourceDriver *) stdin_sd_new(configuration);
+      *instance = &sd->super.super;
+      affile_grammar_set_file_source_driver(sd);
+    }
+    source_affile_options { $$ = last_driver; }
+  ;
 
 source_affile_params
-	: string
-	  {
-	    if (!affile_is_legacy_wildcard_source($1))
-	      {
-	        AFFileSourceDriver *sd = (AFFileSourceDriver *) affile_sd_new($1, configuration);
-	        *instance = &sd->super.super;
-	        affile_grammar_set_file_source_driver(sd);
-	      }
-	    else
-	      {
-	        WildcardSourceDriver *sd = (WildcardSourceDriver *) wildcard_sd_legacy_new($1, configuration);
-	        last_legacy_wildcard_src_driver = sd;
-	        *instance = &sd->super.super;
-	        affile_grammar_set_wildcard_file_source_driver(sd);
-	      }
-	  }
-          source_affile_options                 { $$ = last_driver; last_legacy_wildcard_src_driver = NULL; free($1); }
-        ;
+  : string
+    {
+      if (!affile_is_legacy_wildcard_source($1))
+        {
+          AFFileSourceDriver *sd = (AFFileSourceDriver *) affile_sd_new($1, configuration);
+          *instance = &sd->super.super;
+          affile_grammar_set_file_source_driver(sd);
+        }
+      else
+        {
+          WildcardSourceDriver *sd = (WildcardSourceDriver *) wildcard_sd_legacy_new($1, configuration);
+          last_legacy_wildcard_src_driver = sd;
+          *instance = &sd->super.super;
+          affile_grammar_set_wildcard_file_source_driver(sd);
+        }
+    }
+    source_affile_options { $$ = last_driver; last_legacy_wildcard_src_driver = NULL; free($1); }
+  ;
 
 source_affile_options
-        : source_affile_option source_affile_options
-        | source_legacy_wildcard_option source_affile_options
-        |
-        ;
+  : source_affile_option source_affile_options
+  | source_legacy_wildcard_option source_affile_options
+  |
+  ;
 
 source_affile_option
-	: KW_FOLLOW_FREQ '(' nonnegative_float ')' { file_reader_options_set_follow_freq(last_file_reader_options, (gint) ($3 * 1000)); }
-	| KW_PAD_SIZE '(' nonnegative_integer ')' { last_log_proto_options->pad_size = $3; }
-	| multi_line_option
-	| multi_line_timeout
-	| file_perm_option
-        | source_reader_option
-	| source_driver_option
-        ;
+  : KW_FOLLOW_FREQ '(' nonnegative_float ')' { file_reader_options_set_follow_freq(last_file_reader_options, (gint) ($3 * 1000)); }
+  | KW_FOLLOW_METHOD '(' string ')' { CHECK_ERROR(file_reader_options_set_follow_method(last_file_reader_options, $3), @3, "Invalid follow-method"); free($3); }
+  | KW_PAD_SIZE '(' nonnegative_integer ')' { last_log_proto_options->pad_size = $3; }
+  | multi_line_option
+  | multi_line_timeout
+  | file_perm_option
+  | source_reader_option
+  | source_driver_option
+  ;
 
 source_wildcard_option
   : KW_BASE_DIR '(' string ')' { wildcard_sd_set_base_dir(last_driver, $3); free($3); }
@@ -196,135 +198,136 @@ source_wildcard_option
       wildcard_sd_set_filename_pattern(last_driver, $3);
       free($3);
     }
-	| KW_RECURSIVE '(' yesno ')' { wildcard_sd_set_recursive(last_driver, $3); }
-	| KW_MAX_FILES '(' positive_integer ')' { wildcard_sd_set_max_files(last_driver, $3); }
-	| KW_MONITOR_METHOD '(' string ')' { CHECK_ERROR(wildcard_sd_set_monitor_method(last_driver, $3), @3, "Invalid monitor-method"); free($3); }
-	| KW_MONITOR_FREQ '(' nonnegative_float ')' { wildcard_sd_set_monitor_freq(last_driver, (gint) ($3 * 1000)); }
-	| source_affile_option
-	;
+  | KW_RECURSIVE '(' yesno ')' { wildcard_sd_set_recursive(last_driver, $3); }
+  | KW_MAX_FILES '(' positive_integer ')' { wildcard_sd_set_max_files(last_driver, $3); }
+  | KW_MONITOR_METHOD '(' string ')' { CHECK_ERROR(wildcard_sd_set_monitor_method(last_driver, $3), @3, "Invalid monitor-method"); free($3); }
+  | KW_MONITOR_FREQ '(' nonnegative_float ')' { wildcard_sd_set_monitor_freq(last_driver, (gint) ($3 * 1000)); }
+  | source_affile_option
+  ;
 
 source_legacy_wildcard_option
-	: KW_FORCE_DIRECTORY_POLLING '(' yesno ')'
-	  {
-	    CHECK_ERROR(last_legacy_wildcard_src_driver, @1, "Invalid option, force-directory-polling() can only be used in legacy wildcard file sources");
-	    wildcard_sd_set_monitor_method(&last_legacy_wildcard_src_driver->super.super, $3 ? "poll" : "auto");
-	  }
-	| KW_RECURSIVE '(' yesno ')'
-	  {
-	    CHECK_ERROR(last_legacy_wildcard_src_driver, @1, "Invalid option, recursive() can only be used in legacy wildcard file sources");
-	    wildcard_sd_set_recursive(&last_legacy_wildcard_src_driver->super.super, $3);
-	  }
-	;
+  : KW_FORCE_DIRECTORY_POLLING '(' yesno ')'
+    {
+      CHECK_ERROR(last_legacy_wildcard_src_driver, @1, "Invalid option, force-directory-polling() can only be used in legacy wildcard file sources");
+      wildcard_sd_set_monitor_method(&last_legacy_wildcard_src_driver->super.super, $3 ? "poll" : "auto");
+    }
+  | KW_RECURSIVE '(' yesno ')'
+    {
+      CHECK_ERROR(last_legacy_wildcard_src_driver, @1, "Invalid option, recursive() can only be used in legacy wildcard file sources");
+      wildcard_sd_set_recursive(&last_legacy_wildcard_src_driver->super.super, $3);
+    }
+  ;
 
 source_afpipe_params
-	: string
-	  {
-	    AFFileSourceDriver *sd = (AFFileSourceDriver *) pipe_sd_new($1, configuration);
-	    *instance = &sd->super.super;
-	    affile_grammar_set_file_source_driver(sd);
-	  }
-	  source_afpipe_options				{ $$ = last_driver; free($1); }
-	;
+  : string
+    {
+      AFFileSourceDriver *sd = (AFFileSourceDriver *) pipe_sd_new($1, configuration);
+      *instance = &sd->super.super;
+      affile_grammar_set_file_source_driver(sd);
+    }
+    source_afpipe_options { $$ = last_driver; free($1); }
+  ;
 
 source_afpipe_options
-        : source_afpipe_option source_afpipe_options
-        |
-        ;
+  : source_afpipe_option source_afpipe_options
+  |
+  ;
 
 source_afpipe_option
-	: KW_OPTIONAL '(' yesno ')'			{ last_driver->optional = $3; }
-	| KW_PAD_SIZE '(' nonnegative_integer ')'	{ last_log_proto_options->pad_size = $3; }
-	| KW_CREATE_DIRS '(' yesno ')'			{ pipe_sd_set_create_dirs(last_driver, $3); }
-	| multi_line_option
-	| file_perm_option
-	| source_reader_option
-	| source_driver_option
-	;
+  : KW_OPTIONAL '(' yesno ')' { last_driver->optional = $3; }
+  | KW_PAD_SIZE '(' nonnegative_integer ')'  { last_log_proto_options->pad_size = $3; }
+  | KW_CREATE_DIRS '(' yesno ')' { pipe_sd_set_create_dirs(last_driver, $3); }
+  | multi_line_option
+  | file_perm_option
+  | source_reader_option
+  | source_driver_option
+  ;
 
 multi_line_timeout
-	: KW_MULTI_LINE_TIMEOUT '(' nonnegative_float ')'
-	  {
-		file_reader_options_set_multi_line_timeout(last_file_reader_options, (gint) ($3 * 1000));
-	  }
-	;
+  : KW_MULTI_LINE_TIMEOUT '(' nonnegative_float ')'
+    {
+    file_reader_options_set_multi_line_timeout(last_file_reader_options, (gint) ($3 * 1000));
+    }
+  ;
 
 dest_affile
-	: KW_FILE '(' _inner_dest_context_push dest_affile_params _inner_dest_context_pop ')'	{ $$ = $4; }
-	| KW_PIPE '(' _inner_dest_context_push dest_afpipe_params _inner_dest_context_pop ')'   { $$ = $4; }
-	| KW_STDOUT '(' _inner_dest_context_push dest_stdout_params _inner_dest_context_pop ')'	{ $$ = $4; }
-	;
+  : KW_FILE '(' _inner_dest_context_push dest_affile_params _inner_dest_context_pop ')'  { $$ = $4; }
+  | KW_PIPE '(' _inner_dest_context_push dest_afpipe_params _inner_dest_context_pop ')' { $$ = $4; }
+  | KW_STDOUT '(' _inner_dest_context_push dest_stdout_params _inner_dest_context_pop ')'  { $$ = $4; }
+  ;
 
 dest_affile_params
-	: template_content
-	  {
-	    last_driver = *instance = affile_dd_new($1, configuration);
-	    last_writer_options = &((AFFileDestDriver *) last_driver)->writer_options;
-	    last_file_perm_options = &((AFFileDestDriver *) last_driver)->file_opener_options.file_perm_options;
-	  }
-	  dest_affile_options                           { $$ = last_driver; }
-	;
+  : template_content
+    {
+      last_driver = *instance = affile_dd_new($1, configuration);
+      last_writer_options = &((AFFileDestDriver *) last_driver)->writer_options;
+      last_file_perm_options = &((AFFileDestDriver *) last_driver)->file_opener_options.file_perm_options;
+    }
+    dest_affile_options { $$ = last_driver; }
+  ;
 
 dest_affile_options
-	: dest_affile_option dest_affile_options
-        |
-	;
+  : dest_affile_option dest_affile_options
+  |
+  ;
 
 dest_affile_option
-	: dest_writer_option
-	| dest_driver_option
-        | file_perm_option
-	| KW_OPTIONAL '(' yesno ')'		{ last_driver->optional = $3; }
-	| KW_OVERWRITE_IF_OLDER '(' nonnegative_integer ')'	{ affile_dd_set_overwrite_if_older(last_driver, $3); }
-	| KW_SYMLINK_AS '(' string ')'		{ affile_dd_set_symlink_as(last_driver, $3); }
-	| KW_FSYNC '(' yesno ')'		{ affile_dd_set_fsync(last_driver, $3); }
-        | dest_affile_common_option
-	;
+  : dest_writer_option
+  | dest_driver_option
+  | file_perm_option
+  | KW_OPTIONAL '(' yesno ')' { last_driver->optional = $3; }
+  | KW_OVERWRITE_IF_OLDER '(' nonnegative_integer ')'  { affile_dd_set_overwrite_if_older(last_driver, $3); }
+  | KW_SYMLINK_AS '(' string ')' { affile_dd_set_symlink_as(last_driver, $3); }
+  | KW_FSYNC '(' yesno ')' { affile_dd_set_fsync(last_driver, $3); }
+  | dest_affile_common_option
+  ;
 
 dest_afpipe_params
-	: template_content
-	  {
-	    last_driver = *instance = pipe_dd_new($1, configuration);
-	    last_writer_options = &((AFFileDestDriver *) last_driver)->writer_options;
-	    last_file_perm_options = &((AFFileDestDriver *) last_driver)->file_opener_options.file_perm_options;
-	  }
-	  dest_afpipe_options                   { $$ = last_driver; }
-	;
+  : template_content
+    {
+      last_driver = *instance = pipe_dd_new($1, configuration);
+      last_writer_options = &((AFFileDestDriver *) last_driver)->writer_options;
+      last_file_perm_options = &((AFFileDestDriver *) last_driver)->file_opener_options.file_perm_options;
+    }
+    dest_afpipe_options { $$ = last_driver; }
+  ;
 
 dest_afpipe_options
-	: dest_afpipe_option dest_afpipe_options
-	|
-	;
+  : dest_afpipe_option dest_afpipe_options
+  |
+  ;
 
 dest_afpipe_option
-	: dest_writer_option
-	| dest_driver_option
-	| file_perm_option
-        | dest_affile_common_option
-	;
+  : dest_writer_option
+  | dest_driver_option
+  | file_perm_option
+  | dest_affile_common_option
+  ;
 
 dest_affile_common_option
-	: KW_TIME_REAP '(' nonnegative_integer ')'		{ affile_dd_set_time_reap(last_driver, $3); }
-	| KW_CREATE_DIRS '(' yesno ')'		{ affile_dd_set_create_dirs(last_driver, $3); }
-        ;
+  : KW_TIME_REAP '(' nonnegative_integer ')' { affile_dd_set_time_reap(last_driver, $3); }
+  | KW_CREATE_DIRS '(' yesno ')' { affile_dd_set_create_dirs(last_driver, $3); }
+  ;
 
 dest_stdout_params
-	:
-          {
-            last_driver = *instance = stdout_dd_new(configuration);
-            last_writer_options = &((AFFileDestDriver *) last_driver)->writer_options;
-            last_file_perm_options = &((AFFileDestDriver *) last_driver)->file_opener_options.file_perm_options;
-          } dest_stdout_options
-	;
+  :
+    {
+      last_driver = *instance = stdout_dd_new(configuration);
+      last_writer_options = &((AFFileDestDriver *) last_driver)->writer_options;
+      last_file_perm_options = &((AFFileDestDriver *) last_driver)->file_opener_options.file_perm_options;
+    }
+    dest_stdout_options
+  ;
 
 dest_stdout_options
-        : dest_stdout_option dest_stdout_options
-        |
-        ;
+  : dest_stdout_option dest_stdout_options
+  |
+  ;
 
 dest_stdout_option
-	: dest_writer_option
-	| dest_driver_option
-        ;
+  : dest_writer_option
+  | dest_driver_option
+  ;
 
 /* INCLUDE_RULES */
 
diff --git a/modules/affile/affile-parser.c b/modules/affile/affile-parser.c
index 849bb96457d..53b5e2062d3 100644
--- a/modules/affile/affile-parser.c
+++ b/modules/affile/affile-parser.c
@@ -50,6 +50,7 @@ static CfgLexerKeyword affile_keywords[] =
   { "overwrite_if_older", KW_OVERWRITE_IF_OLDER },
   { "symlink_as",         KW_SYMLINK_AS },
   { "follow_freq",        KW_FOLLOW_FREQ },
+  { "follow_method",      KW_FOLLOW_METHOD },
   { "monitor_freq",       KW_MONITOR_FREQ },
   { "multi_line_timeout", KW_MULTI_LINE_TIMEOUT },
   { "time_reap",          KW_TIME_REAP },
diff --git a/modules/affile/file-monitor-factory.c b/modules/affile/file-monitor-factory.c
new file mode 100644
index 00000000000..051f4b02a76
--- /dev/null
+++ b/modules/affile/file-monitor-factory.c
@@ -0,0 +1,86 @@
+/*
+ * Copyright (c) 2025 One Identity
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 as published
+ * by the Free Software Foundation, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ * As an additional exemption you are allowed to compile & link against the
+ * OpenSSL libraries as published by the OpenSSL project. See the file
+ * COPYING for details.
+ *
+ */
+
+#include "file-monitor-factory.h"
+#include "notified-fd-events.h"
+#include "poll-fd-events.h"
+#include "poll-file-changes.h"
+#include "poll-multiline-file-changes.h"
+
+#include <string.h>
+#include <stdio.h>
+
+FollowMethod
+file_monitor_factory_follow_method_from_string(const gchar *method)
+{
+  if (strcmp(method, "legacy") == 0)
+    {
+      return FM_LEGACY;
+    }
+  else if (strcmp(method, "poll") == 0)
+    {
+      return FM_POLL;
+    }
+  else if (strcmp(method, "system") == 0)
+    {
+      return FM_SYSTEM_POLL;
+    }
+#if SYSLOG_NG_HAVE_INOTIFY
+  else if (strcmp(method, "inotify") == 0)
+    {
+      return FM_INOTIFY;
+    }
+#endif
+  return FM_UNKNOWN;
+}
+
+PollEvents *
+create_file_monitor_events(FileReader *self, FollowMethod file_follow_mode, gint fd)
+{
+  PollEvents *poll_events = NULL;
+
+  switch (file_follow_mode)
+    {
+    case FM_SYSTEM_POLL:
+      poll_events = poll_fd_events_new(fd);
+      break;
+
+#if SYSLOG_NG_HAVE_INOTIFY
+    case FM_INOTIFY:
+      poll_events = notified_fd_events_new(fd);
+      break;
+#endif
+
+    case FM_POLL:
+      if (file_reader_options_get_log_proto_options(self->options)->multi_line_options.mode == MLM_NONE)
+        poll_events = poll_file_changes_new(fd, self->filename->str, self->options->follow_freq, &self->super);
+      else
+        poll_events = poll_multiline_file_changes_new(fd, self->filename->str, self->options->follow_freq,
+                                                      self->options->multi_line_timeout, self);
+      break;
+
+    default:
+      g_assert_not_reached();
+      break;
+    }
+  return poll_events;
+}
diff --git a/modules/affile/file-monitor-factory.h b/modules/affile/file-monitor-factory.h
new file mode 100644
index 00000000000..9b91cc2b744
--- /dev/null
+++ b/modules/affile/file-monitor-factory.h
@@ -0,0 +1,31 @@
+/*
+ * Copyright (c) 2025 One Identity
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 as published
+ * by the Free Software Foundation, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ * As an additional exemption you are allowed to compile & link against the
+ * OpenSSL libraries as published by the OpenSSL project. See the file
+ * COPYING for details.
+ *
+ */
+#ifndef MODULES_AFFILE_FILE_MONITOR_FACTORY_H_
+#define MODULES_AFFILE_FILE_MONITOR_FACTORY_H_
+
+#include "file-reader.h"
+
+FollowMethod file_monitor_factory_follow_method_from_string(const gchar *method_name);
+
+PollEvents *create_file_monitor_events(FileReader *self, FollowMethod poll_event_type, gint fd);
+
+#endif /* MODULES_AFFILE_FILE_MONITOR_FACTORY_H_ */
diff --git a/modules/affile/file-reader.c b/modules/affile/file-reader.c
index 69649b9db87..b6f9f8930a2 100644
--- a/modules/affile/file-reader.c
+++ b/modules/affile/file-reader.c
@@ -30,9 +30,7 @@
 #include "transport/transport-pipe.h"
 #include "transport-prockmsg.h"
 #include "logproto/logproto-buffered-server.h"
-#include "poll-fd-events.h"
-#include "poll-file-changes.h"
-#include "poll-multiline-file-changes.h"
+#include "file-monitor-factory.h"
 #include "ack-tracker/ack_tracker_factory.h"
 #include "stats/stats-cluster-key-builder.h"
 
@@ -225,62 +223,142 @@ _reader_check_watches(PollEvents *poll_events, gpointer user_data)
                 evt_tag_int("fn", fd));
       check_again = _reader_on_eof(self);
     }
+  else
+    {
+      if (poll_events_system_notified(poll_events))
+        log_reader_trigger_one_check(self->reader);
+    }
+
   return check_again;
 }
 
-static gboolean
-_is_fd_pollable(gint fd)
+gboolean
+iv_can_poll_fd(gint fd)
 {
   struct iv_fd check_fd;
-  gboolean pollable;
-
   IV_FD_INIT(&check_fd);
   check_fd.fd = fd;
   check_fd.cookie = NULL;
 
-  pollable = (iv_fd_register_try(&check_fd) == 0);
+  gboolean pollable = (iv_fd_register_try(&check_fd) == 0);
   if (pollable)
     iv_fd_unregister(&check_fd);
   return pollable;
 }
 
-static PollEvents *
-_construct_poll_events(FileReader *self, gint fd)
+static gboolean is_poll_options(FileReaderOptions *options)
 {
-  PollEvents *poll_events = NULL;
-  if (self->options->follow_freq > 0)
-    {
-      LogProtoFileReaderOptions *proto_opts = file_reader_options_get_log_proto_options(self->options);
+  return (options->follow_method == FM_LEGACY && options->follow_freq > 0) ||
+         options->follow_method == FM_POLL;
+}
 
-      if (proto_opts->multi_line_options.mode == MLM_NONE)
-        poll_events = poll_file_changes_new(fd, self->filename->str, self->options->follow_freq, &self->super);
-      else
-        poll_events = poll_multiline_file_changes_new(fd, self->filename->str, self->options->follow_freq,
-                                                      self->options->multi_line_timeout, self);
-      msg_debug("File follow-mode is syslog-ng poll");
-    }
-  else if (fd >= 0 && FALSE == self->should_poll_for_events)
+static gboolean is_inotify_options(FileReaderOptions *options, gboolean monitor_can_notify_file_changes)
+{
+#if SYSLOG_NG_HAVE_INOTIFY
+  return (options->follow_method == FM_LEGACY && monitor_can_notify_file_changes &&
+          (getenv("IV_SELECT_POLL_METHOD") == NULL || getenv("IV_SELECT_POLL_METHOD")[0] == 0) &&
+          (getenv("IV_EXCLUDE_POLL_METHOD") == NULL || getenv("IV_EXCLUDE_POLL_METHOD")[0] == 0)
+         ) ||
+         options->follow_method == FM_INOTIFY;
+#else
+  return FALSE;
+#endif
+}
+
+static gboolean is_system_poll_options(FileReaderOptions *options, gboolean can_poll_fd)
+{
+  return (options->follow_method == FM_LEGACY && can_poll_fd) ||
+         (options->follow_method == FM_SYSTEM_POLL && can_poll_fd);
+}
+
+static FollowMethod
+_get_effective_file_follow_mode(FileReader *self, gint fd)
+{
+  FollowMethod file_follow_mode = FM_UNKNOWN;
+
+  if (is_poll_options(self->options))
     {
-      poll_events = notified_fd_events_new(fd);
-      msg_debug("File follow-mode is inotify from directory-monitor");
+      file_follow_mode = FM_POLL;
+      msg_debug("File follow-mode is syslog-ng poll", evt_tag_int("follow_freq", self->options->follow_freq));
     }
-  else if (fd >= 0 && _is_fd_pollable(fd))
+  else if (fd >= 0)
     {
-      poll_events = poll_fd_events_new(fd);
-      msg_debug("File follow-mode is ivykis poll", evt_tag_str("poll_method", iv_poll_method_name()));
+      if (is_inotify_options(self->options, self->monitor_can_notify_file_changes))
+        {
+          file_follow_mode = FM_INOTIFY;
+          msg_debug("File follow-mode is inotify from directory-monitor");
+        }
+      else if (is_system_poll_options(self->options, iv_can_poll_fd(fd)))
+        {
+          file_follow_mode = FM_SYSTEM_POLL;
+          msg_debug("File follow-mode is system (ivykis) poll", evt_tag_str("poll_method", iv_poll_method_name()));
+        }
     }
-  else
+  return file_follow_mode;
+}
+
+static gboolean
+_validate_file_follow_mode(FileReader *self, FollowMethod file_follow_mode, gint fd)
+{
+  gboolean valid = TRUE;
+
+  switch (file_follow_mode)
     {
-      msg_error("Unable to determine how to monitor this file, follow_freq() unset and it is not possible to poll it "
-                "with the current ivykis polling method. Set follow-freq() for regular files or change "
-                "IV_EXCLUDE_POLL_METHOD environment variable to override the automatically selected polling method",
-                evt_tag_str("file_poll_method", iv_poll_method_name()),
-                evt_tag_str("filename", self->filename->str),
-                evt_tag_int("fd", fd));
-      return NULL;
+    case FM_POLL:
+      if (self->options->follow_freq <= 0)
+        {
+          msg_error("Follow-mode file follow-freq() must be greater than 0 for follow-method(poll)",
+                    evt_tag_str("filename", self->filename->str),
+                    evt_tag_int("follow_freq", self->options->follow_freq));
+          valid = FALSE;
+        }
+      break;
+
+#if SYSLOG_NG_HAVE_INOTIFY
+    case FM_INOTIFY:
+      if (self->options->follow_method == FM_INOTIFY && FALSE == self->monitor_can_notify_file_changes)
+        {
+          msg_error("The value of monitor-method() must be set to `inotify` to use follow-method(inotify)",
+                    evt_tag_str("filename", self->filename->str));
+          valid = FALSE;
+        }
+      /* Falling down!!! */
+#endif
+
+    case FM_SYSTEM_POLL:
+      if (self->options->follow_freq > 0)
+        msg_warning("File follow-method() is not `poll`, but the value of follow-freq() is not 0, ignoring follow-freq()",
+                    evt_tag_str("filename", self->filename->str),
+                    evt_tag_int("follow_freq", self->options->follow_freq));
+      break;
+
+    case FM_UNKNOWN:
+      if (fd >= 0)
+        {
+          msg_error("Unable to determine how to monitor this file, follow_freq() unset and it is not possible to poll it "
+                    "with the current ivykis polling method. Set follow-freq() for regular files or change the "
+                    "IV_SELECT_POLL_METHOD or IV_EXCLUDE_POLL_METHOD environment variable to override the automatically "
+                    "selected system (ivykis) polling method",
+                    evt_tag_str("filename", self->filename->str),
+                    evt_tag_str("poll_method", iv_poll_method_name()));
+          valid = FALSE;
+        }
+      break;
+
+    default:
+      valid = FALSE;
+      g_assert_not_reached();
+      break;
     }
+  return valid;
+}
 
-  if (_can_check_eof(self, fd))
+static PollEvents *
+_construct_file_monitor(FileReader *self, FollowMethod file_follow_mode, gint fd)
+{
+  PollEvents *poll_events = create_file_monitor_events(self, file_follow_mode, fd);
+
+  if (poll_events && _can_check_eof(self, fd))
     poll_events_set_checker(poll_events, _reader_check_watches, self);
 
   return poll_events;
@@ -324,6 +402,7 @@ _setup_logreader(LogPipe *s, PollEvents *poll_events, LogProtoServer *proto)
   FileReader *self = (FileReader *) s;
 
   self->reader = log_reader_new(log_pipe_get_config(s));
+  self->reader->can_fetch_after_handshake = TRUE;
   log_pipe_set_options(&self->reader->super.super, &self->super.options);
   log_reader_open(self->reader, proto, poll_events);
 
@@ -347,36 +426,34 @@ _reader_open_file(LogPipe *s, gboolean recover_state)
 {
   FileReader *self = (FileReader *) s;
   GlobalConfig *cfg = log_pipe_get_config(s);
-  gint fd;
-  gboolean open_deferred = FALSE;
 
+  gint fd = -1;
   FileOpenerResult res = file_opener_open_fd(self->opener, self->filename->str, AFFILE_DIR_READ, &fd);
-  gboolean file_opened =  res == FILE_OPENER_RESULT_SUCCESS;
+  gboolean file_opened = (res == FILE_OPENER_RESULT_SUCCESS);
+  g_assert(file_opened || fd == -1);
 
-  if (!file_opened && self->options->follow_freq > 0)
+  FollowMethod file_follow_mode = _get_effective_file_follow_mode(self, fd);
+  if (!_validate_file_follow_mode(self, file_follow_mode, fd))
     {
-      msg_info("Follow-mode file source not found, deferring open",
-               evt_tag_str("filename", self->filename->str));
-      open_deferred = TRUE;
-      fd = -1;
+      close(fd);
+      return FALSE;
     }
 
+  gboolean open_deferred = (!file_opened && file_follow_mode == FM_POLL);
+  if (open_deferred)
+    msg_info("Follow-mode file source not found, deferring open", evt_tag_str("filename", self->filename->str));
+
   if (file_opened || open_deferred)
     {
-      LogProtoServer *proto;
-      PollEvents *poll_events;
-
-      poll_events = _construct_poll_events(self, fd);
-      if (!poll_events)
-        {
-          close(fd);
-          return FALSE;
-        }
-      proto = _construct_proto(self, fd);
+      PollEvents *poll_events = _construct_file_monitor(self, file_follow_mode, fd);
+      g_assert(poll_events);
 
+      LogProtoServer *proto = _construct_proto(self, fd);
       _setup_logreader(s, poll_events, proto);
+
       if (recover_state)
         _recover_state(s, cfg, proto);
+
       if (!log_pipe_init((LogPipe *) self->reader))
         {
           msg_error("Error initializing log_reader, closing fd",
@@ -395,7 +472,6 @@ _reader_open_file(LogPipe *s, gboolean recover_state)
       return self->owner->super.optional;
     }
   return TRUE;
-
 }
 
 static void
@@ -431,10 +507,16 @@ _on_file_deleted(FileReader *self)
    * NOTE: Do not try to close the reader directly from here, as there might already be
    *       an io-operation in progress!
    */
-  if (poll_events_system_polled(self->reader->poll_events))
+  if (poll_events_system_polled(self->reader->poll_events) || poll_events_system_notified(self->reader->poll_events))
     log_reader_trigger_one_check(self->reader);
 }
 
+static inline void
+_on_file_modified(FileReader *self)
+{
+  log_reader_trigger_one_check(self->reader);
+}
+
 static void
 _on_read_error(FileReader *self)
 {
@@ -468,7 +550,7 @@ file_reader_notify_method(LogPipe *s, gint notify_code, gpointer user_data)
 
     case NC_FILE_MODIFIED:
       /* This is a notification from the directory monitor, we can read the file for changes */
-      log_reader_trigger_one_check(self->reader);
+      _on_file_modified(self);
       break;
 
     default:
@@ -541,10 +623,10 @@ file_reader_cue_buffer_flush(FileReader *self)
 }
 
 void
-file_reader_init_instance (FileReader *self, const gchar *filename,
-                           FileReaderOptions *options, FileOpener *opener,
-                           LogSrcDriver *owner, GlobalConfig *cfg,
-                           const gchar *persist_name_prefix)
+file_reader_init_instance(FileReader *self, const gchar *filename,
+                          FileReaderOptions *options, FileOpener *opener,
+                          LogSrcDriver *owner, GlobalConfig *cfg,
+                          const gchar *persist_name_prefix)
 {
   log_pipe_init_instance (&self->super, cfg);
   self->super.init = file_reader_init_method;
@@ -557,7 +639,7 @@ file_reader_init_instance (FileReader *self, const gchar *filename,
   self->filename = g_string_new (filename);
   self->options = options;
   self->opener = opener;
-  self->should_poll_for_events = TRUE;
+  self->monitor_can_notify_file_changes = FALSE;
   self->owner = owner;
   self->super.expr_node = owner->super.super.expr_node;
 }
@@ -568,7 +650,7 @@ file_reader_new(const gchar *filename, FileReaderOptions *options, FileOpener *o
 {
   FileReader *self = g_new0(FileReader, 1);
 
-  file_reader_init_instance (self, filename, options, opener, owner, cfg, "affile_sd");
+  file_reader_init_instance(self, filename, options, opener, owner, cfg, "affile_sd");
   return self;
 }
 
@@ -578,6 +660,21 @@ file_reader_options_set_follow_freq(FileReaderOptions *options, gint follow_freq
   options->follow_freq = follow_freq;
 }
 
+gboolean
+file_reader_options_set_follow_method(FileReaderOptions *options, const gchar *follow_method)
+{
+  FollowMethod new_method = file_monitor_factory_follow_method_from_string(follow_method);
+
+  if (new_method == FM_UNKNOWN)
+    {
+      msg_error("file-reader(): Invalid value for follow-method()",
+                evt_tag_str("follow-method", follow_method));
+      return FALSE;
+    }
+  options->follow_method = new_method;
+  return TRUE;
+}
+
 void
 file_reader_options_set_multi_line_timeout(FileReaderOptions *options, gint multi_line_timeout)
 {
@@ -591,6 +688,7 @@ file_reader_options_defaults(FileReaderOptions *options)
   log_proto_file_reader_options_defaults(file_reader_options_get_log_proto_options(options));
   options->reader_options.parse_options.flags |= LP_LOCAL;
   options->restore_state = FALSE;
+  options->follow_method = FM_LEGACY;
 }
 
 static gboolean
diff --git a/modules/affile/file-reader.h b/modules/affile/file-reader.h
index f8c720cfe8a..fe625762cf9 100644
--- a/modules/affile/file-reader.h
+++ b/modules/affile/file-reader.h
@@ -29,6 +29,7 @@
 typedef struct _FileReaderOptions
 {
   gint follow_freq;
+  FollowMethod follow_method;
   gint multi_line_timeout;
   gboolean restore_state;
   LogReaderOptions reader_options;
@@ -45,7 +46,7 @@ struct _FileReader
   LogReader *reader;
   const gchar *persist_name;
   const gchar *persist_name_prefix;
-  gboolean should_poll_for_events;
+  gboolean monitor_can_notify_file_changes;
 
   void (*on_file_moved)(FileReader *);
 };
@@ -72,7 +73,10 @@ void file_reader_remove_persist_state(FileReader *self);
 void file_reader_stop_follow_file(FileReader *self);
 void file_reader_cue_buffer_flush(FileReader *self);
 
+gboolean iv_can_poll_fd(gint fd);
+
 void file_reader_options_set_follow_freq(FileReaderOptions *options, gint follow_freq);
+gboolean file_reader_options_set_follow_method(FileReaderOptions *options, const gchar *follow_method);
 void file_reader_options_set_multi_line_timeout(FileReaderOptions *options, gint multi_line_timeout);
 
 void file_reader_options_defaults(FileReaderOptions *options);
diff --git a/modules/affile/poll-file-changes.c b/modules/affile/poll-file-changes.c
index e2e872de438..56677db7c51 100644
--- a/modules/affile/poll-file-changes.c
+++ b/modules/affile/poll-file-changes.c
@@ -220,7 +220,7 @@ poll_file_changes_init_instance(PollFileChanges *self, gint fd, const gchar *fol
 {
   self->super.stop_watches = poll_file_changes_stop_watches;
   self->super.update_watches = poll_file_changes_update_watches;
-  self->super.system_polled = FALSE;
+  self->super.type = FM_POLL;
   self->super.get_fd = _get_fd;
   self->super.free_fn = poll_file_changes_free;
 
diff --git a/modules/affile/poll-multiline-file-changes.c b/modules/affile/poll-multiline-file-changes.c
index b6a3729c673..6558b077550 100644
--- a/modules/affile/poll-multiline-file-changes.c
+++ b/modules/affile/poll-multiline-file-changes.c
@@ -139,25 +139,30 @@ poll_multiline_file_changes_stop_watches(PollEvents *s)
   poll_file_changes_stop_watches(s);
 }
 
-PollEvents *
-poll_multiline_file_changes_new(gint fd, const gchar *follow_filename, gint follow_freq,
-                                gint multi_line_timeout, FileReader *reader)
+static void
+poll_multiline_file_changes_init_instance(PollMultilineFileChanges *self, gint fd, const gchar *follow_filename,
+                                          gint follow_freq, gint multi_line_timeout, FileReader *reader)
 {
-  PollMultilineFileChanges *self = g_new0(PollMultilineFileChanges, 1);
-  poll_file_changes_init_instance(&self->super, fd, follow_filename, follow_freq, &reader->super);
-
+  self->file_reader = reader;
   self->multi_line_timeout = multi_line_timeout;
 
-  if (!self->multi_line_timeout)
-    return &self->super.super;
-
-  self->file_reader = reader;
   self->super.on_read = poll_multiline_file_changes_on_read;
   self->super.on_eof = poll_multiline_file_changes_on_eof;
   self->super.on_file_moved = poll_multiline_file_changes_on_file_moved;
 
   self->super.super.update_watches = poll_file_changes_update_watches;
   self->super.super.stop_watches = poll_multiline_file_changes_stop_watches;
+}
+
+PollEvents *
+poll_multiline_file_changes_new(gint fd, const gchar *follow_filename, gint follow_freq,
+                                gint multi_line_timeout, FileReader *reader)
+{
+  PollMultilineFileChanges *self = g_new0(PollMultilineFileChanges, 1);
+  poll_file_changes_init_instance(&self->super, fd, follow_filename, follow_freq, &reader->super);
+
+  if (multi_line_timeout != 0)
+    poll_multiline_file_changes_init_instance(self, fd, follow_filename, follow_freq, multi_line_timeout, reader);
 
   return &self->super.super;
 }
diff --git a/modules/affile/tests/test_wildcard_file_reader.c b/modules/affile/tests/test_wildcard_file_reader.c
index c44896b241d..999d643e1ac 100644
--- a/modules/affile/tests/test_wildcard_file_reader.c
+++ b/modules/affile/tests/test_wildcard_file_reader.c
@@ -121,7 +121,7 @@ _init(void)
   app_startup();
   configuration = cfg_new_snippet();
   test_event = test_deleted_file_state_event_new();
-  reader = (WildcardFileReader *)wildcard_file_reader_new(TEST_FILE_NAME, NULL, NULL, NULL, configuration, TRUE);
+  reader = (WildcardFileReader *)wildcard_file_reader_new(TEST_FILE_NAME, NULL, NULL, NULL, configuration, FALSE);
   wildcard_file_reader_on_deleted_file_eof(reader, _eof, test_event);
   cr_assert_eq(log_pipe_init(&reader->super.super), TRUE);
 }
diff --git a/modules/affile/wildcard-file-reader.c b/modules/affile/wildcard-file-reader.c
index 1b9a7298fac..82fd27dee72 100644
--- a/modules/affile/wildcard-file-reader.c
+++ b/modules/affile/wildcard-file-reader.c
@@ -159,7 +159,7 @@ wildcard_file_reader_is_deleted(WildcardFileReader *self)
 
 WildcardFileReader *
 wildcard_file_reader_new(const gchar *filename, FileReaderOptions *options, FileOpener *opener, LogSrcDriver *owner,
-                         GlobalConfig *cfg, gboolean should_poll_for_events)
+                         GlobalConfig *cfg, gboolean monitor_can_notify_file_changes)
 {
   WildcardFileReader *self = g_new0(WildcardFileReader, 1);
   file_reader_init_instance(&self->super, filename, options, opener, owner, cfg, "wildcard_file_sd");
@@ -170,6 +170,6 @@ wildcard_file_reader_new(const gchar *filename, FileReaderOptions *options, File
   IV_TASK_INIT(&self->file_state_event_handler);
   self->file_state_event_handler.cookie = self;
   self->file_state_event_handler.handler = _handle_file_state_event;
-  self->super.should_poll_for_events = should_poll_for_events;
+  self->super.monitor_can_notify_file_changes = monitor_can_notify_file_changes;
   return self;
 }
diff --git a/modules/affile/wildcard-file-reader.h b/modules/affile/wildcard-file-reader.h
index 87f6ace2b9e..16c89a0c209 100644
--- a/modules/affile/wildcard-file-reader.h
+++ b/modules/affile/wildcard-file-reader.h
@@ -55,7 +55,7 @@ struct _WildcardFileReader
 WildcardFileReader *
 wildcard_file_reader_new(const gchar *filename, FileReaderOptions *options,
                          FileOpener *opener, LogSrcDriver *owner,
-                         GlobalConfig *cfg, gboolean should_poll_for_events);
+                         GlobalConfig *cfg, gboolean monitor_can_notify_file_changes);
 
 void wildcard_file_reader_on_deleted_file_eof(WildcardFileReader *self, FileStateEventCallback cb, gpointer user_data);
 gboolean wildcard_file_reader_is_deleted(WildcardFileReader *self);
diff --git a/modules/affile/wildcard-source.c b/modules/affile/wildcard-source.c
index 6c8c205c170..3545d4f4ab3 100644
--- a/modules/affile/wildcard-source.c
+++ b/modules/affile/wildcard-source.c
@@ -113,13 +113,12 @@ _create_file_reader(WildcardSourceDriver *self, const gchar *full_path)
   gchar *base_dir = g_path_get_dirname(full_path);
   DirectoryMonitor *monitor = g_hash_table_lookup(self->directory_monitors, base_dir);
   g_free(base_dir);
-  gboolean file_reader_should_poll_for_events = (FALSE == monitor->can_notify_file_changes);
   WildcardFileReader *reader = wildcard_file_reader_new(full_path,
                                                         &self->file_reader_options,
                                                         self->file_opener,
                                                         &self->super,
                                                         cfg,
-                                                        file_reader_should_poll_for_events);
+                                                        monitor->can_notify_file_changes);
   wildcard_file_reader_on_deleted_file_eof(reader, _remove_and_readd_file_reader, self);
 
   log_pipe_set_options(&reader->super.super, &self->super.super.super.options);
@@ -134,6 +133,8 @@ _create_file_reader(WildcardSourceDriver *self, const gchar *full_path)
   else
     {
       g_hash_table_insert(self->file_readers, g_strdup(full_path), reader);
+      msg_debug("wildcard-file(): file created, start tailing",
+                evt_tag_str("filename", full_path));
     }
 }
 
@@ -147,8 +148,6 @@ _handle_file_created(WildcardSourceDriver *self, const DirectoryMonitorEvent *ev
       if (!reader)
         {
           _create_file_reader(self, event->full_path);
-          msg_debug("wildcard-file(): file created, start tailing",
-                    evt_tag_str("filename", event->full_path));
         }
       else
         {
@@ -191,13 +190,13 @@ _handle_directory_created(WildcardSourceDriver *self, const DirectoryMonitorEven
 void
 _handle_file_deleted(WildcardSourceDriver *self, const DirectoryMonitorEvent *event)
 {
-  FileReader *reader = g_hash_table_lookup(self->file_readers, event->full_path);
+  WildcardFileReader *reader = g_hash_table_lookup(self->file_readers, event->full_path);
 
   if (reader)
     {
       msg_debug("wildcard-file(): Monitored file was deleted, reading it to the end",
                 evt_tag_str("filename", event->full_path));
-      log_pipe_notify(&reader->super, NC_FILE_DELETED, NULL);
+      log_pipe_notify(&reader->super.super, NC_FILE_DELETED, NULL);
     }
 
   if (pending_file_list_remove(self->waiting_list, event->full_path))
@@ -227,10 +226,10 @@ _handler_directory_deleted(WildcardSourceDriver *self, const DirectoryMonitorEve
 static void
 _handler_file_modified(WildcardSourceDriver *self, const DirectoryMonitorEvent *event)
 {
-  FileReader *reader = g_hash_table_lookup(self->file_readers, event->full_path);
+  WildcardFileReader *reader = g_hash_table_lookup(self->file_readers, event->full_path);
 
   if (reader)
-    log_pipe_notify(&reader->super, NC_FILE_MODIFIED, NULL);
+    log_pipe_notify(&reader->super.super, NC_FILE_MODIFIED, NULL);
 }
 
 static void
diff --git a/news/feature-5338.md b/news/feature-5338.md
new file mode 100644
index 00000000000..b35638abb5c
--- /dev/null
+++ b/news/feature-5338.md
@@ -0,0 +1,9 @@
+`file()`, `wildcard-file()`: Added `follow-method()` option.
+
+|Accepted values:| legacy \| inotify \| poll \| system |
+
+This option controls how syslog-ng will follow file changes.\
+The default `legacy` mode preserves the pre-4.9 version file follow-mode behavior of syslog-ng, which is based on the value of follow-freq().\
+The `poll` value forces syslog-ng to poll for file changes at the interval specified by the monitor-freq() option, even if a more efficient method (such as `inotify` or `kqueue`) is available.\
+If `inotify` is selected and supported by the platform, syslog-ng uses it to detect changes in source files. This is the most efficient and least resource-consuming option available on Linux for regular files.\
+The `system` value will use system poll methods (via ivykis) like `port-timer` `port` `dev_poll` `epoll-timerfd` `epoll` `kqueue` `ppoll` `poll` and `uring`. For more information about how to control the system polling methods used, see [How content changes are followed in file() and wildcard-file() sources](https://syslog-ng.github.io/admin-guide/060_Sources/020_File/001_File_following).


Reviews:
user: login: kovgeri01
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-17T12:48:08Z
user: login: HofiOne
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-19T08:05:12Z
user: login: kovgeri01
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T08:19:44Z
user: login: HofiOne
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-19T08:21:35Z
user: login: kovgeri01
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T09:04:22Z

Review comments:
diff_hunk: @@ -225,62 +223,142 @@ _reader_check_watches(PollEvents *poll_events, gpointer user_data)
                 evt_tag_int("fn", fd));
       check_again = _reader_on_eof(self);
     }
+  else
+    {
+      if (poll_events_system_notified(poll_events))
+        log_reader_trigger_one_check(self->reader);
+    }
+
   return check_again;
 }
 
-static gboolean
-_is_fd_pollable(gint fd)
+gboolean
+iv_can_poll_fd(gint fd)
 {
   struct iv_fd check_fd;
-  gboolean pollable;
-
   IV_FD_INIT(&check_fd);
   check_fd.fd = fd;
   check_fd.cookie = NULL;
 
-  pollable = (iv_fd_register_try(&check_fd) == 0);
+  gboolean pollable = (iv_fd_register_try(&check_fd) == 0);
   if (pollable)
     iv_fd_unregister(&check_fd);
   return pollable;
 }
 
-static PollEvents *
-_construct_poll_events(FileReader *self, gint fd)
+static gboolean is_poll_options(FileReaderOptions *options)
 {
-  PollEvents *poll_events = NULL;
-  if (self->options->follow_freq > 0)
-    {
-      LogProtoFileReaderOptions *proto_opts = file_reader_options_get_log_proto_options(self->options);
+  return (options->follow_method == FM_LEGACY && options->follow_freq > 0) ||
+         options->follow_method == FM_POLL;
+}
 
-      if (proto_opts->multi_line_options.mode == MLM_NONE)
-        poll_events = poll_file_changes_new(fd, self->filename->str, self->options->follow_freq, &self->super);
-      else
-        poll_events = poll_multiline_file_changes_new(fd, self->filename->str, self->options->follow_freq,
-                                                      self->options->multi_line_timeout, self);
-      msg_debug("File follow-mode is syslog-ng poll");
-    }
-  else if (fd >= 0 && FALSE == self->should_poll_for_events)
+static gboolean is_inotify_options(FileReaderOptions *options, gboolean monitor_can_notify_file_changes)
+{
+#if SYSLOG_NG_HAVE_INOTIFY
+  return (options->follow_method == FM_LEGACY && monitor_can_notify_file_changes &&
+          (getenv("IV_SELECT_POLL_METHOD") == NULL || getenv("IV_SELECT_POLL_METHOD")[0] == 0) &&
+          (getenv("IV_EXCLUDE_POLL_METHOD") == NULL || getenv("IV_EXCLUDE_POLL_METHOD")[0] == 0)
path: modules/affile/file-reader.c
user: login: kovgeri01
body: why are we selecting inotify when the IV_SELECT_POLL_METHOD or the IV_EXCLUDE_POLL_METHOD is not set?
created_at: 2025-05-17T11:34:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -225,62 +223,142 @@ _reader_check_watches(PollEvents *poll_events, gpointer user_data)
                 evt_tag_int("fn", fd));
       check_again = _reader_on_eof(self);
     }
+  else
+    {
+      if (poll_events_system_notified(poll_events))
+        log_reader_trigger_one_check(self->reader);
+    }
+
   return check_again;
 }
 
-static gboolean
-_is_fd_pollable(gint fd)
+gboolean
+iv_can_poll_fd(gint fd)
 {
   struct iv_fd check_fd;
-  gboolean pollable;
-
   IV_FD_INIT(&check_fd);
   check_fd.fd = fd;
   check_fd.cookie = NULL;
 
-  pollable = (iv_fd_register_try(&check_fd) == 0);
+  gboolean pollable = (iv_fd_register_try(&check_fd) == 0);
   if (pollable)
     iv_fd_unregister(&check_fd);
   return pollable;
 }
 
-static PollEvents *
-_construct_poll_events(FileReader *self, gint fd)
+static gboolean is_poll_options(FileReaderOptions *options)
 {
-  PollEvents *poll_events = NULL;
-  if (self->options->follow_freq > 0)
-    {
-      LogProtoFileReaderOptions *proto_opts = file_reader_options_get_log_proto_options(self->options);
+  return (options->follow_method == FM_LEGACY && options->follow_freq > 0) ||
+         options->follow_method == FM_POLL;
+}
 
-      if (proto_opts->multi_line_options.mode == MLM_NONE)
-        poll_events = poll_file_changes_new(fd, self->filename->str, self->options->follow_freq, &self->super);
-      else
-        poll_events = poll_multiline_file_changes_new(fd, self->filename->str, self->options->follow_freq,
-                                                      self->options->multi_line_timeout, self);
-      msg_debug("File follow-mode is syslog-ng poll");
-    }
-  else if (fd >= 0 && FALSE == self->should_poll_for_events)
+static gboolean is_inotify_options(FileReaderOptions *options, gboolean monitor_can_notify_file_changes)
+{
+#if SYSLOG_NG_HAVE_INOTIFY
+  return (options->follow_method == FM_LEGACY && monitor_can_notify_file_changes &&
+          (getenv("IV_SELECT_POLL_METHOD") == NULL || getenv("IV_SELECT_POLL_METHOD")[0] == 0) &&
+          (getenv("IV_EXCLUDE_POLL_METHOD") == NULL || getenv("IV_EXCLUDE_POLL_METHOD")[0] == 0)
path: modules/affile/file-reader.c
user: login: kovgeri01
body: why are the environment variables important here?
created_at: 2025-05-17T11:37:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -225,62 +223,142 @@ _reader_check_watches(PollEvents *poll_events, gpointer user_data)
                 evt_tag_int("fn", fd));
       check_again = _reader_on_eof(self);
     }
+  else
+    {
+      if (poll_events_system_notified(poll_events))
+        log_reader_trigger_one_check(self->reader);
+    }
+
   return check_again;
 }
 
-static gboolean
-_is_fd_pollable(gint fd)
+gboolean
+iv_can_poll_fd(gint fd)
 {
   struct iv_fd check_fd;
-  gboolean pollable;
-
   IV_FD_INIT(&check_fd);
   check_fd.fd = fd;
   check_fd.cookie = NULL;
 
-  pollable = (iv_fd_register_try(&check_fd) == 0);
+  gboolean pollable = (iv_fd_register_try(&check_fd) == 0);
   if (pollable)
     iv_fd_unregister(&check_fd);
   return pollable;
 }
 
-static PollEvents *
-_construct_poll_events(FileReader *self, gint fd)
+static gboolean is_poll_options(FileReaderOptions *options)
 {
-  PollEvents *poll_events = NULL;
-  if (self->options->follow_freq > 0)
-    {
-      LogProtoFileReaderOptions *proto_opts = file_reader_options_get_log_proto_options(self->options);
+  return (options->follow_method == FM_LEGACY && options->follow_freq > 0) ||
+         options->follow_method == FM_POLL;
+}
 
-      if (proto_opts->multi_line_options.mode == MLM_NONE)
-        poll_events = poll_file_changes_new(fd, self->filename->str, self->options->follow_freq, &self->super);
-      else
-        poll_events = poll_multiline_file_changes_new(fd, self->filename->str, self->options->follow_freq,
-                                                      self->options->multi_line_timeout, self);
-      msg_debug("File follow-mode is syslog-ng poll");
-    }
-  else if (fd >= 0 && FALSE == self->should_poll_for_events)
+static gboolean is_inotify_options(FileReaderOptions *options, gboolean monitor_can_notify_file_changes)
+{
+#if SYSLOG_NG_HAVE_INOTIFY
+  return (options->follow_method == FM_LEGACY && monitor_can_notify_file_changes &&
+          (getenv("IV_SELECT_POLL_METHOD") == NULL || getenv("IV_SELECT_POLL_METHOD")[0] == 0) &&
+          (getenv("IV_EXCLUDE_POLL_METHOD") == NULL || getenv("IV_EXCLUDE_POLL_METHOD")[0] == 0)
+         ) ||
+         options->follow_method == FM_INOTIFY;
+#else
+  return FALSE;
+#endif
+}
+
+static gboolean is_system_poll_options(FileReaderOptions *options, gboolean can_poll_fd)
+{
+  return (options->follow_method == FM_LEGACY && can_poll_fd) ||
+         (options->follow_method == FM_SYSTEM_POLL && can_poll_fd);
+}
+
+static FollowMethod
+_get_file_follow_mode(FileReader *self, gint fd)
+{
+  FollowMethod file_follow_mode = FM_UNKNOWN;
+
+  if (is_poll_options(self->options))
     {
-      poll_events = notified_fd_events_new(fd);
-      msg_debug("File follow-mode is inotify from directory-monitor");
+      file_follow_mode = FM_POLL;
+      msg_debug("File follow-mode is syslog-ng poll", evt_tag_int("follow_freq", self->options->follow_freq));
     }
-  else if (fd >= 0 && _is_fd_pollable(fd))
+  else if (fd >= 0)
     {
-      poll_events = poll_fd_events_new(fd);
-      msg_debug("File follow-mode is ivykis poll", evt_tag_str("poll_method", iv_poll_method_name()));
+      if (is_inotify_options(self->options, self->monitor_can_notify_file_changes))
+        {
+          file_follow_mode = FM_INOTIFY;
+          msg_debug("File follow-mode is inotify from directory-monitor");
+        }
+      else if (is_system_poll_options(self->options, iv_can_poll_fd(fd)))
+        {
+          file_follow_mode = FM_SYSTEM_POLL;
+          msg_debug("File follow-mode is system (ivykis) poll", evt_tag_str("poll_method", iv_poll_method_name()));
+        }
     }
-  else
+  return file_follow_mode;
+}
+
+static gboolean
+_validate_file_follow_mode(FileReader *self, FollowMethod file_follow_mode, gint fd)
+{
+  gboolean valid = TRUE;
+
+  switch (file_follow_mode)
     {
-      msg_error("Unable to determine how to monitor this file, follow_freq() unset and it is not possible to poll it "
-                "with the current ivykis polling method. Set follow-freq() for regular files or change "
-                "IV_EXCLUDE_POLL_METHOD environment variable to override the automatically selected polling method",
-                evt_tag_str("file_poll_method", iv_poll_method_name()),
-                evt_tag_str("filename", self->filename->str),
-                evt_tag_int("fd", fd));
-      return NULL;
+    case FM_POLL:
+      if (self->options->follow_freq <= 0)
+        {
+          msg_error("Follow-mode file follow-freq() must be greater than 0 for follow-method(poll)",
+                    evt_tag_str("filename", self->filename->str),
+                    evt_tag_int("follow_freq", self->options->follow_freq));
+          valid = FALSE;
+        }
+      break;
+
+#if SYSLOG_NG_HAVE_INOTIFY
+    case FM_INOTIFY:
+      if (self->options->follow_method == FM_INOTIFY && FALSE == self->monitor_can_notify_file_changes)
path: modules/affile/file-reader.c
user: login: kovgeri01
body: this does not accept FM_LEGACY but previously it was allowed as an inotify option
maybe we should set it ot FM_UNKNOWN since it will always be this error, or am i missing something?
created_at: 2025-05-17T11:39:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -347,36 +426,34 @@ _reader_open_file(LogPipe *s, gboolean recover_state)
 {
   FileReader *self = (FileReader *) s;
   GlobalConfig *cfg = log_pipe_get_config(s);
-  gint fd;
-  gboolean open_deferred = FALSE;
 
+  gint fd;
path: modules/affile/file-reader.c
user: login: kovgeri01
body: initialise the `fd` to somethin (preferably 0 or something because if we are too unlucky the memory piece might contain -1 or even crash) this should also result in a warning that we are using uninitialised variable there
created_at: 2025-05-17T12:46:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -347,36 +426,34 @@ _reader_open_file(LogPipe *s, gboolean recover_state)
 {
   FileReader *self = (FileReader *) s;
   GlobalConfig *cfg = log_pipe_get_config(s);
-  gint fd;
-  gboolean open_deferred = FALSE;
 
+  gint fd;
path: modules/affile/file-reader.c
user: login: HofiOne
body: Even though I hate uninitialized variables, but
- this variable was not initialized initially either
- I checked the file_opener_open_fd() function, and as I could see, it will always correctly set the value of the passed in gint*
- just to be sure there is an assertion below that prevents file_opener_open_fd() from returning with an invalid value
- the whole source code is full of uninitialized variables, I guess for performance reasons, which I personally really hate because any later internal code restructuring can easily lead to issues you mentioned if one forgets to handle an uninitialized variable according to a possible flow logic change
created_at: 2025-05-19T07:43:28Z
author_association: COLLABORATOR
diff_hunk: @@ -225,62 +223,142 @@ _reader_check_watches(PollEvents *poll_events, gpointer user_data)
                 evt_tag_int("fn", fd));
       check_again = _reader_on_eof(self);
     }
+  else
+    {
+      if (poll_events_system_notified(poll_events))
+        log_reader_trigger_one_check(self->reader);
+    }
+
   return check_again;
 }
 
-static gboolean
-_is_fd_pollable(gint fd)
+gboolean
+iv_can_poll_fd(gint fd)
 {
   struct iv_fd check_fd;
-  gboolean pollable;
-
   IV_FD_INIT(&check_fd);
   check_fd.fd = fd;
   check_fd.cookie = NULL;
 
-  pollable = (iv_fd_register_try(&check_fd) == 0);
+  gboolean pollable = (iv_fd_register_try(&check_fd) == 0);
   if (pollable)
     iv_fd_unregister(&check_fd);
   return pollable;
 }
 
-static PollEvents *
-_construct_poll_events(FileReader *self, gint fd)
+static gboolean is_poll_options(FileReaderOptions *options)
 {
-  PollEvents *poll_events = NULL;
-  if (self->options->follow_freq > 0)
-    {
-      LogProtoFileReaderOptions *proto_opts = file_reader_options_get_log_proto_options(self->options);
+  return (options->follow_method == FM_LEGACY && options->follow_freq > 0) ||
+         options->follow_method == FM_POLL;
+}
 
-      if (proto_opts->multi_line_options.mode == MLM_NONE)
-        poll_events = poll_file_changes_new(fd, self->filename->str, self->options->follow_freq, &self->super);
-      else
-        poll_events = poll_multiline_file_changes_new(fd, self->filename->str, self->options->follow_freq,
-                                                      self->options->multi_line_timeout, self);
-      msg_debug("File follow-mode is syslog-ng poll");
-    }
-  else if (fd >= 0 && FALSE == self->should_poll_for_events)
+static gboolean is_inotify_options(FileReaderOptions *options, gboolean monitor_can_notify_file_changes)
+{
+#if SYSLOG_NG_HAVE_INOTIFY
+  return (options->follow_method == FM_LEGACY && monitor_can_notify_file_changes &&
+          (getenv("IV_SELECT_POLL_METHOD") == NULL || getenv("IV_SELECT_POLL_METHOD")[0] == 0) &&
+          (getenv("IV_EXCLUDE_POLL_METHOD") == NULL || getenv("IV_EXCLUDE_POLL_METHOD")[0] == 0)
+         ) ||
+         options->follow_method == FM_INOTIFY;
+#else
+  return FALSE;
+#endif
+}
+
+static gboolean is_system_poll_options(FileReaderOptions *options, gboolean can_poll_fd)
+{
+  return (options->follow_method == FM_LEGACY && can_poll_fd) ||
+         (options->follow_method == FM_SYSTEM_POLL && can_poll_fd);
+}
+
+static FollowMethod
+_get_file_follow_mode(FileReader *self, gint fd)
+{
+  FollowMethod file_follow_mode = FM_UNKNOWN;
+
+  if (is_poll_options(self->options))
     {
-      poll_events = notified_fd_events_new(fd);
-      msg_debug("File follow-mode is inotify from directory-monitor");
+      file_follow_mode = FM_POLL;
+      msg_debug("File follow-mode is syslog-ng poll", evt_tag_int("follow_freq", self->options->follow_freq));
     }
-  else if (fd >= 0 && _is_fd_pollable(fd))
+  else if (fd >= 0)
     {
-      poll_events = poll_fd_events_new(fd);
-      msg_debug("File follow-mode is ivykis poll", evt_tag_str("poll_method", iv_poll_method_name()));
+      if (is_inotify_options(self->options, self->monitor_can_notify_file_changes))
+        {
+          file_follow_mode = FM_INOTIFY;
+          msg_debug("File follow-mode is inotify from directory-monitor");
+        }
+      else if (is_system_poll_options(self->options, iv_can_poll_fd(fd)))
+        {
+          file_follow_mode = FM_SYSTEM_POLL;
+          msg_debug("File follow-mode is system (ivykis) poll", evt_tag_str("poll_method", iv_poll_method_name()));
+        }
     }
-  else
+  return file_follow_mode;
+}
+
+static gboolean
+_validate_file_follow_mode(FileReader *self, FollowMethod file_follow_mode, gint fd)
+{
+  gboolean valid = TRUE;
+
+  switch (file_follow_mode)
     {
-      msg_error("Unable to determine how to monitor this file, follow_freq() unset and it is not possible to poll it "
-                "with the current ivykis polling method. Set follow-freq() for regular files or change "
-                "IV_EXCLUDE_POLL_METHOD environment variable to override the automatically selected polling method",
-                evt_tag_str("file_poll_method", iv_poll_method_name()),
-                evt_tag_str("filename", self->filename->str),
-                evt_tag_int("fd", fd));
-      return NULL;
+    case FM_POLL:
+      if (self->options->follow_freq <= 0)
+        {
+          msg_error("Follow-mode file follow-freq() must be greater than 0 for follow-method(poll)",
+                    evt_tag_str("filename", self->filename->str),
+                    evt_tag_int("follow_freq", self->options->follow_freq));
+          valid = FALSE;
+        }
+      break;
+
+#if SYSLOG_NG_HAVE_INOTIFY
+    case FM_INOTIFY:
+      if (self->options->follow_method == FM_INOTIFY && FALSE == self->monitor_can_notify_file_changes)
path: modules/affile/file-reader.c
user: login: HofiOne
body: this function will not ever be called with the (placeholder, backward compatibility only) FM_LEGACY value; the `_get_file_follow_mode` should use the earlier logic (order) to get the effective follow mode (again, to be backward compatible, that is what the legacy mode is introduced)

for better readability I renamed the `_get_file_follow_mode` to `_get_effective_file_follow_mode`
created_at: 2025-05-19T08:04:13Z
author_association: COLLABORATOR
diff_hunk: @@ -347,36 +426,34 @@ _reader_open_file(LogPipe *s, gboolean recover_state)
 {
   FileReader *self = (FileReader *) s;
   GlobalConfig *cfg = log_pipe_get_config(s);
-  gint fd;
-  gboolean open_deferred = FALSE;
 
+  gint fd;
path: modules/affile/file-reader.c
user: login: kovgeri01
body: The second point is not true the `file_opener_open_fd` function can indeed return `FILE_OPENER_RESULT_ERROR_PERMANENT`, `FILE_OPENER_RESULT_ERROR_TRANSIENT` or `FILE_OPENER_RESULT_ERROR_TRANSIENT` without ever initializing the variable, as the initialization of the fd variable is done after some prechecks, so in the rare case when this happens and we unlucky enough that the uninitialised value is -1 we will pass as if theese errors are nothing
created_at: 2025-05-19T08:19:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -225,62 +223,142 @@ _reader_check_watches(PollEvents *poll_events, gpointer user_data)
                 evt_tag_int("fn", fd));
       check_again = _reader_on_eof(self);
     }
+  else
+    {
+      if (poll_events_system_notified(poll_events))
+        log_reader_trigger_one_check(self->reader);
+    }
+
   return check_again;
 }
 
-static gboolean
-_is_fd_pollable(gint fd)
+gboolean
+iv_can_poll_fd(gint fd)
 {
   struct iv_fd check_fd;
-  gboolean pollable;
-
   IV_FD_INIT(&check_fd);
   check_fd.fd = fd;
   check_fd.cookie = NULL;
 
-  pollable = (iv_fd_register_try(&check_fd) == 0);
+  gboolean pollable = (iv_fd_register_try(&check_fd) == 0);
   if (pollable)
     iv_fd_unregister(&check_fd);
   return pollable;
 }
 
-static PollEvents *
-_construct_poll_events(FileReader *self, gint fd)
+static gboolean is_poll_options(FileReaderOptions *options)
 {
-  PollEvents *poll_events = NULL;
-  if (self->options->follow_freq > 0)
-    {
-      LogProtoFileReaderOptions *proto_opts = file_reader_options_get_log_proto_options(self->options);
+  return (options->follow_method == FM_LEGACY && options->follow_freq > 0) ||
+         options->follow_method == FM_POLL;
+}
 
-      if (proto_opts->multi_line_options.mode == MLM_NONE)
-        poll_events = poll_file_changes_new(fd, self->filename->str, self->options->follow_freq, &self->super);
-      else
-        poll_events = poll_multiline_file_changes_new(fd, self->filename->str, self->options->follow_freq,
-                                                      self->options->multi_line_timeout, self);
-      msg_debug("File follow-mode is syslog-ng poll");
-    }
-  else if (fd >= 0 && FALSE == self->should_poll_for_events)
+static gboolean is_inotify_options(FileReaderOptions *options, gboolean monitor_can_notify_file_changes)
+{
+#if SYSLOG_NG_HAVE_INOTIFY
+  return (options->follow_method == FM_LEGACY && monitor_can_notify_file_changes &&
+          (getenv("IV_SELECT_POLL_METHOD") == NULL || getenv("IV_SELECT_POLL_METHOD")[0] == 0) &&
+          (getenv("IV_EXCLUDE_POLL_METHOD") == NULL || getenv("IV_EXCLUDE_POLL_METHOD")[0] == 0)
path: modules/affile/file-reader.c
user: login: HofiOne
body: This is a correction of the latest changes when we blindly forced the inotify method to be used in the legacy mode if it's available, checking and using the values of the ENV variables, lets the user override the forced default in the legacy mode as well (just like it happened before we added the inotify method)
created_at: 2025-05-19T08:21:34Z
author_association: COLLABORATOR

PR comments:
user: login: HofiOne
created_at: 2025-04-28T11:29:31Z
body: @kira-syslogng test this please;
author_association: COLLABORATOR
user: login: github-actions[bot]
created_at: 2025-04-28T21:19:44Z
body: #### This Pull Request introduces config grammar changes
[syslog-ng/c6a85b1b204619a078440231caf21471e390dee2](https://github.com/syslog-ng/syslog-ng/tree/c6a85b1b204619a078440231caf21471e390dee2) -> [HofiOne/fix-wildcard-file-regression](https://github.com/HofiOne/syslog-ng/tree/fix-wildcard-file-regression)
<details>

```diff
--- a/source
+++ b/source

 file(
+    follow-method(<string>)
 )

 stdin(
+    follow-method(<string>)
 )

 wildcard-file(
+    follow-method(<string>)
 )

```
</details>
author_association: CONTRIBUTOR
user: login: kira-syslogng
created_at: 2025-04-28T21:55:34Z
body: Build FAILURE

author_association: CONTRIBUTOR
user: login: kira-syslogng
created_at: 2025-05-07T16:25:05Z
body: Build FAILURE

author_association: CONTRIBUTOR
user: login: kira-syslogng
created_at: 2025-05-16T13:52:53Z
body: Build FAILURE

author_association: CONTRIBUTOR
user: login: kira-syslogng
created_at: 2025-05-19T09:18:10Z
body: Build FAILURE

author_association: CONTRIBUTOR