Taille de texte par Defaut
Agrandir la Taille du Texte
Réduire la Taille du Texte
Options d'accessibilité
Informatique

Mail report pour Spring- batch

itk IT
24 juin 2015
Pas de commentaires

 

Lorsque l’on doit développer des traitements d’intégration de données , l’utilisation du projet Spring-Batch est une des solutions possibles.

Bien que le coût d’entrée ne soit pas gratuit, Spring-Batch permet de donner de la rigueur à votre traitement en vous forçant à respecter les phases de lecture / traitement / écriture, communs à tous les traitements et vous fournit out-of-the-box un squelette prêt à l’emploi.

Les batchs étant des traitements sans interaction utilisateur, la problématique de remontée d’informations sur le résultat d’un traitement devient importante. Dois-je me connecter tous les matins en SSH pour consulter mes logs ou alors me connecter à la base de données pour consulter les tables stockant les informations sur l’exécution des steps et des jobs ?

Sauf si vous n’avez rien de mieux à faire, aucune des solutions mentionnées n’est satisfaisante. Personnellement je veux pouvoir disposer d’un système me permettant de remonter de façon automatique le status des traitements !

Cet article vous propose de réaliser cette remontée à travers l’envoi d’un mail à la fin de l’exécution de vos jobs.

JobExecutionListener à la rescousse

JobExecutionListener est une interface permettant d’intervenir dans le cycle de vie d’un Job.

public interface JobExecutionListener {
	void beforeJob(JobExecution jobExecution);
	void afterJob(JobExecution jobExecution);
}

L’idée est donc de créer une implémentation  JobReporter implémentant cette interface et envoyant un mail en fonction du status final du Job.

public class JobReporter implements JobExecutionListener {

    private static final Logger LOG = LoggerFactory.getLogger(JobReporter.class);

    @Autowired
    private MailService mailService;
    @Autowired
    private Templater templater;

    private List<Report> reports;
    private String sender;

    @Override
    public void beforeJob(JobExecution jobExecution) {
    }

    @Override
    public void afterJob(JobExecution jobExecution) {
        if (reports != null && !reports.isEmpty()) {
            for (Report report : reports) {
                if (jobExecution.getStatus().name().equals(report.getStatus())) {
                    HashMap<String, Object> scopes = new HashMap<String, Object>();
                    scopes.put("jobExecution", jobExecution);
                    try {
                        String content =templater.build(report.getTemplate(), scopes);
                        mailService.sendMail(sender, report.getRecipient(), report.getSubject(), content, true);
                    } catch (Exception e) {
                        LOG.error("Job reporter was unable to send an email", e);
                    }
                    break;
                }
            }
        } else {
            LOG.warn("JobReporter has been setup for job {}, but has no reports defined", jobExecution.getJobInstance()
                    .getJobName());
        }
    }

    public void setReports(List<Report> reports) {
        this.reports = reports;
    }

    public void setSender(String sender) {
        this.sender = sender;
    }

}

La responsabilité du JobReporter se limite à vérifier la concordance du status du Job avec les différents Report que l’on souhaite implémenter. Cela permet de définir :

  • Les status où l’on souhaite avoir un report
  • Le sujet du mail en fonction du status
  • Un template spécifique en fonction du status
  • Un destinataire spécifique par status

Pour un exemple de définition de Report, se reporter à La configuration Spring

public class Report {
    private String status;
    private String subject;
    private String recipient;
    private String template;

    // Getters / Setters omitted....
}

La génération du contenu du mail ainsi que l’envoi proprement dit sont délégués respectivement au Templater et au MailService.

Système de template

Pour générer le contenu du mail, c’est Mustache qui a été utilisé mais vous pouvez adapter le fonctionnement à ThymeleafVelocityFreemarker ou tout autre système de templating.

La génération du template est définie au travers de l’interface Templater ce qui permettra de définir les différentes implémentations (Mustache, Thymeleaf, …) dans des projets différents et de linker celle-ci au run-time.

public interface Templater {

    public String build(String template, Map<String, Object> scopes) throws IOException;

}

L’implémentation Mustache, est quand à elle sans difficulté particulière.

@Service
public class MustacheTemplate implements Templater {

    private MustacheFactory mustacheFactory = new DefaultMustacheFactory();

    /* (non-Javadoc)
     * @see com.itkweb.itklabs.loader.common.mail.Templater#build(java.lang.String, java.util.Map)
     */
    @Override
    public String build(String template, Map<String, Object> scopes) throws IOException {
        Mustache mustache = mustacheFactory.compile(template);
        StringWriter writer = new StringWriter();
        mustache.execute(writer, scopes).flush();   
        return writer.toString();
    }
    
}

Un exemple de template

Ceci n’est qu’un exemple pour les Jobs ayant échoué avec Mustache (affichage du Step ayant provoqué l’erreur ainsi que de la stack trace). Reportez-vous à la documentation de Mustache pour plus d’informations.

<h2>Job "{{jobExecution.jobInstance.jobName}}" (id #{{jobExecution.jobInstance.id}}) has returned with status {{jobExecution.exitStatus.exitCode}}</h2>
Start time: {{jobExecution.startTime}}, End time: {{jobExecution.endTime}}<br/>
Parameters:{{jobExecution.jobParameters}}<br/>
Step count: {{jobExecution.stepExecutions.size}}
{{#jobExecution.stepExecutions}}
    <hr/>
	{{#failureExceptions}}
		<br/>
		<h4>Step "{{stepName}}" has returned with status {{exitStatus.exitCode}}</h4>
		<table border="0">
		<tr><td width="15%">Start time:</td><td width="15%">{{startTime}}</td><td width="15%">End time:</td><td width="15%">{{endTime}}</td><td width="15%"></td><td width="15%"></td></tr>
		<tr><td>Read count:</td><td>{{readCount}}</td><td>Write count:</td><td>{{writeCount}}</td><td>Commit count:</td><td>{{commitCount}}</td><td>Rollback count:</td><td>{{rollbackCount}}</td></tr>
		<tr><td>Read skip count:</td><td>{{readSkipCount}}</td><td>Process skip count:</td><td>{{processSkipCount}}</td><td></td><td></td></tr>
		</table><br/>
		<b>Failures:</b><br/>
		{{failureExceptions}}<br/>
		{{#stackTrace}}
			{{toString}}<br/>
		{{/stackTrace}}
		{{#cause}}
		  <b>Cause by:</b><br/>
		  {{toString}}
		{{/cause}}
	{{/failureExceptions}}
{{/jobExecution.stepExecutions}}

L’envoi de mail

L’envoi de mail est délégué au MailService. Les propriétés de ce service sont les suivantes :

  1. host : le nom du serveur d’envoi
  2. port : le port du serveur d’envoi
  3. procotol : le protocol à utiliser (« stmp », « smtps », …)
  4. username : le nom de connexion pour l’envoi d’un mail avec un système à authentification
  5. password : le mot de passe du compte de connexion
  6. mailProperties : chaîne définissant les propriétés supplémentaires à utiliser

Arrêtons nous 2 minutes sur la dernière propriété mailProperties. Suivant l’implémentation utilisée à JavaMailAPI, de nombreuses propriétés sont disponibles pour paramétrer la connexion au serveur de mail (cf com.sun.mail.smtp). Malheureusement ces propriétés sont spécifiques à l’implémentation utilisée : il n’est donc pas possible d’avoir toutes ces propriétés définies dans MailService. L’idée de mailProperties est de pouvoir définir ces propriétés spécifiques sous forme de chaîne avec le format suivant <proprety_key1>=<property_value1>:<property_key2>=<property_value2>. Par exemple « mail.smtps.auth=true:mail.debug=false »

@Service
public class MailService implements InitializingBean {

    private static final String KEY_VALUE_SEPARATOR = "=";
    private static final String PROPERTIES_SEPARATOR = ":";

    private static final Logger LOG = LoggerFactory.getLogger(MailService.class);

    private JavaMailSenderImpl sender;
    private String host;
    private Integer port;
    private String protocol;
    private String username;
    private String password;
    private String mailProperties;

    public void sendMail(final String senderAddress, final String recipient, final String subject,
            final String content, final boolean asHtml) {
        sendMail(senderAddress, recipient, subject, content, asHtml, null);
    }

    public void sendMail(final String senderAddress, final String recipient, final String subject,
            final String content, final boolean asHtml, final String attachmentFilePath) {
        LOG.info("Sending mail from {} to {} with subject {}", senderAddress, recipient, subject);
        MimeMessagePreparator preparator = new MimeMessagePreparator() {
            @Override
            public void prepare(MimeMessage mimeMessage) throws Exception {
                mimeMessage.setRecipient(Message.RecipientType.TO, new InternetAddress(recipient));
                mimeMessage.setFrom(new InternetAddress(senderAddress));
                mimeMessage.setSubject(subject);
                MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true);
                helper.setText(content, asHtml);
                if (attachmentFilePath != null) {
                    File file = new File(attachmentFilePath);
                    if (file != null && file.exists()) {
                        helper.addAttachment(file.getName(), file);
                    }
                }
            }
        };
        sender.send(preparator);
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        sender = new JavaMailSenderImpl();
        sender.setHost(host);
        sender.setPort(port);
        sender.setProtocol(protocol);
        sender.setUsername(username);
        sender.setPassword(password);
        if (mailProperties != null && !mailProperties.isEmpty()) {
            Properties properties = new Properties();
            for (String property : mailProperties.split(PROPERTIES_SEPARATOR)) {
                String[] attrValue = property.split(KEY_VALUE_SEPARATOR);
                properties.put(attrValue[0], attrValue[1]);

            }
            sender.setJavaMailProperties(properties);
        }
    }

    // Getters / Setters omitted
}

La configuration Spring

	<jee:jndi-lookup id="smtpHost" jndi-name="smtp.host"/>
	<jee:jndi-lookup id="smtpPort" jndi-name="smtp.port"/>
	<jee:jndi-lookup id="smtpProtocol" jndi-name="smtp.protocol"/>
	<jee:jndi-lookup id="smtpUsername" jndi-name="smtp.username"/>
	<jee:jndi-lookup id="smtpPassword" jndi-name="smtp.password"/>
	<jee:jndi-lookup id="smtpMail_properties" jndi-name="smtp.mail_properties"/>
	
	<jee:jndi-lookup id="jobReporterTag" jndi-name="job.reporter.tag"/>
	<jee:jndi-lookup id="jobReporterRecipient" jndi-name="job.reporter.recipient"/>
	
	<bean id="mailService" class="com.itkweb.itklabs.loader.common.mail.MailService">
		<property name="host" value="#{smtpHost}"/>
		<property name="port" value="#{smtpPort}"/>
		<property name="protocol" value="#{smtpProtocol}"/>
		<property name="username" value="#{smtpUsername}"/>
		<property name="password" value="#{smtpPassword}"/>
		<property name="mailProperties" value="#{smtpMail_properties}"/>
	</bean>
	
	<bean id="jobReporter" class="com.itkweb.itklabs.loader.common.mail.JobReporter">
	    <property name="sender" value="#{smtpUsername}" />
		<property name="reports">
			<util:list>
				<bean class="com.itkweb.itklabs.loader.common.mail.Report">
					<property name="status" value="COMPLETED"/>
					<property name="recipient" value="#{jobReporterRecipient}"/>
					<property name="subject" value="#{jobReporterTag} Job has COMPLETED"/>
					<property name="template" value="template/job-completed.mst"/>
				</bean>
				<bean class="com.itkweb.itklabs.loader.common.mail.Report">
					<property name="status" value="FAILED"/>
					<property name="recipient" value="#{jobReporterRecipient}"/>
					<property name="subject" value="#{jobReporterTag} Job has FAILED"/>
					<property name="template" value="template/job-failed.mst"/>
				</bean>
			</util:list>
		</property>
	</bean>

On notera au passage que les propriétés ne sont pas directement définies dans le fichier de configuration Spring, bien que cela soit possible, mais au travers de l’environnement JNDI. En effet chacun de nos environnements (test, intégration, production) définit ses propres variables d’environnement. Cela permet par exemple de pouvoir utiliser un tag spécifique dans le sujet du mail afin de bien savoir si c’est un batch en production ou en test qui a planté !

Utilisation dans les jobs

Le JobReporter avec ses dépendances est défini, il ne reste donc plus qu’à relier ce dernier avec les Jobs.

Il est possible de définir pour chaque Job un listener. Cependant si votre application définit de nombreux jobs, cela risque d’être du copier/coller et c’est mal !

Afin de pallier à ce problème il est préférable de définir un Job parent dont tous vos Jobs vont « hériter ».

<batch:job id="reportedJob" abstract="true">
	<batch:listeners>
		<batch:listener ref="jobReporter" />
	</batch:listeners>
</batch:job>


<batch:job id="myJob1" parent="reportedJob">
...
</batch:job>

<batch:job id="myJob2" parent="reportedJob">
...
<//batch:job>

Ainsi tous vos jobs auront le reporting de façon automatique ! Elle est pas belle la vie ?

A noter que le job parent est défini en abstract, sinon Spring provoque une erreur au chargement car aucun step n’est défini !

Le résultat

Voici un extrait du résultat que nous recevons dans nos boites email lorsqu’un job se plante:

Job "myJob1" (id #89) has returned with status FAILED
Start time: Mon Jun 01 09:27:54 CEST 2015, End time: Mon Jun 01 09:27:55 CEST 2015
Parameters:{filename=file:/tmp/filename.csv, time=1433143674626}
Step count: 1

Step "myStep1" has returned with status FAILED
Failures:
[org.springframework.batch.item.ItemStreamException: Failed to initialize the reader]
org.springframework.batch.item.support.AbstractItemCountingItemStreamItemReader.open(AbstractItemCountingItemStreamItemReader.java:147)
sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
...

 

 

Conclusion

Ce système nous a permis de passer d’un système où l’on est aveugle sur le résultat des tâches de fond à un système permettant, de façon rapide et modulable, de nous avertir d’une erreur tout en gardant de la souplesse et de la configuration en fonction de l’environnement d’exécution.

Si vous avez résolu ce type de problématique d’une autre façon, n’hésitez pas à laisser des commentaires.

Commentaires 0

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée.