How can I log the number of documents / bytes I've processed?

Tags: loggingcounteriText 5

I want to log the number of documents I've processed using iText, as well as the number of bytes I've read or produced. Is there a logging mechanism that allows me to do this?

Question asked at an internal meeting at iText Group NV.

Please take a look at the CounterDemoSyso. In this example, we get an instance of the CounterFactory. Simultaneously, we create a new SysoCounter object, which is an implementation of the Counter interface that writes information to the console (System.out):

 CounterFactory.getInstance().setCounter(new SysoCounter());

We can now use ordinary iText code without having to worry about counting bytes or documents; that single line activated the counting mechanism.

For instance, we can create a PDF:

public void createPdf(String filename) throws IOException, DocumentException {
    Document document = new Document();
    PdfWriter.getInstance(document, new FileOutputStream(filename));
    document.open();
    document.add(new Paragraph("Hello World!"));
    document.close();
}

Or we can manipulate a PDF:

public void manipulatePdf(String src, String dest) throws IOException, DocumentException {
    PdfReader reader = new PdfReader(src);
    PdfStamper stamper = new PdfStamper(reader, new FileOutputStream(dest));
    PdfContentByte pagecontent = stamper.getOverContent(1);
    ColumnText.showTextAligned(pagecontent, Element.ALIGN_RIGHT,
                new Phrase("Stamped text"), 559, 806, 0);
    stamper.close();
    reader.close();
}

If we execute createPdf() and manipulatePdf(), the SysoCounter will automatically write the following information to the System.out:

[com.itextpdf.text.pdf.PdfWriter] 915 bytes written
[com.itextpdf.text.pdf.PdfReader] 915 bytes read
[com.itextpdf.text.pdf.PdfStamper] 1380 bytes written

In the first line, we see that PdfWriter wrote 915 bytes (this happened in the createPdf() method). In the second and third line, we see what happened in the manipulatePdf() method: PdfReader has read 915 bytes and we added the text "Stamped text" resulting in 1380 bytes being written by PdfStamper.

This is only a simple example that writes to the System.out. It's not difficult to write some code that stores this information (and more) in another place. Let's take a look at the CounterDemo example. In this example, we write our own implementation of the Counter interface so that information about the processed documents is written to a file.

We need to implement three methods:

  • getCounter(): this method will return an instance of your counter,

  • read(long l): this method will be triggered when a file is read,

  • written(long l): this method will be triggered when a file is written.

We implemented these methods as follows:

  1. public class MyCounter implements Counter {
  2.  
  3. public static final String LOG = "results/logging/counter.txt";
  4. protected FileWriter writer;
  5. protected String yourClass;
  6. protected String iTextClass;
  7.  
  8. public MyCounter(Class<?> klass) throws IOException {
  9. this.yourClass = klass.getName();
  10. writer = new FileWriter(LOG, true);
  11. }
  12.  
  13. private MyCounter(Class<?> klass, String yourClass, FileWriter writer)
  14. throws IOException {
  15. this.yourClass = yourClass;
  16. this.iTextClass = klass.getName();
  17. this.writer = writer;
  18. }
  19.  
  20. public Counter getCounter(Class<?> klass) {
  21. try {
  22. return new MyCounter(klass, yourClass, writer);
  23. } catch (IOException e) {
  24. throw new ExceptionConverter(e);
  25. }
  26. }
  27.  
  28. public void read(long l) {
  29. if (writer == null)
  30. throw new RuntimeException("No writer defined!");
  31. try {
  32. writer.write(String.format(
  33. "[%s:%s] %s: %s read\n",
  34. yourClass, iTextClass, new Date().toString(), l));
  35. writer.flush();
  36. } catch (IOException e) {
  37. throw new ExceptionConverter(e);
  38. }
  39. }
  40.  
  41. public void written(long l) {
  42. if (writer == null)
  43. throw new RuntimeException("No writer defined!");
  44. try {
  45. writer.write(String.format(
  46. "[%s:%s] %s: %s written\n",
  47. yourClass, iTextClass, new Date().toString(), l));
  48. writer.flush();
  49. } catch (IOException e) {
  50. throw new ExceptionConverter(e);
  51. }
  52. }
  53.  
  54. public void close() throws IOException {
  55. writer.close();
  56. }
  57. }

In line 3, we see a path to a log file. Instead of writing to the System.out, we will write to that file using the writer object declared in line 4. The yourClass object declared in line 5 will store the class name of your application that uses the Counter; the iTextClass object declared in line 6 will store the iText class that reads, writes or manipulates the PDF document.

We have a public constructor that initializes the yourClass variable (line 8-11). We use in our own code like this:

public void initCounter() throws IOException {
    counter = new MyCounter(getClass());
    CounterFactory.getInstance().setCounter(counter);
}

The private constructor (line 13-18) will be called by iText internally through the getCounter() method (line 20-26). It initializes the iTextClass variable and passes the yourClass and writer variables as parameters.

When bytes are read (line 28-39), we will write information to the log file that looks like this:

String.format("[%s:%s] %s: %s read\n", yourClass, iTextClass, new Date().toString(), l)

When bytes are written (line 41-52), we will write information to the log file that looks like:

String.format("[%s:%s] %s: %s written\n", yourClass, iTextClass, new Date().toString(), l)

If we execute the same createPdf() and manipulatePdf() methods as before, we get the following result:

[sandbox.logging.CounterDemo:com.itextpdf.text.pdf.PdfWriter] Mon Nov 09 10:42:59 CET 2015: 915 written
[sandbox.logging.CounterDemo:com.itextpdf.text.pdf.PdfReader] Mon Nov 09 10:42:59 CET 2015: 915 read
[sandbox.logging.CounterDemo:com.itextpdf.text.pdf.PdfStamper] Mon Nov 09 10:42:59 CET 2015: 1380 written

As we are writing to a file, we mustn't forget to close the FileWriter (line 54-56) once we've finished reading and writing documents:

public void closeCounter() throws IOException {
    counter.close();
}

This example is provided FYI. In a real-world situation, you typically won't write to a file because writing to disk is a slow operation and it's not a good idea to write to a single file in a multi-threaded environment. Typically, you'll write this information to a database, so that you can output information that looks for instance like this:

Monthapplicationoperationamountsize

2015-09

InvoiceApp

PdfWriter

8451

10479242

2015-09

WatermarkApp

PdfReader

798

518700

2015-09

WatermarkApp

PdfStamper

798

638400

2015-10

InvoiceApp

PdfWriter

8757

10957278

2015-10

WatermarkApp

PdfReader

804

522600

2015-10

WatermarkApp

PdfStamper

804

643200

A possible use case could be to use such a table to monitor the iText use of customers running their custom PDF application in a specific JVM on your servers. You could use these metrics to charge your customers based on the number of PDF documents or the number of PDF bytes they are processing.

Important: the CounterFactory is currently implemented a singleton that stores a single Counter instance. This means that you can only store a single Counter at a time for each JVM. In the above table, we'd have an InvoiceApp running on one JVM and a WatermarkApp on another JVM.

Another use case (that is more realistic) would be to count the use of iText on different JVMs:

MonthJVMoperationamountsize

2015-09

server1

PdfWriter

8451

10479242

2015-09

server2

PdfWriter

8757

10957278

2015-09

server1

PdfReader

798

518700

2015-09

server2

PdfReader

804

522600

2015-09

server1

PdfStamper

798

638400

2015-09

server2

PdfStamper

804

643200