Remote Logging using CocoaLumberjack, Antenna and DDAntennaLogger

Apr 12, 2015

The classic iOS way of logging debug messages is to use the built in NSLog method.

NSLog(@"Application Started");

The dissadvantage of this method is that when your application is ready for release, you have to manually remove them. Alternatively, you can include them in if or #ifdef/#endif blocks:

if(DEBUG)
    NSLog(@"Application Started");

An improvement to the above method of debugging is using the following DLog/ALog macros:

#ifdef DEBUG
#   define DLog(fmt, ...) NSLog((@"%s [Line %d] " fmt), __PRETTY_FUNCTION__, __LINE__, ##__VA_ARGS__)
#else
#   define DLog(...)
#endif

// ALog always displays output regardless of the DEBUG setting
#define ALog(fmt, ...) NSLog((@"%s [Line %d] " fmt), __PRETTY_FUNCTION__, __LINE__, ##__VA_ARGS__)

Then sprinkle them in your code:

DLog(@"Application Started");

If you need granular debug levels, one of the best options is the logging framework CocoaLumberjack. Using Cocoapods, it’s only one line of code (TM):

pod 'CocoaLumberjack'

Then in your code:

DDLogDebug(@"Will Start Application");
// ...
DDLogInfo(@"Application Started");

Then in your Application.pch or AppDelegate.m you can set the desired log level:

static const int ddLogLevel = DDLogFlagDebug;

One powerfull feature of CocoaLumberjack is that you can define your custom log levels, so you can separate logging of different parts of your project:

[DDLog addLogger:[DDTTYLogger sharedInstance]];

DDLogSync(@"Starting Sync Operation");
DDLogRts(@"Broadcasting Real Time event");

Which then can be enabled or disabled by by setting the corresponding ddLogLevel:

static const int ddLogLevel = DDLogFlagDebug|LOG_FLAG_RTS;

So far, so good. When your friends are testing your app, your logs are saved on the device. If you’d like to access them, you can connect the device to your Mac and explore them in the Window > Devices menu (shift + cmd + 2).

One improuvement to the current setup would be send your logs to the net. For that, we’ll be using two more frameworks Antenna and DDAntennaLogger. Antenna is responsible for shipping the logs to your server, DDAntennaLogger is a custom logger for CocoaLumberjack. Once you plug them together, you’ll be able to have your logs automatically sent to your server.

pod 'CocoaLumberjack'
pod 'Antenna'
pod 'DDAntennaLogger'

Then in your code:

NSURL *logUrl = [NSURL URLWithString:@"http://log.marius.me.uk/log/"];
[[Antenna sharedLogger] addChannelWithURL:logUrl method:@"POST"];
[[Antenna sharedLogger] startLoggingApplicationLifecycleNotifications];

DDAntennaLogger *logger = [[DDAntennaLogger alloc] initWithAntenna:[Antenna sharedLogger]];
[DDLog addLogger:logger];
[DDLog addLogger:[DDTTYLogger sharedInstance]]; // To see them in the Xcode debugger

The server should have this end-point setup. You can save them to file, or db, or forward them to the logger of your choice.

For a node.js based example check out this npm package express-antenna-cocoalumberjack, or check this Ruby Rack middleware rack-http-logger.

npm install express-antenna-cocoalumberjack
export NODE_EXPRESS_ANTENNA_LOG_PATH=/tmp/
node node_modules/express-antenna-cocoalumberjack/app.js
tail -f /tmp/antenna-cocoalumberjack.log

And change the link to the remote logger to:

NSURL *logUrl = [NSURL URLWithString:@"http://yourserver:3205/log/"];
[[Antenna sharedLogger] addChannelWithURL:logUrl method:@"POST"];

There is one more bit to change, switch from http to https.

Now, there are two possible scenarios. The trivial one is when you setup the logger on the same server where you already have a SSL certificate setup (e.g. https://api.yourserver.com) or you own a wildcard SSL certificate. Everything up to this point should work great together.

Now let’s try to use a self signed certificate. Generate a SSL self signed certificate for 10 years:

openssl req -x509 -newkey rsa:2048 -keyout log.marius.me.uk.key \
  -out log.marius.me.uk.cert -days 3560 -nodes

Configure Apache to use that then see what happens.

The other one is when you try to use self-signed certificates, at which point you’ll get the following error message in the logs:

CFNetwork SSLHandshake failed (-9847)
NSURLConnection/CFURLConnection HTTP load failed (kCFStreamErrorDomainSSL, -9847)

Which pretty much means that your setup is not happy with the self-signed certificate.

Because we’re controlling both the server and the client, we’re able to use SSL pinning to overcome this issue. The basic of SSL pinning is we distribute our public key certificate with the application and configure our logging setup to authenticate using this certificate. TODO: try to add some analogies.

Convert the public key certificate to binary DER:

openssl x509 -in log.marius.me.uk.cert -out log.marius.me.uk.der -outform der

Configure your request operation manager to use the public key certificate, which you already dragged in your project:

- (AFSecurityPolicy*)remoteLoggingSecurityPolicy
{
    NSString *cerPath = [[NSBundle mainBundle] pathForResource:@"log.marius.me.uk"
                                                        ofType:@"cer"];
    NSData *certData = [NSData dataWithContentsOfFile:cerPath];
    AFSecurityPolicy *securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModePublicKey];
    [securityPolicy setAllowInvalidCertificates:YES]; // Unfortunate name
    [securityPolicy setPinnedCertificates:@[certData]];
    return securityPolicy;
}

Then you setup your Antenna logger to use a different channel:

AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
[manager setSecurityPolicy:[self remoteLoggingSecurityPolicy]];

NSURL *logUrl = [NSURL URLWithString:@"https://log.marius.me.uk/log/"];
AntennaHTTPSChannel *httpsChannel =
  [[AntennaHTTPSChannel alloc] initWithURL:logUrl
                                    method:@"POST"
                   requestOperationManager:manager];
[[Antenna sharedLogger] addChannel:httpsChannel];
[[Antenna sharedLogger] startLoggingApplicationLifecycleNotifications];

If your end-point is using authentication, you can set it up in the request operation manager, after setting up the security policy:

NSURLCredential *credential =
    [[NSURLCredential alloc] initWithUser:@"user"
                                 password:@"password"
                              persistence:NSURLCredentialPersistenceSynchronizable];
[manager setCredential:credential];

Now, the earlier error message should dissapear and you should see the logs comming to your server.

If you’re asking, why would you like to send the debug logs to a server, there are several reason. First of them is to get debug info from your beta builds, when your testers are not in the same city as you. Second of them is to corelate data coming to your server, with data sent to your iPad with debug messages from your iPad.

References:

How to Open Jura ENA 5 Automatic Coffee Machine

Mar 29, 2015

Tools needed:

  • Torx T15 Screwdriver
  • Jura Oval Security Key (available on eBay)

Duration:

  • About 5 minutes

Start by removing the water reservoir and the front tray.

Unscrew the two T15 screw located at the back of the machine.

Unscrew the back Jura logo to access the back oval security screw. Press gently on the Jura logo and rotate it counter-clockwise. To open it, you’ll need to rotate it about 10 degrees.

Unscrew the back oval security screw.

Pull up the locking mechanism located.

Move the coffee spout up and unscrew the front oval security screw.

Gently, pull the unscrewed tag out and then down.

Using a plastic opener tool (or your finger) pull down the two front security locks.

Slide the lateral panels to the back.

Now you have access to the water pump, the grinder and the other internals.

To assemble the coffee machine follow the same steps in reverse order.

Tips for node.js deployment on SmartOS

Apr 2, 2013

At the lovely company I work, we’re doing quite some work with node.js. Best environment for node.js deployment is a SmartOS machine, of course. One of the nice features of SmartOS is the deep level of dtrace integration. Check Max’s presentation from Node Dublin ‘12.

On the other side, SmartOS is SunOS’s younger nephew and not Linux’, which means that all your init.d/upstart scripts your already have will not be 100% portable to this new OS.

I’ll show here a couple of tips, to make the first deployment of your node.js service or site easier.

First, give your server a nicer name, so when you ssh in, it’s easier on your eyes and memory then the default md5/guid name.

echo "massive-lannister.local" > /etc/nodename

Second prepare your server to communicate with your Github account. Generate a private key…

ssh-keygen -t rsa
cat ~/.ssh/id_rsa.pub

…and copy the content of ~/.ssh/id_rsa.pub to GitHub > Account Settings > SSH Keys > Add SSH key.

Now let’s set up a home for your project.

mkdir /home/massive/
cd /home/massive/

Clone the repository

git clone git@github.com:bamse/massive-lannister.git

Install the npm dependencies

cd massive-lannister/
npm install

To set up the node.js service, you’ll be using Service Management Facility.

In your git project, create a directory srvconf where the you’ll store the non-critical server configuration files like nginx.conf or SMF manifest.

Now create a SMF xml manifest based on this template.

<?xml version="1.0"?>
<!DOCTYPE service_bundle SYSTEM "/usr/share/lib/xml/dtd/service_bundle.dtd.1">
<service_bundle type="manifest" name="node-massive-lannister-service">
  <service name="site/node-massive-lannister-service" type="service" version="1">
	<create_default_instance enabled="true"/>
	<single_instance/>
	<dependency name="network" grouping="require_all" restart_on="refresh" type="service">
	  <service_fmri value="svc:/milestone/network:default"/>
	</dependency>
	<dependency name="filesystem" grouping="require_all" restart_on="refresh" type="service">
	  <service_fmri value="svc:/system/filesystem/local"/>
	</dependency>

	<method_context working_directory="/home/massive/massive-lannister">
	  <method_credential user="admin" group="staff" privileges='basic,net_privaddr'  />
	  <method_environment>
		<envvar name="PATH" value="/home/massive/massive-lannister/bin:/usr/local/bin:/usr/bin:/usr/sbin:/bin"/>
		<envvar name="HOME" value="/home/massive/massive-lannister"/>
		<envvar name="NODE_ENV" value="production"/>
	  </method_environment>
	</method_context>

	<exec_method
	  type="method"
	  name="start"
	  exec="/opt/local/bin/node /home/massive/massive-lannister/app.js"
	  timeout_seconds="60"/>

	<exec_method
	  type="method"
	  name="stop"
	  exec=":kill"
	  timeout_seconds="60"/>

	<property_group name="startd" type="framework">
	  <propval name="duration" type="astring" value="child"/>
	  <propval name="ignore_error" type="astring" value="core,signal"/>
	</property_group>

	<property_group name="application" type="application">
	</property_group>

	<stability value="Evolving"/>

	<template>
	  <common_name>
		<loctext xml:lang="C">node.js massive lannister service</loctext>
	  </common_name>
	</template>
  </service>
</service_bundle>

Import the service and start it.

svccfg import srvconf/node-massive-lannister-service-manifest.xml
svcadm enable node-massive-lannister-service

And verify that it works:

tail -f -n 50 "/var/svc/log/site-node-massive-lannister-service:default.log"

If you wish to stop the service:

svcadm disable node-massive-lannister-service

If you’re using the node.js service behind a nginx server, set this up too. Some companies (like Joyent) are exposing node.js directly, not behind a proxy.

Install nginx:

pkgin in nginx -y

Copy the nginx config:

mkdir -p /opt/local/etc/nginx/sites-enabled
cp srvconf/nginx-site-api.conf /opt/local/etc/nginx/sites-enabled

Edit nginx.conf and add the include line after http {

vim /opt/local/etc/nginx/nginx.conf
#add this line after http {
include /opt/local/etc/nginx/sites-enabled/*.conf;

Enable nginx:

svcadm enable nginx

Sample of nginx-massive-lannister.conf

server {
	listen   80; ## listen for ipv4; this line is default and implied
	#listen   [::]:80 default ipv6only=on; ## listen for ipv6

	root /home/massive/massive-lannister/public;
	index index.html index.htm;

	# Make site accessible from http://localhost/
	server_name api.massive-lannister.com;

	location /api {
		proxy_pass http://127.0.0.1:5648;
		proxy_buffering off;
		proxy_connect_timeout 66;
		proxy_read_timeout 66;
		proxy_send_timeout 66;
		proxy_hide_header X-Powered-By;
		proxy_hide_header X-Response-Time;
		proxy_set_header Host $host;
	}
}

To make a new deployment, at a later point:

ssh root@massive-lannister.com
cd /home/massive/massive-lannister/
git pull
svcadm restart node-massive-lannister-service

Another useful tip, with this setup is the separation of production and development environment. If you noticed, in the manifest xml file, we set a variable called NODE_ENV to production.

In app.js, include the reference to your config file:

var conf = require('./lib/config.js')

And later:

seneca.use('mongo-store', conf.mongo)

The content of lib/config.js is pretty small:

var env = process.env.NODE_ENV || 'development'
console.log('Config in ' + env + ' mode')

var config = {}
config.port = 5648; //TYRI
if (env === 'development') {
  config.mongo = {
	name:'db-dev',
	host:'',
	username:'massive-test',
	password:''
  }
} else {
  config.mongo = {
	name:'db-prod',
	host:'',
	username:'massive-prod',
	password:''
  }
}

module.exports = config

Inspired by Isaac Schlueter’s Github project.

Happy coding!

P.S. In case you’re wandering who’s massive-lannister, that’s the name of the project suggested by Github, when I went on to create a new repository. Some of their suggestions were north-american-hipster and mustached-sansa. If Tyrion Lannister and Sansa are present, I guess that someone from Github is a big fan of George RR Martin’s A Song of Ice and Fire. Or HBO’s Game of Thrones, I can’t tell until I see if they used Khaleesi or Daenerys for the Mother of Dragons’ name.