@@ -287,10 +287,178 @@ 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+ if maxfiles > soft {
339+ if setrlimit ( Resource :: RLIMIT_NOFILE , maxfiles, hard) . is_ok ( ) {
340+ info ! (
341+ message = "Raised file descriptor limit." ,
342+ from = soft,
343+ to = maxfiles,
344+ ) ;
345+ return ;
346+ }
347+ }
348+ }
349+ }
350+
351+ warn ! (
352+ message = "Failed to raise file descriptor limit." ,
353+ current = soft,
354+ attempted = hard,
355+ ) ;
356+ }
357+
358+ /// Query the macOS kernel limit on per-process open files.
359+ #[ cfg( target_os = "macos" ) ]
360+ fn macos_maxfilesperproc ( ) -> Option < libc:: rlim_t > {
361+ let mut maxfiles: libc:: c_int = 0 ;
362+ let mut len = std:: mem:: size_of :: < libc:: c_int > ( ) as libc:: size_t ;
363+ // Safety: sysctlbyname with a valid null-terminated name and correctly sized output buffer.
364+ // No safe wrapper exists for this macOS-specific call.
365+ let ret = unsafe {
366+ libc:: sysctlbyname (
367+ b"kern.maxfilesperproc\0 " . as_ptr ( ) as * const libc:: c_char ,
368+ & mut maxfiles as * mut libc:: c_int as * mut libc:: c_void ,
369+ & mut len,
370+ std:: ptr:: null_mut ( ) ,
371+ 0 ,
372+ )
373+ } ;
374+ if ret == 0 && maxfiles > 0 {
375+ Some ( maxfiles as libc:: rlim_t )
376+ } else {
377+ None
378+ }
379+ }
380+
381+ #[ cfg( test) ]
382+ mod tests {
383+ #[ test]
384+ #[ cfg( unix) ]
385+ fn test_raise_file_descriptor_limit ( ) {
386+ use nix:: sys:: resource:: { Resource , getrlimit, setrlimit} ;
387+
388+ // Save original limits
389+ let ( original_soft, hard) = getrlimit ( Resource :: RLIMIT_NOFILE ) . unwrap ( ) ;
390+
391+ // Lower the soft limit to simulate a constrained environment
392+ let lowered = std:: cmp:: min ( original_soft, 256 ) ;
393+ if lowered < hard {
394+ setrlimit ( Resource :: RLIMIT_NOFILE , lowered, hard) . unwrap ( ) ;
395+
396+ // Verify it was lowered
397+ let ( soft_before, _) = getrlimit ( Resource :: RLIMIT_NOFILE ) . unwrap ( ) ;
398+ assert_eq ! ( soft_before, lowered) ;
399+
400+ // Call the function under test
401+ super :: raise_file_descriptor_limit ( ) ;
402+
403+ // Verify the soft limit was raised above the lowered value
404+ let ( soft_after, _) = getrlimit ( Resource :: RLIMIT_NOFILE ) . unwrap ( ) ;
405+ assert ! (
406+ soft_after > lowered,
407+ "Expected soft limit to be raised above {lowered}, got {soft_after}"
408+ ) ;
409+
410+ // Restore original limits
411+ setrlimit ( Resource :: RLIMIT_NOFILE , original_soft, hard) . unwrap ( ) ;
412+ }
413+ }
414+
415+ #[ test]
416+ #[ cfg( unix) ]
417+ fn test_raise_file_descriptor_limit_already_at_max ( ) {
418+ use nix:: sys:: resource:: { Resource , getrlimit, setrlimit} ;
419+
420+ // Save original limits
421+ let ( original_soft, hard) = getrlimit ( Resource :: RLIMIT_NOFILE ) . unwrap ( ) ;
422+
423+ // Set soft = hard so there's nothing to raise
424+ setrlimit ( Resource :: RLIMIT_NOFILE , hard, hard)
425+ . or_else ( |_| {
426+ // On macOS, hard might be RLIM_INFINITY; use a fallback
427+ #[ cfg( target_os = "macos" ) ]
428+ if let Some ( maxfiles) = super :: macos_maxfilesperproc ( ) {
429+ return setrlimit ( Resource :: RLIMIT_NOFILE , maxfiles, hard) ;
430+ }
431+ Err ( nix:: errno:: Errno :: EINVAL )
432+ } )
433+ . ok ( ) ;
434+
435+ let ( soft_before, _) = getrlimit ( Resource :: RLIMIT_NOFILE ) . unwrap ( ) ;
436+
437+ // Call the function — should be a no-op
438+ super :: raise_file_descriptor_limit ( ) ;
439+
440+ let ( soft_after, _) = getrlimit ( Resource :: RLIMIT_NOFILE ) . unwrap ( ) ;
441+ assert_eq ! ( soft_before, soft_after) ;
442+
443+ // Restore original limits
444+ setrlimit ( Resource :: RLIMIT_NOFILE , original_soft, hard) . unwrap ( ) ;
445+ }
446+
447+ #[ test]
448+ #[ cfg( target_os = "macos" ) ]
449+ fn test_macos_maxfilesperproc_returns_positive ( ) {
450+ let result = super :: macos_maxfilesperproc ( ) ;
451+ assert ! (
452+ result. is_some( ) ,
453+ "macos_maxfilesperproc() should return Some on macOS"
454+ ) ;
455+ assert ! (
456+ result. unwrap( ) > 0 ,
457+ "kern.maxfilesperproc should be positive"
458+ ) ;
459+ }
460+ }
461+
294462#[ derive( Parser , Debug ) ]
295463#[ command( rename_all = "kebab-case" ) ]
296464pub enum SubCommand {
0 commit comments