REST API is returning only 20 worklogs per issue

We are using the REST API to retrieve some information from JIRA for reporting purposes. Basically, we are getting the number of reported hours per project/project category in a specific time period.

Recently we have noticed that the numbers of reported hours are not correct i.e. that those are smaller than the real numbers in JIRA. The analysis has shown that the getWorklogs method called on an issue is returning only up to 20 worklogs. This looks like some kind of pagination, but we were not able to find this limitation anywhere in the documentation.

Can somebody please shed some light on this?

4 answers

1 accepted

Accepted Answer
3 votes

We had the same problem.

The restAPI (/rest/api/2/issue/{issueIdOrKey}) that is behind de IssueRestClient contains max. 20 worklogs per issue.

Te same for the SearchRestClient: the restAPI (/rest/api/2/search) also returns max. 20 worklogs per issue.

The only restAPI, that returns all the worklogs, is this one: /rest/api/2/issue/{issueIdOrKey}/worklog. Unfortunately, there is no corresponding Java RestClient in JRJC.

After looking into the JRJC code, I created some new classes, based on the existing code.

First I created a new IssueWorklogsRestClient interface with its class. There is one method getIssueWorklogs(BasicIssue issue) that returns a List of Worklogs per issue:

public interface IssueWorklogsRestClient 
{
    Promise<List<Worklog>> getIssueWorklogs(BasicIssue issue);
}

public class AsynchronousIssueWorklogsRestClient extends AbstractAsynchronousRestClient implements IssueWorklogsRestClient
{
    private final WorklogsJsonParser worklogsParser = new WorklogsJsonParser();
    private final URI baseUri;

    public AsynchronousIssueWorklogsRestClient(final URI baseUri, final HttpClient client)
    {
        super(client);
        this.baseUri = baseUri;
    }
  
    @Override
    public Promise<List<Worklog>> getIssueWorklogs(final BasicIssue issue)
    {
        final UriBuilder uriBuilder = UriBuilder.fromUri(baseUri);
        uriBuilder.path("issue").path(issue.getKey()).path("worklog");
        worklogsParser.setIssue(issue);
        return getAndParse(uriBuilder.build(), worklogsParser);
    }
}


I also created a new WorklogsJsonParser, based on the existing IssueJsonParser

public class WorklogsJsonParser implements JsonObjectParser<List<Worklog>>
{
    private BasicIssue basicIssue;

    public void setIssue(BasicIssue issue)
    {
        this.basicIssue = issue;
    }

    @Nullable
    private <T> Collection<T> parseOptionalArray(final JSONObject json, final JsonWeakParser<T> jsonParser, final String... path)
            throws JSONException
    {
        final JSONArray jsonArray = JsonParseUtil.getNestedOptionalArray(json, path);
        if (jsonArray == null)
        {
            return null;
        }
        final Collection<T> res = new ArrayList<T>(jsonArray.length());
        for (int i = 0; i < jsonArray.length(); i++)
        {
            res.add(jsonParser.parse(jsonArray.get(i)));
        }
        return res;
    }

    @Override
    public List<Worklog> parse(JSONObject s) throws JSONException
    {
        final Collection<Worklog> worklogs;
        final URI selfUri = basicIssue.getSelf();
        worklogs = parseOptionalArray(s, new JsonWeakParserForJsonObject<Worklog>(new WorklogJsonParserV5(selfUri)), WORKLOGS_FIELD.id);
       
        if(worklogs==null)
        {
            return Collections.<Worklog>emptyList();
        }
        return new ArrayList(worklogs);
    }

    private static class JsonWeakParserForJsonObject<T> implements JsonWeakParser<T>
    {
        private final JsonObjectParser<T> jsonParser;

        public JsonWeakParserForJsonObject(JsonObjectParser<T> jsonParser)
        {
            this.jsonParser = jsonParser;
        }

        private <T> T convert(Object o, Class<T> clazz) throws JSONException
        {
            try
            {
                return clazz.cast(o);
            } catch (ClassCastException e)
            {
                throw new JSONException("Expected [" + clazz.getSimpleName() + "], but found [" + o.getClass().getSimpleName() + "]");
            }
        }

        @Override
        public T parse(Object o) throws JSONException
        {
            return jsonParser.parse(convert(o, JSONObject.class));
        }
    }

    private interface JsonWeakParser<T>
    {
        T parse(Object o) throws JSONException;
    }
}


finally I created a subclass+interface of the existing JiraRestClient that creates the new IssueWorklogsClient

public interface JiraRestClientPlus extends JiraRestClient
{
    IssueWorklogsRestClient getIssueWorklogRestClient();
}

public class AsynchronousJiraRestClientPlus extends AsynchronousJiraRestClient implements JiraRestClientPlus
{
    private final IssueWorklogsRestClient issueWorklogsRestClient;
   
    public AsynchronousJiraRestClientPlus(final URI serverUri, final DisposableHttpClient httpClient)
    {
        super(serverUri,httpClient);
        final URI baseUri = UriBuilder.fromUri(serverUri).path("/rest/api/latest").build(); // '/rest/api/latest' or '/rest/api/2'
        issueWorklogsRestClient = new AsynchronousIssueWorklogsRestClient(baseUri, httpClient);
    }
   
    @Override
    public IssueWorklogsRestClient getIssueWorklogRestClient()
    {
        return issueWorklogsRestClient;
    }
}


+ a subclass of the existing AsynchronousJiraRestClientFactory to create the new JiraRestClientPlus

public class AsynchronousJiraRestClientFactoryPlus extends AsynchronousJiraRestClientFactory
{
    @Override
    public JiraRestClientPlus create(final URI serverUri, final AuthenticationHandler authenticationHandler)
    {
        final DisposableHttpClient httpClient = new AsynchronousHttpClientFactory()
                .createClient(serverUri, authenticationHandler);
        return new AsynchronousJiraRestClientPlus(serverUri, httpClient);
    }

    @Override
    public JiraRestClientPlus createWithBasicHttpAuthentication(final URI serverUri, final String username, final String password)
    {
        return create(serverUri, new BasicHttpAuthenticationHandler(username, password));
    }

    @Override
    public JiraRestClientPlus create(final URI serverUri, final HttpClient httpClient)
    {
        final DisposableHttpClient disposableHttpClient = new AsynchronousHttpClientFactory().createClient(httpClient);
        return new AsynchronousJiraRestClientPlus(serverUri, disposableHttpClient);
    }
}


For the rest, I use my existing code, but added a extra check that when worklogs.size >=20, it will get the worklogs by using the new IssueWorklogsRestClient.

If you ever try to make a copy of this great workaround you might stumble over line 32 of WorklogsJsonParser: The reference to WORKLOGS_FIELD is missing.

Replace it with com.atlassian.jira.rest.client.api.domain.IssueFieldId.WORKLOGS_FIELD.id

 

0 votes
Timothy Chin Community Champion Aug 27, 2013

https://docs.atlassian.com/jira/REST/latest/#idp1939456

returns a collection of worklogs associated with the issue, with count and pagination information

I don't know if this will work but base on maxResults, you might have to do recursive calls with the startAt parameter added.

Thanks. Recursive calls would be fine, but I am not sure that I see a way to do it using only the Java API. We have something like this :

Issue issue = restClient.getIssueClient().getIssue(key).claim();

Iterable<Worklog> worklogs = issue.getWorklogs();

And there is no way, or at least I do not see it, to set the startAt or max Results parameters.

Timothy Chin Community Champion Aug 27, 2013

Sorry. I don't use the rest client. I make my own REST calls. I'm sure the REST client does not expose the other results. Tough luck.

We will go with the Tempo API instead since it provides the data we need.

0 votes

This issue is being fixed in: https://jira.atlassian.com/browse/JRA-34746

Do we know if this is an issue on JIRA 7?

It's "being fixed" for several years now

@Александр Коновалов,

The original bug ticket has been splitted in 2, one for Server and one for Cloud. As you can see the one for Cloud is already fixed since the limit has been added on purpose as you can read in:

We are sorry there was change in a behaviour. However as it was made due to performance reasons we want to keep it as it is.

If anyone is still willing to get full list of worklog items I would recommend using GET /rest/api/2/issue/{issueIdOrKey}/worklog]

 

If you have a look at the REST API documentation you can check that indeed there are new worklog related endpoints (per issue, per time interval, etc):

 

As regarding Jira Server, I am not even sure that issue has ever been there. So maybe it is just there open by mistake.

I'm using Jira Server, the issue is there. 

Problem is, Atlassian tries to decide for the end-users what's best for them: We are sorry there was change in a behaviour. However as it was made due to performance reasons we want to keep it as it is.

If there are any performance reasons (and I'm pretty sure there are!) with querying issues containing tonnes of worklogs then by all means warn me BUT let me decide if it's acceptable in my particular scenario.

In some cases it would be totally ok if Jira took its time and returned all changed worklogs in one giant and neat response, albeit after a week of processing (ok, we all understand it will take several minutes tops).

And I'm forced to call several consequent endpoints and create a process of parsing additional data, looping etc, though a perfectly working architecture for providing all the data I need is already there

Suggest an answer

Log in or Sign up to answer
Community showcase
Posted Tuesday in Jira

Looking for anyone who made the switch to Data Center

The Jira Marketing team is putting together an ebook on migrating to Data Center. We're looking for pro tips on how you staffed your project team and organized your Proof of Concept. Share yo...

34 views 0 2
Join discussion

Atlassian User Groups

Connect with like-minded Atlassian users at free events near you!

Find a group

Connect with like-minded Atlassian users at free events near you!

Find my local user group

Unfortunately there are no AUG chapters near you at the moment.

Start an AUG

You're one step closer to meeting fellow Atlassian users at your local meet up. Learn more about AUGs

Groups near you