PageRenderTime 56ms CodeModel.GetById 25ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/Log/Log4perl/Config/LDAPConfigurator.pm

http://github.com/mschilli/log4perl
Perl | 790 lines | 189 code | 114 blank | 487 comment | 20 complexity | 727683f6b49e4efb32c5929eb8e1bece MD5 | raw file
  1. #NOTE: this LDAP stuff is experimental, in progress,
  2. #not meant to be used for ANYTHING yet --kg 5/2004
  3. package Log::Log4perl::Config::LDAPConfigurator;
  4. use Log::Log4perl::Config::BaseConfigurator;
  5. our @ISA = qw(Log::Log4perl::Config::BaseConfigurator);
  6. use Net::LDAP;
  7. use URI;
  8. use Log::Log4perl::Level;
  9. use Data::Dump qw(dump); #DEBUG
  10. use Carp;
  11. use strict;
  12. use constant _INTERNAL_DEBUG => 1;
  13. our $VERSION = 0.01;
  14. #poor man's export
  15. *eval_if_perl = \&Log::Log4perl::Config::eval_if_perl;
  16. *unlog4j = \&Log::Log4perl::Config::unlog4j;
  17. ############################################
  18. sub parse {
  19. ############################################
  20. my($self, $newtext) = @_;
  21. $self->text($newtext) if defined $newtext;
  22. my $uri = $self->{text}->[0];
  23. $uri =
  24. URI->new($uri); #ldap://localhost/dc=testsystem,dc=log4perl,dc=goess,dc=org??sub?
  25. my ($userdn, $passwd, $ldap, $mesg, $host, $base, $searchString, $scope);
  26. $userdn = $ENV{LOG4PERL_LDAP_USERDN}; #cn=log4perluser,dc=people,dc=goess,dc=org
  27. $passwd = $ENV{LOG4PERL_LDAP_PWD}; #note not very secure
  28. #http://www.faqs.org/rfcs/rfc2255.html
  29. # Some authentication methods, in particular reusable passwords sent to
  30. # the server, may reveal easily-abused information to the remote server
  31. # or to eavesdroppers in transit, and should not be used in URL
  32. # processing unless explicitly permitted by policy.
  33. eval {
  34. #DEBUG need to check for presence of necessary strings
  35. $host = $uri->host; #localhost
  36. $base = $uri->dn; #"dc=testsystem,dc=log4perl,dc=goess,dc=org" ;
  37. $searchString = $uri->filter;#"(objectclass=*)";
  38. $scope = $uri->scope || 'sub';
  39. $ldap = Net::LDAP->new ( "localhost" ) or die "$@";
  40. $mesg = $ldap->bind ( $userdn,
  41. ($passwd ? (password => $passwd) : (noauth => 1)),
  42. version => 3 );
  43. #DEBUG, check authentication failures
  44. if ( $mesg->code ) {
  45. die $mesg->error;
  46. }
  47. };
  48. if ($@) {
  49. die "log4perl: LDAP unable to bind to $uri:\n$@";
  50. }
  51. my $result = $ldap->search ( base => $base,
  52. scope => $scope,
  53. filter => $searchString, #"sn=*"
  54. attrs => [] , #arrayref
  55. );
  56. #DEBUG where are errors here?
  57. #if ($result->code) {
  58. # die $mesg->error;
  59. #}
  60. my $l4p_tree = {};
  61. foreach my $entry ($result->all_entries){
  62. my $objectclasses = join("\n",$entry->get_value('objectclass'));
  63. if ($objectclasses =~ /^log4(perl|j)Appender/m) { #DEBUG, make a constant?
  64. parse_appender($l4p_tree, $entry);
  65. }elsif ($objectclasses =~ /^log4(perl|j)Layout/m) {
  66. parse_layout($l4p_tree, $entry);
  67. }elsif ($objectclasses =~ /^log4(perl|j)RootLogger/m) {
  68. my $level = $entry->get_value("log4perlLevel");
  69. my @appenders = $entry->get_value("log4perlAppenderName");
  70. map {s/ +$//} @appenders;#ldap preserves trailing spaces :-(
  71. $l4p_tree->{category}{value} = $level;
  72. $l4p_tree->{category}{value} .= ", ".join(',',@appenders);
  73. }elsif ($objectclasses =~ /^log4(perl|j)(Logger|Category)/m) {
  74. parse_category($l4p_tree, $entry);
  75. }else{
  76. ;#?
  77. }
  78. }
  79. #my $href = $result->as_struct;
  80. ##DEBUG, keys don't seem to be normalized?
  81. #dump $href;
  82. $mesg = $ldap->unbind; # take down session
  83. return $l4p_tree;
  84. }
  85. sub parse_appender {
  86. my ($l4p_tree, $entry) = @_;
  87. #foreach my $attr ($entry->attributes) {
  88. # print STDERR join( "\n ", $attr, $entry->get_value( $attr ) ), "\n";
  89. #}
  90. my $name = subst($entry->get_value("name"));
  91. my $l4p_branch = {};
  92. my $class = subst($entry->get_value("log4perlclass")); #DEBUG: handle log4j
  93. $l4p_branch->{value} = $class;
  94. print "looking at $name----------------------\n" if _INTERNAL_DEBUG;
  95. $l4p_tree->{appender}{$name} = $l4p_branch;
  96. my ($attrname, $attrvalue);
  97. foreach ($entry->attributes) {
  98. /^objectclass$/i && next;
  99. /^log4perlclass$/i && next; #already handled
  100. #a gross kind of translation?
  101. /^log4perl/ && do {
  102. ($attrname = $_) =~ s/log4perl//;
  103. $attrvalue = $entry->get_attribute($_);
  104. #note the '->[0]', log4perl doesn't
  105. #accept any multi-valued attributes, does it?
  106. #DEBUG
  107. $attrvalue = $attrvalue->[0] if (ref $attrvalue eq 'ARRAY');
  108. if (lc $attrvalue =~ /true/) {
  109. $attrvalue = 1;
  110. }elsif (lc $attrvalue eq 'false'){
  111. $attrvalue = 0;
  112. }
  113. };
  114. next unless $attrname && defined $attrvalue;
  115. $l4p_branch->{$attrname} = {value => $attrvalue};
  116. }
  117. }
  118. sub parse_layout {
  119. my ($l4ptree, $entry) = @_;
  120. my $layout_tree = {};
  121. my $class_name = subst($entry->get_value("log4perlLayoutClass"));
  122. my $dn = $entry->dn;
  123. $dn =~ /^name=layout,name=(.+?),/
  124. or do {
  125. warn "layout object at $dn doesn't seem to be in a spot wanting a layout, ",
  126. "like 'name=layout,name=<some appender>";
  127. return;
  128. };
  129. my $appender_name = $1;
  130. $l4ptree->{appender}{$appender_name}{layout} = $layout_tree;
  131. $layout_tree->{value} = $class_name;
  132. foreach ($entry->attributes) {
  133. /^objectclass$/i && next;
  134. /^name$/i && next; #is always 'layout'
  135. /^log4perlLayoutClass$/ && next; #already handled
  136. /^log4perlConversionPattern$/ && do
  137. {
  138. $layout_tree->{ConversionPattern}
  139. = {value => $entry->get_attribute($_)};
  140. next;
  141. };
  142. }
  143. return $layout_tree;
  144. }
  145. sub parse_category {
  146. my ($l4p_tree, $entry) = @_;
  147. my $category_name = subst($entry->get_value("log4perlCategoryName"));
  148. print STDERR "parsing category $category_name\n";
  149. $l4p_tree->{category} ||= {};
  150. my $ptr = $l4p_tree->{category};
  151. for my $part (split /\.|::/, $category_name) {
  152. $ptr->{$part} = {} unless exists $ptr->{$part};
  153. $ptr = $ptr->{$part};
  154. }
  155. my $l4p_branch = $ptr;
  156. my $level = $entry->get_value("log4perlLevel");
  157. my @appenders = $entry->get_value("log4perlAppenderName");
  158. map {s/ +$//} @appenders;#ldap preserves trailing spaces :-(
  159. $l4p_branch->{value} = $level;
  160. $l4p_branch->{value} .= ", ".join(',',@appenders);
  161. return $l4p_tree;
  162. }
  163. #all other code below is still from DOMConfigurator (kg 5/12)
  164. =pod
  165. my $parser = $PARSER_CLASS->new;
  166. my $doc = $parser->parse (join('',@$text));
  167. my $l4p_tree = {};
  168. my $config = $doc->getElementsByTagName("$LOG4J_PREFIX:configuration")->item(0)||
  169. $doc->getElementsByTagName("$LOG4PERL_PREFIX:configuration")->item(0);
  170. my $threshold = uc(subst($config->getAttribute('threshold')));
  171. if ($threshold) {
  172. $l4p_tree->{threshold}{value} = $threshold;
  173. }
  174. if (subst($config->getAttribute('oneMessagePerAppender')) eq 'true') {
  175. $l4p_tree->{oneMessagePerAppender}{value} = 1;
  176. }
  177. for my $kid ($config->getChildNodes){
  178. next unless $kid->getNodeType == ELEMENT_NODE;
  179. my $tag_name = $kid->getTagName;
  180. if ($tag_name =~ $APPENDER_TAG) {
  181. &parse_appender($l4p_tree, $kid);
  182. }elsif ($tag_name eq 'category' || $tag_name eq 'logger'){
  183. &parse_category($l4p_tree, $kid);
  184. #Treating them the same is not entirely accurate,
  185. #the dtd says 'logger' doesn't accept
  186. #a 'class' attribute while 'category' does.
  187. #But that's ok, log4perl doesn't do anything with that attribute
  188. }elsif ($tag_name eq 'root'){
  189. &parse_root($l4p_tree, $kid);
  190. }elsif ($tag_name =~ $FILTER_TAG){
  191. #parse log4perl's chainable boolean filters
  192. &parse_l4p_filter($l4p_tree, $kid);
  193. }elsif ($tag_name eq 'renderer'){
  194. warn "Log4perl: ignoring renderer tag in config, unimplemented";
  195. #"log4j will render the content of the log message according to
  196. # user specified criteria. For example, if you frequently need
  197. # to log Oranges, an object type used in your current project,
  198. # then you can register an OrangeRenderer that will be invoked
  199. # whenever an orange needs to be logged. "
  200. }elsif ($tag_name eq 'PatternLayout'){#log4perl only
  201. &parse_patternlayout($l4p_tree, $kid);
  202. }
  203. }
  204. $doc->dispose;
  205. return $l4p_tree;
  206. }
  207. #this is just for toplevel log4perl.PatternLayout tags
  208. #holding the custome cspecs
  209. sub parse_patternlayout {
  210. my ($l4p_tree, $node) = @_;
  211. my $l4p_branch = {};
  212. for my $child ($node->getChildNodes) {
  213. next unless $child->getNodeType == ELEMENT_NODE;
  214. my $name = subst($child->getAttribute('name'));
  215. my $value;
  216. foreach my $grandkid ($child->getChildNodes){
  217. if ($grandkid->getNodeType == TEXT_NODE) {
  218. $value .= $grandkid->getData;
  219. }
  220. }
  221. $value =~ s/^ +//; #just to make the unit tests pass
  222. $value =~ s/ +$//;
  223. $l4p_branch->{$name}{value} = subst($value);
  224. }
  225. $l4p_tree->{PatternLayout}{cspec} = $l4p_branch;
  226. }
  227. #for parsing the root logger, if any
  228. sub parse_root {
  229. my ($l4p_tree, $node) = @_;
  230. my $l4p_branch = {};
  231. &parse_children_of_logger_element($l4p_branch, $node);
  232. $l4p_tree->{category}{value} = $l4p_branch->{value};
  233. }
  234. #this parses a custom log4perl-specific filter set up under
  235. #the root element, as opposed to children of the appenders
  236. sub parse_l4p_filter {
  237. my ($l4p_tree, $node) = @_;
  238. my $l4p_branch = {};
  239. my $name = subst($node->getAttribute('name'));
  240. my $class = subst($node->getAttribute('class'));
  241. my $value = subst($node->getAttribute('value'));
  242. if ($class && $value) {
  243. die "Log4perl: only one of class or value allowed, not both, "
  244. ."in XMLConfig filter '$name'";
  245. }elsif ($class || $value){
  246. $l4p_branch->{value} = ($value || $class);
  247. }
  248. for my $child ($node->getChildNodes) {
  249. if ($child->getNodeType == ELEMENT_NODE){
  250. my $tag_name = $child->getTagName();
  251. if ($tag_name =~ /^(param|param-nested|param-text)$/) {
  252. &parse_any_param($l4p_branch, $child);
  253. }
  254. }elsif ($child->getNodeType == TEXT_NODE){
  255. my $text = $child->getData;
  256. next unless $text =~ /\S/;
  257. if ($class && $value) {
  258. die "Log4perl: only one of class, value or PCDATA allowed, "
  259. ."in XMLConfig filter '$name'";
  260. }
  261. $l4p_branch->{value} .= subst($text);
  262. }
  263. }
  264. $l4p_tree->{filter}{$name} = $l4p_branch;
  265. }
  266. #for parsing a category/logger element
  267. sub parse_category {
  268. my ($l4p_tree, $node) = @_;
  269. my $name = subst($node->getAttribute('name'));
  270. $l4p_tree->{category} ||= {};
  271. my $ptr = $l4p_tree->{category};
  272. for my $part (split /\.|::/, $name) {
  273. $ptr->{$part} = {} unless exists $ptr->{$part};
  274. $ptr = $ptr->{$part};
  275. }
  276. my $l4p_branch = $ptr;
  277. my $class = subst($node->getAttribute('class'));
  278. $class &&
  279. $class ne 'Log::Log4perl' &&
  280. $class ne 'org.apache.log4j.Logger' &&
  281. warn "setting category $name to class $class ignored, only Log::Log4perl implemented";
  282. #this is kind of funky, additivity has its own spot in the tree
  283. my $additivity = subst(subst($node->getAttribute('additivity')));
  284. if (length $additivity > 0) {
  285. $l4p_tree->{additivity} ||= {};
  286. my $add_ptr = $l4p_tree->{additivity};
  287. for my $part (split /\.|::/, $name) {
  288. $add_ptr->{$part} = {} unless exists $add_ptr->{$part};
  289. $add_ptr = $add_ptr->{$part};
  290. }
  291. $add_ptr->{value} = &parse_boolean($additivity);
  292. }
  293. &parse_children_of_logger_element($l4p_branch, $node);
  294. }
  295. # parses the children of a category element
  296. sub parse_children_of_logger_element {
  297. my ($l4p_branch, $node) = @_;
  298. my (@appenders, $priority);
  299. for my $child ($node->getChildNodes) {
  300. next unless $child->getNodeType == ELEMENT_NODE;
  301. my $tag_name = $child->getTagName();
  302. if ($tag_name eq 'param') {
  303. my $name = subst($child->getAttribute('name'));
  304. my $value = subst($child->getAttribute('value'));
  305. if ($value =~ /^(all|debug|info|warn|error|fatal|off|null)^/) {
  306. $value = uc $value;
  307. }
  308. $l4p_branch->{$name} = {value => $value};
  309. }elsif ($tag_name eq 'appender-ref'){
  310. push @appenders, subst($child->getAttribute('ref'));
  311. }elsif ($tag_name eq 'level' || $tag_name eq 'priority'){
  312. $priority = &parse_level($child);
  313. }
  314. }
  315. $l4p_branch->{value} = $priority.', '.join(',', @appenders);
  316. return;
  317. }
  318. sub parse_level {
  319. my $node = shift;
  320. my $level = uc (subst($node->getAttribute('value')));
  321. die "Log4perl: invalid level in config: $level"
  322. unless Log::Log4perl::Level::is_valid($level);
  323. return $level;
  324. }
  325. sub parse_appender {
  326. my ($l4p_tree, $node) = @_;
  327. my $name = subst($node->getAttribute("name"));
  328. my $l4p_branch = {};
  329. my $class = subst($node->getAttribute("class"));
  330. $l4p_branch->{value} = $class;
  331. print "looking at $name----------------------\n" if _INTERNAL_DEBUG;
  332. for my $child ($node->getChildNodes) {
  333. next unless $child->getNodeType == ELEMENT_NODE;
  334. my $tag_name = $child->getTagName();
  335. my $name = unlog4j(subst($child->getAttribute('name')));
  336. if ($tag_name =~ /^(param|param-nested|param-text)$/) {
  337. &parse_any_param($l4p_branch, $child);
  338. my $value;
  339. }elsif ($tag_name =~ /($LOG4PERL_PREFIX:)?layout/){
  340. $l4p_branch->{layout} = parse_layout($child);
  341. }elsif ($tag_name =~ $FILTER_TAG){
  342. $l4p_branch->{Filter} = parse_filter($child);
  343. }elsif ($tag_name =~ $FILTER_REF_TAG){
  344. $l4p_branch->{Filter} = parse_filter_ref($child);
  345. }elsif ($tag_name eq 'errorHandler'){
  346. die "errorHandlers not supported yet";
  347. }elsif ($tag_name eq 'appender-ref'){
  348. #dtd: Appenders may also reference (or include) other appenders.
  349. #This feature in log4j is only for appenders who implement the
  350. #AppenderAttachable interface, and the only one that does that
  351. #is the AsyncAppender, which writes logs in a separate thread.
  352. #I don't see the need to support this on the perl side any
  353. #time soon. --kg 3/2003
  354. die "Log4perl: in config file, <appender-ref> tag is unsupported in <appender>";
  355. }else{
  356. die "Log4perl: in config file, <$tag_name> is unsupported\n";
  357. }
  358. }
  359. $l4p_tree->{appender}{$name} = $l4p_branch;
  360. }
  361. sub parse_any_param {
  362. my ($l4p_branch, $child) = @_;
  363. my $tag_name = $child->getTagName();
  364. my $name = subst($child->getAttribute('name'));
  365. my $value;
  366. print "parse_any_param: <$tag_name name=$name\n" if _INTERNAL_DEBUG;
  367. #<param-nested>
  368. #note we don't set it to { value => $value }
  369. #and we don't test for multiple values
  370. if ($tag_name eq 'param-nested'){
  371. if ($l4p_branch->{$name}){
  372. die "Log4perl: in config file, multiple param-nested tags for $name not supported";
  373. }
  374. $l4p_branch->{$name} = &parse_param_nested($child);
  375. return;
  376. #<param>
  377. }elsif ($tag_name eq 'param') {
  378. $value = subst($child->getAttribute('value'));
  379. print "parse_param_nested: got param $name = $value\n"
  380. if _INTERNAL_DEBUG;
  381. if ($value =~ /^(all|debug|info|warn|error|fatal|off|null)$/) {
  382. $value = uc $value;
  383. }
  384. if ($name !~ /warp_message|filter/ &&
  385. $child->getParentNode->getAttribute('name') ne 'cspec') {
  386. $value = eval_if_perl($value);
  387. }
  388. #<param-text>
  389. }elsif ($tag_name eq 'param-text'){
  390. foreach my $grandkid ($child->getChildNodes){
  391. if ($grandkid->getNodeType == TEXT_NODE) {
  392. $value .= $grandkid->getData;
  393. }
  394. }
  395. if ($name !~ /warp_message|filter/ &&
  396. $child->getParentNode->getAttribute('name') ne 'cspec') {
  397. $value = eval_if_perl($value);
  398. }
  399. }
  400. $value = subst($value);
  401. #multiple values for the same param name
  402. if (defined $l4p_branch->{$name}{value} ) {
  403. if (ref $l4p_branch->{$name}{value} ne 'ARRAY'){
  404. my $temp = $l4p_branch->{$name}{value};
  405. $l4p_branch->{$name}{value} = [$temp];
  406. }
  407. push @{$l4p_branch->{$name}{value}}, $value;
  408. }else{
  409. $l4p_branch->{$name} = {value => $value};
  410. }
  411. }
  412. #handles an appender's <param-nested> elements
  413. sub parse_param_nested {
  414. my ($node) = shift;
  415. my $l4p_branch = {};
  416. for my $child ($node->getChildNodes) {
  417. next unless $child->getNodeType == ELEMENT_NODE;
  418. my $tag_name = $child->getTagName();
  419. if ($tag_name =~ /^param|param-nested|param-text$/) {
  420. &parse_any_param($l4p_branch, $child);
  421. }
  422. }
  423. return $l4p_branch;
  424. }
  425. #this handles filters that are children of appenders, as opposed
  426. #to the custom filters that go under the root element
  427. sub parse_filter {
  428. my $node = shift;
  429. my $filter_tree = {};
  430. my $class_name = subst($node->getAttribute('class'));
  431. $filter_tree->{value} = $class_name;
  432. print "\tparsing filter on class $class_name\n" if _INTERNAL_DEBUG;
  433. for my $child ($node->getChildNodes) {
  434. next unless $child->getNodeType == ELEMENT_NODE;
  435. my $tag_name = $child->getTagName();
  436. if ($tag_name =~ 'param|param-nested|param-text') {
  437. &parse_any_param($filter_tree, $child);
  438. }else{
  439. die "Log4perl: don't know what to do with a ".$child->getTagName()
  440. ."inside a filter element";
  441. }
  442. }
  443. return $filter_tree;
  444. }
  445. sub parse_filter_ref {
  446. my $node = shift;
  447. my $filter_tree = {};
  448. my $filter_id = subst($node->getAttribute('id'));
  449. $filter_tree->{value} = $filter_id;
  450. return $filter_tree;
  451. }
  452. sub parse_layout {
  453. my $node = shift;
  454. my $layout_tree = {};
  455. my $class_name = subst($node->getAttribute('class'));
  456. $layout_tree->{value} = $class_name;
  457. #
  458. print "\tparsing layout $class_name\n" if _INTERNAL_DEBUG;
  459. for my $child ($node->getChildNodes) {
  460. next unless $child->getNodeType == ELEMENT_NODE;
  461. if ($child->getTagName() eq 'param') {
  462. my $name = subst($child->getAttribute('name'));
  463. my $value = subst($child->getAttribute('value'));
  464. if ($value =~ /^(all|debug|info|warn|error|fatal|off|null)$/) {
  465. $value = uc $value;
  466. }
  467. print "\tparse_layout: got param $name = $value\n"
  468. if _INTERNAL_DEBUG;
  469. $layout_tree->{$name}{value} = $value;
  470. }elsif ($child->getTagName() eq 'cspec') {
  471. my $name = subst($child->getAttribute('name'));
  472. my $value;
  473. foreach my $grandkid ($child->getChildNodes){
  474. if ($grandkid->getNodeType == TEXT_NODE) {
  475. $value .= $grandkid->getData;
  476. }
  477. }
  478. $value =~ s/^ +//;
  479. $value =~ s/ +$//;
  480. $layout_tree->{cspec}{$name}{value} = subst($value);
  481. }
  482. }
  483. return $layout_tree;
  484. }
  485. sub parse_boolean {
  486. my $a = shift;
  487. if ($a eq '0' || lc $a eq 'false') {
  488. return '0';
  489. }elsif ($a eq '1' || lc $a eq 'true'){
  490. return '1';
  491. }else{
  492. return $a; #probably an error, punt
  493. }
  494. }
  495. =cut
  496. #this handles variable substitution
  497. sub subst {
  498. my $val = shift;
  499. $val =~ s/\${(.*?)}/
  500. Log::Log4perl::Config::var_subst($1, {})/gex;
  501. return $val;
  502. }
  503. 1;
  504. __END__
  505. =encoding utf8
  506. =head1 NAME
  507. Log::Log4perl::Config::LDAPConfigurator - configures log4perl via LDAP
  508. =head1 SYNOPSIS
  509. =head1 DESCRIPTION
  510. This module implements
  511. =head1 WHY
  512. =head1 HOW
  513. =head1 VARIABLE SUBSTITUTION
  514. ???
  515. This supports variable substitution like C<${foobar}> in text and in
  516. attribute values except for appender-ref. If an environment variable is defined
  517. for that name, its value is substituted. So you can do stuff like
  518. <param name="${hostname}" value="${hostnameval}.foo.com"/>
  519. <param-text name="to">${currentsysadmin}@foo.com</param-text>
  520. =head1 REQUIRES
  521. =head1 CAVEATS
  522. =head1 CHANGES
  523. 0.01 2004-05-12 initial version
  524. =head1 SEE ALSO
  525. t/047-ldap.t
  526. ldap/log4perl.schema DEBUG
  527. Log::Log4perl::Config
  528. =head1 LICENSE
  529. Copyright 2002-2013 by Mike Schilli E<lt>m@perlmeister.comE<gt>
  530. and Kevin Goess E<lt>cpan@goess.orgE<gt>.
  531. This library is free software; you can redistribute it and/or modify
  532. it under the same terms as Perl itself.
  533. =head1 AUTHOR
  534. Please contribute patches to the project on Github:
  535. http://github.com/mschilli/log4perl
  536. Send bug reports or requests for enhancements to the authors via our
  537. MAILING LIST (questions, bug reports, suggestions/patches):
  538. log4perl-devel@lists.sourceforge.net
  539. Authors (please contact them via the list above, not directly):
  540. Mike Schilli <m@perlmeister.com>,
  541. Kevin Goess <cpan@goess.org>
  542. Contributors (in alphabetical order):
  543. Ateeq Altaf, Cory Bennett, Jens Berthold, Jeremy Bopp, Hutton
  544. Davidson, Chris R. Donnelly, Matisse Enzer, Hugh Esco, Anthony
  545. Foiani, James FitzGibbon, Carl Franks, Dennis Gregorovic, Andy
  546. Grundman, Paul Harrington, Alexander Hartmaier David Hull,
  547. Robert Jacobson, Jason Kohles, Jeff Macdonald, Markus Peter,
  548. Brett Rann, Peter Rabbitson, Erik Selberg, Aaron Straup Cope,
  549. Lars Thegler, David Viner, Mac Yang.