package Log::Any::Adapter::MacOS::OSLog v0.0.5;

# ABSTRACT: log to macOS' unified logging system

#pod =head1 SYNOPSIS
#pod
#pod     use Log::Any::Adapter ('MacOS::OSLog',
#pod       subsystem => 'com.example.foo',
#pod     );
#pod
#pod     # or
#pod
#pod     use Log::Any::Adapter;
#pod     Log::Any::Adapter->set('MacOS::OSLog',
#pod       subsystem => 'org.example.bar',
#pod     );
#pod
#pod     # You can override defaults:
#pod     Log::Any::Adapter->set('MacOS::OSLog',
#pod       subsystem   => 'net.example.baz',
#pod       os_category => 'secret',
#pod       private     => 1,
#pod     );
#pod
#pod =head1 DESCRIPTION
#pod
#pod This L<Log::Any> adapter lets Perl applications log directly to macOS'
#pod L<unified logging system|https://developer.apple.com/documentation/os/logging>
#pod using FFI and C wrappers--no Swift required.
#pod
#pod To send log entries easily from the command line, a C<maclog> utility
#pod script is also included in this distribution.
#pod
#pod =cut

use 5.018;
use warnings;
use Carp;
use File::Spec::Functions qw(catfile);
use File::Basename;

my $ffi = FFI::Platypus->new( api => 2, lib => [ _find_my_bundle() ] );

$ffi->attach(
    [ os_log_create => '_os_log_create' ] => [ 'string', 'string' ],
    'opaque',
);

my %LOGGING_LEVELS;
my @LOGGING_METHOD_NAMES = logging_methods();
for my $level_num ( 0 .. $#LOGGING_METHOD_NAMES ) {
    $LOGGING_LEVELS{ $LOGGING_METHOD_NAMES[$level_num] } = $level_num;
}
my %LOG_LEVEL_ALIASES = log_level_aliases();
for ( keys %LOG_LEVEL_ALIASES ) {
    $LOGGING_LEVELS{$_} = $LOGGING_LEVELS{ $LOG_LEVEL_ALIASES{$_} };
}

#pod =method init
#pod
#pod This method is not called directly, but rather is passed named arguments
#pod as a hash when setting a L<Log::Any::Adapter>:
#pod
#pod =over
#pod
#pod =item subsystem
#pod
#pod =for stopwords FQDN
#pod
#pod Required. Must be a reversed fully-qualified domain name (FQDN), e.g.,
#pod C<com.example.perl>.
#pod
#pod =item log_level, min_level, level
#pod
#pod These are all synonymous and strings that set the minimum logging level
#pod for the adapter. Whatever level is set, messages for that level and
#pod above will be logged.
#pod
#pod Defaults to C<trace> and is affected by various
#pod L<environment variables|/"CONFIGURATION AND ENVIRONMENT">.
#pod
#pod =cut

sub _min_level {
    my $self = shift;
    return $ENV{LOG_LEVEL}
        if $ENV{LOG_LEVEL}
        and defined $LOGGING_LEVELS{ $ENV{LOG_LEVEL} };
    return 'trace' if $ENV{TRACE};
    return 'debug' if $ENV{DEBUG};
    return 'info'  if $ENV{VERBOSE};
    return 'error' if $ENV{QUIET};
    return 'trace';
}

#pod =item os_category
#pod
#pod Not to be confused with L<Log::Any categories|Log::Any/CATEGORIES>,
#pod this is used to categorize log entries in the unified log. However, just
#pod to keep things simple, it defaults to the name of the category used by
#pod L<Log::Any>.
#pod
#pod =item private
#pod
#pod Optional, defaults to false. A Boolean value indicating whether logged
#pod messages should be redacted in the macOS unified logging system.
#pod
#pod =back
#pod
#pod =cut

sub init {
    my $self = shift;
    $self->{os_category} ||= $self->{category};
    $self->{private}     ||= 0;
    $self->{log_level}
        ||= $self->{min_level} || $self->{level} || $self->_min_level;

    # TODO: extract this into a Regexp::Common module
    ## no critic (RegularExpressions::ProhibitComplexRegexes, RegularExpressions::RequireDotMatchAnything, RegularExpressions::RequireLineBoundaryMatching)
    croak 'subsystem must be reversed FQDN'
        if not defined $self->{subsystem}
        and $self->{subsystem} !~ m{^
            (?= .{1,255} $)     # entire string length max 255 chars
            (?:                     # first segment is either
                  [[:alpha:]]{2,63}     # plain
                | xn--                  # or Punycode prefix followed by
                  [[:alnum:]-]{1,59}    # remaining Punycode octets
            )
            (?:                     # subsequent segments
                [.]                     # dot separator
                (?:                     # followed by either
                      [[:alpha:]_]          # plain, can't start with hyphen
                      [[:alnum:]_-]{0,62}
                    | xn--                  # or Punycode prefix followed by
                      [[:alnum:]-]{1,59}    # remaining Punycode octets
                )
            )+                      # repeat one or more times
        $}ix;

    $self->{_os_log}
        //= _os_log_create( @{$self}{qw(subsystem os_category)} );

    return;
}

#pod =head2 L<Log::Any> methods
#pod
#pod The following L<Log::Any> methods are mapped to macOS L<os_log(3)>
#pod functions as follows:
#pod
#pod =over
#pod
#pod =item * trace: L<os_log_debug(3)>
#pod
#pod =item * debug: L<os_log_debug(3)>
#pod
#pod =item * info (or inform): L<os_log_info(3)>
#pod
#pod =item * notice: L<os_log_info(3)>
#pod
#pod =item * warning: L<os_log_fault(3)>
#pod
#pod =item * error (or err): L<os_log_error(3)>
#pod
#pod =for stopwords crit
#pod
#pod =item * critical (or crit or fatal): L<os_log(3)>
#pod
#pod =item * alert: L<os_log(3)>
#pod
#pod =item * emergency: L<os_log(3)>
#pod
#pod =back
#pod
#pod Formatted methods like C<infof>, C<errorf>, etc., are supported via
#pod L<Log::Any>'s standard interface.
#pod
#pod =cut

my @OS_LOG_LEVEL_MAP = qw(
    os_log_debug
    os_log_debug
    os_log_info
    os_log_info
    os_log_fault
    os_log_error
    os_log_default
    os_log_default
    os_log_default
);

# attach each wrapper function
my %UNIQUE_OS_LOG = map { $_ => 1 } @OS_LOG_LEVEL_MAP;
foreach my $function ( keys %UNIQUE_OS_LOG ) {
    for my $variant (qw(public private)) {
        my $name = "${function}_$variant";
        $ffi->attach(
            [ $name => "_$name" ] => [ 'opaque', 'string' ],
            'void',
        );
    }
}

foreach my $method ( keys %LOGGING_LEVELS ) {
    my $log_level            = $LOGGING_LEVELS{$method};
    my $os_log_function_name = $OS_LOG_LEVEL_MAP[$log_level]
        // 'os_log_error';

    make_method(
        $method,
        sub {
            my $self = shift;
            return
                if $log_level < $LOGGING_LEVELS{ $self->{log_level} };

            no strict 'refs';    ## no critic (TestingAndDebugging::ProhibitNoStrict)
            &{ "_${os_log_function_name}_"
                    . ( $self->{private} ? 'private' : 'public' ) }
                ( $self->{_os_log}, join q{}, @_ );
        } );
}

foreach my $method ( detection_methods() ) {
    my $level = $method =~ s/^ is_ //rx;    ## no critic (RegularExpressions::RequireDotMatchAnything, RegularExpressions::RequireLineBoundaryMatching)
    make_method(
        $method,
        sub {
            my $self = shift;

            return $LOGGING_LEVELS{$level}
                >= $LOGGING_LEVELS{ $self->{log_level} };
        } );
}

sub _find_my_bundle {
    my @module_parts = split / :: /x, __PACKAGE__;    ## no critic (RegularExpressions::RequireDotMatchAnything, RegularExpressions::RequireLineBoundaryMatching)

    my $module_pm = $INC{ join( q{/}, @module_parts ) . '.pm' }
        or croak 'Cannot find module in @INC for ' . __PACKAGE__;    ## no critic (ValuesAndExpressions::RequireInterpolationOfMetachars)
    my $module_dir = dirname($module_pm);

    my $auto_path
        = catfile( 'auto', @module_parts, "$module_parts[-1].bundle" );
    my $bundle_path = catfile( $module_dir,   $auto_path );
    my $blib_path   = catfile( qw(blib arch), $auto_path );

    return -e $blib_path ? $blib_path : $bundle_path;
}

#pod =head1 DIAGNOSTICS
#pod
#pod Using this adapter without specifying a properly-formatted C<subsystem>
#pod argument will throw an exception.
#pod
#pod =head1 CONFIGURATION AND ENVIRONMENT
#pod
#pod Configure the same as L<Log::Any>.
#pod
#pod The following environment variables can set the logging level if no
#pod level is set on the adapter itself:
#pod
#pod =over
#pod
#pod =item * C<TRACE> sets the minimum level to B<trace>
#pod
#pod =item * C<DEBUG> sets the minimum level to B<debug>
#pod
#pod =item * C<VERBOSE> sets the minimum level to B<info>
#pod
#pod =item * C<QUIET> sets the minimum level to B<error>
#pod
#pod =back
#pod
#pod In addition, the C<LOG_LEVEL> environment variable may be set to a
#pod string indicating the desired logging level.
#pod
#pod =head1 DEPENDENCIES
#pod
#pod =over
#pod
#pod =item * L<Log::Any::Adapter::Base>
#pod
#pod =cut

use parent qw(Log::Any::Adapter::Base);

#pod =item * L<Log::Any::Adapter::Util>
#pod
#pod =cut

use Log::Any::Adapter::Util qw(
    detection_methods
    log_level_aliases
    logging_methods
    make_method
    numeric_level
);

#pod =item * L<FFI::Platypus> 2.00 or greater
#pod
#pod =cut

use FFI::Platypus 2.00;

#pod =item * L<namespace::autoclean>
#pod
#pod =cut

use namespace::autoclean;

#pod =back
#pod
#pod =cut

1;

__END__

=pod

=encoding UTF-8

=head1 NAME

Log::Any::Adapter::MacOS::OSLog - log to macOS' unified logging system

=head1 VERSION

version 0.0.5

=head1 SYNOPSIS

    use Log::Any::Adapter ('MacOS::OSLog',
      subsystem => 'com.example.foo',
    );

    # or

    use Log::Any::Adapter;
    Log::Any::Adapter->set('MacOS::OSLog',
      subsystem => 'org.example.bar',
    );

    # You can override defaults:
    Log::Any::Adapter->set('MacOS::OSLog',
      subsystem   => 'net.example.baz',
      os_category => 'secret',
      private     => 1,
    );

=head1 DESCRIPTION

This L<Log::Any> adapter lets Perl applications log directly to macOS'
L<unified logging system|https://developer.apple.com/documentation/os/logging>
using FFI and C wrappers--no Swift required.

To send log entries easily from the command line, a C<maclog> utility
script is also included in this distribution.

=head1 METHODS

=head2 init

This method is not called directly, but rather is passed named arguments
as a hash when setting a L<Log::Any::Adapter>:

=over

=item subsystem

=for stopwords FQDN

Required. Must be a reversed fully-qualified domain name (FQDN), e.g.,
C<com.example.perl>.

=item log_level, min_level, level

These are all synonymous and strings that set the minimum logging level
for the adapter. Whatever level is set, messages for that level and
above will be logged.

Defaults to C<trace> and is affected by various
L<environment variables|/"CONFIGURATION AND ENVIRONMENT">.

=item os_category

Not to be confused with L<Log::Any categories|Log::Any/CATEGORIES>,
this is used to categorize log entries in the unified log. However, just
to keep things simple, it defaults to the name of the category used by
L<Log::Any>.

=item private

Optional, defaults to false. A Boolean value indicating whether logged
messages should be redacted in the macOS unified logging system.

=back

=head2 L<Log::Any> methods

The following L<Log::Any> methods are mapped to macOS L<os_log(3)>
functions as follows:

=over

=item * trace: L<os_log_debug(3)>

=item * debug: L<os_log_debug(3)>

=item * info (or inform): L<os_log_info(3)>

=item * notice: L<os_log_info(3)>

=item * warning: L<os_log_fault(3)>

=item * error (or err): L<os_log_error(3)>

=for stopwords crit

=item * critical (or crit or fatal): L<os_log(3)>

=item * alert: L<os_log(3)>

=item * emergency: L<os_log(3)>

=back

Formatted methods like C<infof>, C<errorf>, etc., are supported via
L<Log::Any>'s standard interface.

=head1 DIAGNOSTICS

Using this adapter without specifying a properly-formatted C<subsystem>
argument will throw an exception.

=head1 CONFIGURATION AND ENVIRONMENT

Configure the same as L<Log::Any>.

The following environment variables can set the logging level if no
level is set on the adapter itself:

=over

=item * C<TRACE> sets the minimum level to B<trace>

=item * C<DEBUG> sets the minimum level to B<debug>

=item * C<VERBOSE> sets the minimum level to B<info>

=item * C<QUIET> sets the minimum level to B<error>

=back

In addition, the C<LOG_LEVEL> environment variable may be set to a
string indicating the desired logging level.

=head1 DEPENDENCIES

=over

=item * L<Log::Any::Adapter::Base>

=item * L<Log::Any::Adapter::Util>

=item * L<FFI::Platypus> 2.00 or greater

=item * L<namespace::autoclean>

=back

=head1 INCOMPATIBILITIES

Because this module relies on the macOS unified logging system
introduced in macOS Sierra version 10.12, it is incompatible with
earlier versions of OS X, Mac OS X, the classic Mac OS, and all other
non-Apple platforms (Microsoft Windows, Linux, other Unixes, etc.).

=for stopwords iPadOS tvOS watchOS

It could conceivably be built and run on Apple iOS, iPadOS, tvOS, and
watchOS, but you'd have to build and deploy a native version of Perl
itself on those systems.

=head1 BUGS AND LIMITATIONS

Undoubtedly. Open an issue in the tracker.

=head1 SEE ALSO

=over

=item * C<maclog>, a utility script included in this distribution,
        used to send log entries easily from the command line or other
        scripts

=item * For a full write-up on the rationale, implementation, and
        integration details, see
        L<the blog post|https://phoenixtrap.com/2025/08/10/perl-macos-oslog>.

=item * Apple's L<unified logging system developer documentation|https://developer.apple.com/documentation/os/logging>

=for stopwords OSLog

=item * Apple's L<OSLog developer documentation|https://developer.apple.com/documentation/OSLog>

=for stopwords explainer

=item * The Eclectic Light Company's
        L<explainer on macOS logs|https://eclecticlight.co/2021/09/27/explainer-logs/>

=item * The Eclectic Light Company's
        L<explainer on macOS subsystem identifiers|https://eclecticlight.co/2022/08/27/explainer-subsystems/>

=back

=for :stopwords cpan testmatrix url bugtracker rt cpants kwalitee diff irc mailto metadata placeholders metacpan

=head1 SUPPORT

=head2 Perldoc

You can find documentation for this module with the perldoc command.

  perldoc Log::Any::Adapter::MacOS::OSLog

=head2 Websites

The following websites have more information about this module, and may be of help to you. As always,
in addition to those websites please use your favorite search engine to discover more resources.

=over 4

=item *

MetaCPAN

A modern, open-source CPAN search engine, useful to view POD in HTML format.

L<https://metacpan.org/release/Log-Any-Adapter-MacOS-OSLog>

=item *

RT: CPAN's Bug Tracker

The RT ( Request Tracker ) website is the default bug/issue tracking system for CPAN.

L<https://rt.cpan.org/Public/Dist/Display.html?Name=Log-Any-Adapter-MacOS-OSLog>

=item *

CPANTS

The CPANTS is a website that analyzes the Kwalitee ( code metrics ) of a distribution.

L<http://cpants.cpanauthors.org/dist/Log-Any-Adapter-MacOS-OSLog>

=item *

CPAN Testers

The CPAN Testers is a network of smoke testers who run automated tests on uploaded CPAN distributions.

L<http://www.cpantesters.org/distro/L/Log-Any-Adapter-MacOS-OSLog>

=item *

CPAN Testers Matrix

The CPAN Testers Matrix is a website that provides a visual overview of the test results for a distribution on various Perls/platforms.

L<http://matrix.cpantesters.org/?dist=Log-Any-Adapter-MacOS-OSLog>

=item *

CPAN Testers Dependencies

The CPAN Testers Dependencies is a website that shows a chart of the test results of all dependencies for a distribution.

L<http://deps.cpantesters.org/?module=Log::Any::Adapter::MacOS::OSLog>

=back

=head2 Bugs / Feature Requests

Please report any bugs or feature requests through the web interface at L<https://codeberg.org/mjgardner/perl-Log-Any-Adapter-MacOS-OSLog/issues>.

=head2 Source Code

The code is open to the world, and available for you to hack on. Please feel free to browse it and play
with it, or whatever. If you want to contribute patches, please send me a diff or prod me to pull
from your repository :)

L<https://codeberg.org/mjgardner/perl-Log-Any-Adapter-MacOS-OSLog>

  git clone https://codeberg.org/mjgardner/perl-Log-Any-Adapter-MacOS-OSLog.git

=head1 AUTHOR

Mark Gardner <mjgardner@cpan.org>

=head1 COPYRIGHT AND LICENSE

This software is copyright (c) 2025 by Mark Gardner.

This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.

=cut
