dannyman.toldme.com


JIRA, Technology

Notes From Atlassian Summit 2013

Link: http://dannyman.toldme.com/2013/10/15/atlassian-summit/

Two weeks ago, I attended Atlassian Summit 2013 in San Francisco.  This is an opportunity to train, network, and absorb propaganda about Atlassian products (JIRA, Greenhopper, Confluence, &c.) and ecosystem partners.  I thought I would share a summary of some of the notes I took along the way, for anyone who might find interest:

At the Keynote, Atlassian launched some interesting products:

As time passes, the ticket gets crankier at you about the SLA in real time.

As time passes, the ticket gets crankier at you in real time about the SLA.

Jira Service Desk

Jira Service Desk is an extension to JIRA 6 oriented around IT needs.  The interesting features include:

The first thing helps people get their work done, and the second is manager catnip.

Confluence Knowledge Base

Confluence 5.3 features a shake-the-box Knowledge Base setup:

Other Stuff I looked into:

REST and Webhooks

There was a presentation on JIRA’s REST API, and mention of Webhooks.

REST is really easy to use.  For example, hit https://jira.atlassian.com/rest/api/latest/issue/JRA-9

There’s API docs here: https://docs.atlassian.com/jira/REST/

Another feature for tight integration is Webhooks: you can configure JIRA so that certain issue actions trigger a hit to a remote URL.  This is generally intended for building apps around JIRA.  We might use this to implement Nagios ACKs.

Atlassian Connect

I haven’t looked too deeply as this is a JIRA 6 feature, but Atlassian Connect promises to be a new method of building JIRA extensions that is lighter-weight than their traditional plugin method.  (Plugins want you to set up Eclipse and build a Java Dev environment in your workstation… Connect sounds like just build something in your own technology stack around REST and Webhooks)

Cultivating Content: Designing Wiki Solutions that Scale

Rebecca Glassman, a tech writer at Opower, gave a really engaging talk that addresses a problem that seems commonplace: how to tame the wiki jungle!  Her methodology went something like this:

As they better learned user needs and what sort of knowledge there was, they built “The BOOK” (Body of Opower Knowledge) based on a National Parks model:

(I have some more notes on how they built, launched, and promoted The BOOK.  The problem they tackled sounds all to familiar and her approach is what I have always imagined as the sort of way to go.)

Ad Hoc Canvas

The Ad Hoc Canvas plugin for Confluence caught my eye.  At first glance, it is like Trello, or Kanban, where you fill out little cards and drag them around to track things.  But it has options to organize the information in different ways depending on the task at hand: wherever you are using a spreadsheet to track knowledge or work, Ad Hoc Canvas might be a much better solution.  Just look at the videos and you get an idea . . .

The Dark Art of Performance Tuning

Adaptavist gave a presentation on performance analysis of JIRA and Confluence.  It was fairly high-level but the gist of this is that you want to monitor and trend the state of the JVM: memory, heap, garbage collection, filehandles, database connections, &c.  He had some cool graphs of stuff like garbage collection events versus latency that had helped them to analyze issues for clients.  One consideration is that each plugin and each code revision to a plugin brings a bunch of new code into the pool with its own potential for issues.  Ideally, you can set up a load testing environment for your staging system.  Short of that, the more system metrics that you can track, you can upgrade plugins one at a time and watch for any effects.  As an example, one plugin upgrade went from reserving 30 database connections to reserving 150 database connections, and that messed up performance because the rest of the system would become starved of available database connections.  (So, they figured that out and increased that resource..)

tl;dr: JIRA Performance Tuning is a variation of managing other JVM Applications

Collaboration For Executives

I popped in on this session near the end, but the takeaway for anyone who wants to deliver effective presentations to upper management are:

The presenter’s narrative was driven by an initial need to capture executive buy-in that their JIRA system was critical to business function and needed adequate resourcing.

Feedback Welcome


JIRA, Python, Technical

Jython: Timezone Manipulation and Meeting Invitations vs Outlook

Link: http://dannyman.toldme.com/2013/09/23/jython-timezone-manipulation-and-meeting-invitations-vs-outlook/

As part of a project at work I’ve built some Jython code that builds iCalendar attachments to include meeting invitations for scheduled maintenance sessions. Jython is Python-in-Java which takes some getting used to but is damned handy when you’re working with JIRA. I will share a few anecdotes:

1) For doing date and time calculations, specifically to determine locale offset from UTC, you’re a lot happier calling the Java SimpleDateFormat stuff than you are dealing with Python. Python is a beautiful language but I burned a lot of time in an earlier version of this code figuring out how to convert between different time objects for manipulation and whatnot. This is not what you would expect from an intuitive, weakly-typed language, and it is interesting to find that the more obtuse, strongly-typed language handles time zones and it just fricking works.

Here is some sample code:

from java.text import SimpleDateFormat
from com.atlassian.jira.timezone import TimeZoneManagerImpl
tzm = TimeZoneManagerImpl(ComponentManager.getInstance().getJiraAuthenticationContext(),
    ComponentManager.getInstance().getUserPreferencesManager(),
    ComponentManager.getInstance().getApplicationProperties())
# df_utc = DateFormat UTC
# df_assignee = DateFormat Assignee
df_utc = SimpleDateFormat("EEE yyyy-MM-dd HH:mm ZZZZZ (zzz)")
df_assignee = SimpleDateFormat("EEE yyyy-MM-dd HH:mm ZZZZZ (zzz)")
tz = df_utc.getTimeZone()
df_utc.setTimeZone(tz.getTimeZone("UTC"))
df_assignee.setTimeZone(tzm.getTimeZoneforUser(assignee))
issue_dict['Start_Time_text'] = df_utc.format(start_time.getTime())
issue_dict['Start_Time_html'] = df_utc.format(start_time.getTime())
if df_utc != df_assignee:
    issue_dict['Start_Time_text'] += "\r\n                   "
    issue_dict['Start_Time_text'] += df_assignee.format(start_time.getTime())
    issue_dict['Start_Time_html'] += "<br />"
    issue_dict['Start_Time_html'] += df_assignee.format(start_time.getTime())
# Get TimeZone of Assignee
# Start Time in Assignee TimeZone

Since our team is global I set up our announcement emails to render the time in UTC, and, if it is different, in the time zone of the person leading the change. For example:

Start Time: Mon 2013-09-23 23:00 +0000 (UTC)
Mon 2013-09-23 16:00 -0700 (PDT)

2) I was sending meeting invitations with the host set to the assignee of the maintenance event. This seemed reasonable to me, but when Mac Outlook saw that the host was set, it would not offer to add the event to the host’s calendar. After all, all meeting invitations come from Microsoft Outlook, right?! If I am the host it must already be on my calendar!!

I tried just not setting the host. This worked fine except now people would RSVP to the event and they would get an error stuck in their outboxes.

So . . . set the host to a bogus email address? My boss was like “just change the code to send two different invitations” which sounds easy enough for him but I know how creaky and fun to debug is my code. I came upon a better solution: I set the host address to user+calendar@domain.com. This way, Outlook is naive enough to believe the email address doesn’t match, but all our software which handles mail delivery knows the old ways of address extension . . . I can send one invitation, and have that much less messy code to maintain.

from icalendar import Calendar, Event, UTC, vText, vCalAddress
# [ . . . ]
event = Event()
# [ . . . ]
# THIS trick allows organizer to add event without breaking RSVP
# decline functionality.  (Outlook and its users suck.)
organizer_a = assignee.getEmailAddress().split('@')
organizer = vCalAddress('MAILTO:' + organizer_a[0]+ '+calendar@' +
    organizer_a[1])
organizer.params['CN'] = vText(assignee.getDisplayName() + ' (' + assignee.getName() + ')')
event['organizer'] = organizer

You can get an idea of what fun it is to build iCalendar invitations, yes? The thing with the parentheses concatenation on the CN line is to follow our organization’s convention of rendering email addresses as “user@organization.com (Full Name)”.

3) Okay, third anecdote. You see in my first code fragment that I’m building up text objects for HTML and plaintext. I feed them into templates and craft a beautiful mime multipart/alternative with HTML and nicely-formatted plaintext . . . however, if there’s a Calendar invite also attached then Microsoft Exchange blows all that away, mangles the HTML to RTF and back again to HTML, and then renders its own text version of the RTF. My effort to make a pretty text email for the users gets chewed up and spat out, and my HTML gets mangled up, too. (And, yes, I work with SysAdmins so some users actually do look at the plain text . . .) I hate you, Microsoft Exchange!

Feedback Welcome


JIRA

Jython: Render Wiki Text in Jira

Link: http://dannyman.toldme.com/2013/07/18/jython-render-wiki-text-in-jira/

I’m building out a simple template system for our email notifications, so of course I want to support multipart, text and email. But, hey, we have some text fields in JIRA that can take wiki markup, and JIRA will format that on display. So, how do I handle those fields in my text and HTML message attachments?

https://answers.atlassian.com/questions/191567/in-a-jira-script-how-do-i-render-wiki-text-fields
https://answers.atlassian.com/questions/135084/method-to-convert-jira-wiki-format-to-html

So, some sample code to render the custom field “Change Summary” into a pair of strings, change_summary_text and change_summary_html, suitable for inclusion into an email message:

from com.atlassian.event.api import EventPublisher
from com.atlassian.jira import ComponentManager
from com.atlassian.jira.component import ComponentAccessor
from com.atlassian.jira.issue import CustomFieldManager
from com.atlassian.jira.issue.fields import CustomField
from com.atlassian.jira.issue.fields.renderer.wiki import AtlassianWikiRenderer
from com.atlassian.jira.util.velocity import VelocityRequestContextFactory
 
# Get Custom Field
cfm = ComponentManager.getInstance().getCustomFieldManager()
change_summary = issue.getCustomFieldValue(cfm.getCustomFieldObjectByName("Change Summary"))
 
# Set up Wiki renderer
eventPublisher = ComponentAccessor.getOSGiComponentInstanceOfType(EventPublisher)
velocityRequestContextFactory = ComponentAccessor.getOSGiComponentInstanceOfType(VelocityRequestContextFactory)
wikiRenderer = AtlassianWikiRenderer(eventPublisher, velocityRequestContextFactory)
 
# Render Custom Field
change_summary_html = wikiRenderer.render(change_summary, None)
change_summary_text = wikiRenderer.renderAsText(change_summary, None)

Feedback Welcome


JIRA, Technical

Embed Page Refresh

Link: http://dannyman.toldme.com/2013/05/13/embed-page-refresh/

Feature request that certain JIRA dashboards should reload more frequently than every fifteen minutes. So, I cooked up some JavaScript to hide in the announcement banner:

<script type="text/javascript">
 
function gup( name ){
name = name.replace(/[\[]/,"\\\[").replace(/[\]]/,"\\\]");
var regexS = "[\\?&]"+name+"=([^&#]*)";
var regex = new RegExp( regexS );
var results = regex.exec( window.location.href );
 if( results == null )    return "";
else    return results[1];}
 
function reload() {
 window.location = window.location
}
 
var refresh = gup('refresh');
if ( refresh > 0 )    window.setInterval(reload, refresh*1000);
</script>

Now users can add refresh=nn and the page will reload every nn seconds. This ought to work in most cases where you can sneak some HTML into a Web App.

Function gup stolen from http://stackoverflow.com/questions/979975/how-to-get-the-value-from-url-parameter.

Feedback Welcome


JIRA

JIRA Cascading Select in Jython

Link: http://dannyman.toldme.com/2012/12/06/jira-cascading-select-in-jython/

The Cascading Select Custom Field type in JIRA is a bear. The first trick is learning to set the “null” value and then the “1″ child value. The next trick is building out a ModifiedValue object to hold your change. Then you get to jump down the rabbit hole of finding the correct Option values for the custom field, and setting them with the tricks just mentioned.

So, in the interests of saving me sanity next time I need to set a Cascading Select, here’s a Jython function that works in Jira 4.2:

import logging
 
from com.atlassian.jira import ComponentManager
from com.atlassian.jira.issue.customfields.manager import OptionsManager
from com.atlassian.jira.issue.customfields.view import CustomFieldParamsImpl
from com.atlassian.jira.issue import ModifiedValue
from com.atlassian.jira.issue.util import DefaultIssueChangeHolder
from java.util import HashSet
 
# cf = custom field
# issue = issue to modify
# parent = top value to set (string value)
# child = child value to set (string value)
def set_cascading_select(cf, issue, parent, child):
    # Get the managers
    cfm = ComponentManager.getInstance().getCustomFieldManager()
    om = ComponentManager.getComponentInstanceOfType(OptionsManager)
    fli = ComponentManager.getInstance().getFieldLayoutManager().getFieldLayout(issue).getFieldLayoutItem(cf)
 
    parent_options = om.getOptions(cf.getRelevantConfig(issue))
    parent_option = None
    child_option = None
    try:
        parent_option = parent_options.getOptionForValue(parent, None)
    except:
        pass
    try:
        child_option = parent_options.getOptionForValue(child, parent_option.getOptionId())
    except:
        pass
 
    if parent_option and child_option:
        old_application = issue.getCustomFieldValue(cf)
        new_application = CustomFieldParamsImpl(cf)
        a_none = HashSet()
        a_none.add(parent_option)
        a_1 = HashSet()
        a_1.add(child_option)
        new_application.put(None, a_none)
        new_application.put("1", a_1)
        mf = ModifiedValue(old_application, new_application)
        cf.updateValue(fli, issue, mf, DefaultIssueChangeHolder())
        logging.debug("set issue " + issue.getKey() + " cf " + cf.getName() + " setting " + parent + "/" + child)
        return True
    else:
        logging.error("invalid parent/child option: " + parent + "/" + child)
        return None

Example function calls from within a validation hook:

cfm = ComponentManager.getInstance().getCustomFieldManager()
 
application_cf = cfm.getCustomFieldObjectByName("Beverages")
 
# good
set_cascading_select(application_cf, issue, "Hard Drinks", "Whiskey")
# bad child
set_cascading_select(application_cf, issue, "Hard Drinks", "Coke")
# bad parent
set_cascading_select(application_cf, issue, "Soft Drinks", "Whiskey")
# total crap
set_cascading_select(application_cf, issue, "Illicit Drugs", "Bath Salts")

The logging stuff is useful for debugging, if you have that set up, else just remove those bits.

Feedback Welcome


JIRA, Technical

JIRA Workflow Transition Condition: check_parent_resolved.py

Link: http://dannyman.toldme.com/2012/11/07/jython-workflow-transition-check-parent/

It took a few hours to figure this hook out, so I’m including my hard-won lines of code here.

# -*- coding: UTF-8 -*-
# Check if PARENT is resolved.
# Monitoring creates Events in the Event queue, these Events
# automatically create Incident children.
# We don't want to resolve any Incident children until the parent Event
# resolves.
# 
# (Normally you want to block on your children instead of your parent.)
 
from com.atlassian.jira import ComponentManager
from com.atlassian.jira.issue.link import IssueLinkManager
 
ilm = ComponentManager.getInstance().getIssueLinkManager()
 
# Assume we are okay ...
result = True
 
for link in ilm.getInwardLinks(issue.getId()):
    if link.getIssueLinkType().getName() == "Parent" and link.getSourceObject().getResolution() == None:
        result = False

Feedback Welcome


JIRA, Python, Technical

Jython Validator Cookbook

Link: http://dannyman.toldme.com/2012/09/25/jython-validator-cookbook/

Building on a previous post to validate user time tracking, a few “cookbook” scripts that may be handy to you or me in the future.

JIRA 4.2, YMMV.

Validate User Time Tracking

Somewhat elaborate: enforce that time worked has been logged, except under certain circumstances. See original post.

import com.atlassian.jira.issue.worklog.Worklog
from com.atlassian.jira import ComponentManager
 
# Time Already Logged
timespent = issue.getTimeSpent()
# Time Logged via current screen
try:
    timelogged = dict(issue.getModifiedFields())['worklog']
except:
    timelogged = False
 
# Duplicate Issue?  It is as good as logged!
resolution = issue.getResolution()
if resolution['name'] == "Duplicate":
    timelogged = True
if resolution['name'] == "Self Corrected":
    timelogged = True
 
# Nagios likes to close tickets, but doesn't get paid
user = ComponentManager.getInstance().getJiraAuthenticationContext().getUser()
if user.getName() == "nagios":
    timelogged = True
 
if timespent < = 0 and timelogged == False:
    result = False
    description = "Please log the time you spent on this ticket."

Assign Unassigned Issue to User

This helps make sure tickets get assigned.

# -*- coding: UTF-8 -*-
from com.atlassian.jira import ComponentManager
 
assignee = issue.getAssignee()
user = ComponentManager.getInstance().getJiraAuthenticationContext().getUser()
 
if not issue.getAssignee():
issue.setAssignee(ComponentManager.getInstance().getJiraAuthenticationContext().getUser())

Validate Custom Field Value

We have a particular custom field which can be set UNKNOWN by the Reporter, but which should be cleaned up by the Assignee.

from com.atlassian.jira import ComponentManager
 
cfm = ComponentManager.getInstance().getCustomFieldManager()
product = issue.getCustomFieldValue(cfm.getCustomFieldObject('customfield_12345'))
 
if product == 'UNKNOWN':
result = False
description = "Please set CUSTOM_FIELD value appropriately."

Feedback Welcome


FreeBSD, JIRA, Linux, Mac OS X, Technical

Strip Non-Ascii From a File

Link: http://dannyman.toldme.com/2012/05/10/mysql-not-configured-for-utf8/

I have had bad luck trying to coax this out of Google, so here’s a Perl one-liner:

perl -pi -e 's/[\x80-\xEF]//g' file.txt

Where file.txt is a file you want to clean up.

Why this comes up is because we have a web application that was set up to hit a MySQL database, which is incorrectly configured to store text as ASCII instead of UTF-8. The application assumes that all text is Unicode and that the database is correctly configured, and every week or two someone asks me why they are getting this weird gnarly error. Typically they are pasting in some weird UTF-8 whitespace character sent to us from Ukraine.

Eventually the database will be reloaded as UTF-8 and the problem will be solved. Until then, I can tell folks to use the Perl command above. It just looks for anything with the high bit set and strips it out.

Feedback Welcome


JIRA, Technical

Work Note: Quarterly Reports from MySQL

Link: http://dannyman.toldme.com/2012/05/08/jira-sla-sql-query/

I am not a DBA. I am but a humble SysAdmin who gets asked to figure out things like “how have we been at meeting our SLAs over time?” After I try to excuse myself I’ll inevitably end up say at the JIRA database running a query like this:

echo
echo "Incidents (P3)"
mysql -u jira jiradb< <__id3q
select year(created) as "Year", quarter(created) as "Quarter",
    count(pkey) as "Total",
    sum(resolutiondate < date_add(created, interval x day)) as "Met SLA",
    sum(resolutiondate < date_add(created, interval x day)) / count(pkey) as "SLA %%"
    from jiraissue where pkey like 'OPS-%' and priority = 3
    and assignee != 'nagios' and issuetype = 26
    group by year(created), quarter(created) order by created;
__id3q

That above is a fragment from a shell script. Shell scripts are great for complex SQL queries, I find. Set a value x at interval x day and the output looks something like:

Incidents (P3)
Year    Quarter Total   Met SLA SLA %%
2011    2       xxx     xxx     x.xxxx
2011    3       xxx     xxx     x.xxxx
2011    4       xxx     xxx     x.xxxx
2012    1       xxx     xxx     x.xxxx
2012    2       xxx     xxx     x.xxxx

The query does some things that are newer to my limited understanding of SQL. For me the magic bits are sum()ed columns and the availability of quarter() … you can do monthly reports just as easily with month(). I’d love to concatenate Year-Month into a string like “2012-05″ but for the purposes of making my boss a little happier queries like this are good to have in the locker.

Some day I’ll be hip enough to convert things like this into JIRA widgets.

Oh yeah, and if your SLAs are measured in “business hours” or “business days” this will give you only a crude understanding of how well you have met your SLAs … an accurate measure would probably get embedded in a handler that gets called on issue close which can evaluate SLA fulfillment per issue priority and the local work schedule.

1 Comment


JIRA

JIRA: Require User Time Tracking

Link: http://dannyman.toldme.com/2011/12/01/jira-jython-validator-enforce-time-spent/

Time tracking in JIRA is a nice feature, but we have to get people to do it. My initial attempts to enforce time tracking ran into trouble, but I was able to develop a Jython Validator to hook on to transitions to the Resolved state. Now it is mandatory for our users to log time worked before they can resolve an issue:

# -*- coding: UTF-8 -*-
import com.atlassian.jira.issue.worklog.Worklog
from com.atlassian.jira import ComponentManager
 
# Time Already Logged
timespent = issue.getTimeSpent()
# Time Logged via current screen
try:
    timelogged = dict(issue.getModifiedFields())['worklog']
except:
    timelogged = False
 
# Duplicate Issue?  It is as good as logged!
resolution = issue.getResolution()
if resolution['name'] == "Duplicate":
    timelogged = True
if resolution['name'] == "Self Corrected":
    timelogged = True
 
# Nagios likes to close tickets, but doesn't get paid
user = ComponentManager.getInstance().getJiraAuthenticationContext().getUser()
if user.getName() == "nagios":
    timelogged = True
 
if timespent < = 0 and timelogged == False:
    result = False
    description = "Please log the time you spent on this ticket."

2012-01-24 Update: the script now contains additional logic, which exempts the nagios user from enforcement and allows resolution of duplicated or self-correcting issues which may not require time tracking. Hopefully this example is useful to somebody.

3 Comments


JIRA, Sundry, Technology

JavaScript Hack: Hide an Element on a Page

Link: http://dannyman.toldme.com/2011/10/25/javascript-hack-hide-an-element-on-a-page/

JIRA is an issue tracking system that is really flexible, but sometimes presents irritatingly arbitrary limitations.

I have been working on a screen which uses multiple tabs. The tabs are there to make it easier for the user to find the fields they want to edit, without scrolling through a single long, complex issue. But every tab has a Comment field rendered on it, which makes things confusing, and makes each tab look like it needs scrolling.

So, just remove the Comment field from the Screen, right? No, it isn’t in there. So, can I remove Comment via the Field Configuration Scheme? No, it is mandatory. Damn your arbitrary limitation, JIRA!

Anyway, I don’t normally speak JavaScript, but I managed to gin up the following snippet to paste into a Field description which appears in the screen I wanted to tweak. It finds the element containing the Comment, and sets its style display attribute to none. As the page loads, the Comment box is rendered, but once the page load completes, the Comment box disappears.

<script type="text/javascript">
function hideCommentField() {
        var elements = document.getElementsByClassName('field-group aui-field-wikiedit');
        elements[0].style.display = 'none';
}
// http://stackoverflow.com/questions/807878/javascript-that-executes-after-page-load
if(window.attachEvent) {
    window.attachEvent('onload', hideCommentField);
} else {
    if(window.onload) {
        var curronload = window.onload;
        var newonload = function() {
            curronload();
            hideCommentField();
        };
        window.onload = newonload;
    } else {
        window.onload = hideCommentField;
    }
}
</script>

It is ugly, but effective. Also, it is helpful for me to learn JavaScript!

PS: Thanks for the Guidance, Ed Burns!

1 Comment


doodles, JIRA, Technical, Technology

TICKET. OR. GTFO.

Link: http://dannyman.toldme.com/2011/08/17/ticket-or-gtfo/

I do not know the provenance of the source material, and can make no claims of intellectual property rights here. TinEye finds 550 similar images.

2 Comments


Site Archive