@@ -287,10 +287,173 @@ impl RootOpts {
287287 }
288288 }
289289
290+ #[ cfg( unix) ]
291+ raise_file_descriptor_limit ( ) ;
292+
290293 crate :: metrics:: init_global ( ) . expect ( "metrics initialization failed" ) ;
291294 }
292295}
293296
297+ /// Raise the soft file descriptor limit (RLIMIT_NOFILE) as high as the OS allows.
298+ ///
299+ /// Many systems default the soft limit to 1024 (Linux) or 256 (macOS), which is too low
300+ /// for Vector when it monitors large numbers of log files. Raising it prevents
301+ /// "Too many open files (os error 24)" errors without requiring manual sysadmin intervention.
302+ ///
303+ /// On Linux, the soft limit is raised to the hard limit (typically 65536+).
304+ /// On macOS, the hard limit can be RLIM_INFINITY, so we first try the hard limit,
305+ /// then fall back to the kernel-enforced `kern.maxfilesperproc` (typically 10240).
306+ #[ cfg( unix) ]
307+ fn raise_file_descriptor_limit ( ) {
308+ use nix:: sys:: resource:: { Resource , getrlimit, setrlimit} ;
309+ use tracing:: { info, warn} ;
310+
311+ let ( soft, hard) = match getrlimit ( Resource :: RLIMIT_NOFILE ) {
312+ Ok ( limits) => limits,
313+ Err ( err) => {
314+ warn ! ( message = "Failed to get file descriptor limit." , %err) ;
315+ return ;
316+ }
317+ } ;
318+
319+ if soft >= hard {
320+ return ; // Already at maximum
321+ }
322+
323+ // Try setting soft limit to hard limit (works on Linux, may fail on macOS)
324+ if setrlimit ( Resource :: RLIMIT_NOFILE , hard, hard) . is_ok ( ) {
325+ info ! (
326+ message = "Raised file descriptor limit." ,
327+ from = soft,
328+ to = hard,
329+ ) ;
330+ return ;
331+ }
332+
333+ // On macOS, the hard limit can be RLIM_INFINITY which setrlimit rejects.
334+ // Fall back to the kernel-enforced kern.maxfilesperproc.
335+ #[ cfg( target_os = "macos" ) ]
336+ {
337+ if let Some ( maxfiles) = macos_maxfilesperproc ( )
338+ && maxfiles > soft
339+ && setrlimit ( Resource :: RLIMIT_NOFILE , maxfiles, hard) . is_ok ( )
340+ {
341+ info ! (
342+ message = "Raised file descriptor limit." ,
343+ from = soft,
344+ to = maxfiles,
345+ ) ;
346+ return ;
347+ }
348+ }
349+
350+ warn ! (
351+ message = "Failed to raise file descriptor limit." ,
352+ current = soft,
353+ attempted = hard,
354+ ) ;
355+ }
356+
357+ /// Query the macOS kernel limit on per-process open files.
358+ #[ cfg( target_os = "macos" ) ]
359+ fn macos_maxfilesperproc ( ) -> Option < libc:: rlim_t > {
360+ let mut maxfiles: libc:: c_int = 0 ;
361+ let mut len = std:: mem:: size_of :: < libc:: c_int > ( ) as libc:: size_t ;
362+ // Safety: sysctlbyname with a valid null-terminated name and correctly sized output buffer.
363+ // No safe wrapper exists for this macOS-specific call.
364+ let ret = unsafe {
365+ libc:: sysctlbyname (
366+ c"kern.maxfilesperproc" . as_ptr ( ) ,
367+ & mut maxfiles as * mut libc:: c_int as * mut libc:: c_void ,
368+ & mut len,
369+ std:: ptr:: null_mut ( ) ,
370+ 0 ,
371+ )
372+ } ;
373+ if ret == 0 && maxfiles > 0 {
374+ Some ( maxfiles as libc:: rlim_t )
375+ } else {
376+ None
377+ }
378+ }
379+
380+ #[ cfg( test) ]
381+ mod tests {
382+ #[ test]
383+ #[ cfg( unix) ]
384+ fn test_raise_file_descriptor_limit ( ) {
385+ use nix:: sys:: resource:: { Resource , getrlimit, setrlimit} ;
386+
387+ // Save original limits
388+ let ( original_soft, hard) = getrlimit ( Resource :: RLIMIT_NOFILE ) . unwrap ( ) ;
389+
390+ // Lower the soft limit to simulate a constrained environment
391+ let lowered = std:: cmp:: min ( original_soft, 256 ) ;
392+ if lowered < hard {
393+ setrlimit ( Resource :: RLIMIT_NOFILE , lowered, hard) . unwrap ( ) ;
394+
395+ // Verify it was lowered
396+ let ( soft_before, _) = getrlimit ( Resource :: RLIMIT_NOFILE ) . unwrap ( ) ;
397+ assert_eq ! ( soft_before, lowered) ;
398+
399+ // Call the function under test
400+ super :: raise_file_descriptor_limit ( ) ;
401+
402+ // Verify the soft limit was raised above the lowered value
403+ let ( soft_after, _) = getrlimit ( Resource :: RLIMIT_NOFILE ) . unwrap ( ) ;
404+ assert ! (
405+ soft_after > lowered,
406+ "Expected soft limit to be raised above {lowered}, got {soft_after}"
407+ ) ;
408+
409+ // Restore original limits
410+ setrlimit ( Resource :: RLIMIT_NOFILE , original_soft, hard) . unwrap ( ) ;
411+ }
412+ }
413+
414+ #[ test]
415+ #[ cfg( unix) ]
416+ fn test_raise_file_descriptor_limit_already_at_max ( ) {
417+ use nix:: sys:: resource:: { Resource , getrlimit, setrlimit} ;
418+
419+ // Save original limits
420+ let ( original_soft, hard) = getrlimit ( Resource :: RLIMIT_NOFILE ) . unwrap ( ) ;
421+
422+ // Set soft = hard so there's nothing to raise
423+ if setrlimit ( Resource :: RLIMIT_NOFILE , hard, hard) . is_err ( ) {
424+ #[ cfg( target_os = "macos" ) ]
425+ if let Some ( maxfiles) = super :: macos_maxfilesperproc ( ) {
426+ let _ = setrlimit ( Resource :: RLIMIT_NOFILE , maxfiles, hard) ;
427+ }
428+ }
429+
430+ let ( soft_before, _) = getrlimit ( Resource :: RLIMIT_NOFILE ) . unwrap ( ) ;
431+
432+ // Call the function — should be a no-op
433+ super :: raise_file_descriptor_limit ( ) ;
434+
435+ let ( soft_after, _) = getrlimit ( Resource :: RLIMIT_NOFILE ) . unwrap ( ) ;
436+ assert_eq ! ( soft_before, soft_after) ;
437+
438+ // Restore original limits
439+ setrlimit ( Resource :: RLIMIT_NOFILE , original_soft, hard) . unwrap ( ) ;
440+ }
441+
442+ #[ test]
443+ #[ cfg( target_os = "macos" ) ]
444+ fn test_macos_maxfilesperproc_returns_positive ( ) {
445+ let result = super :: macos_maxfilesperproc ( ) ;
446+ assert ! (
447+ result. is_some( ) ,
448+ "macos_maxfilesperproc() should return Some on macOS"
449+ ) ;
450+ assert ! (
451+ result. unwrap( ) > 0 ,
452+ "kern.maxfilesperproc should be positive"
453+ ) ;
454+ }
455+ }
456+
294457#[ derive( Parser , Debug ) ]
295458#[ command( rename_all = "kebab-case" ) ]
296459pub enum SubCommand {
0 commit comments