Skip to content

Conversation

@zoltancsontosness
Copy link
Contributor

Redpanda Jira Connector

Connector Path
Jira connect/public/components/jira

Prerequisites for development

  1. Install go

  2. Install go-task

    go install github.com/go-task/task/v3/cmd/task@latest

Running & testing the connector

  1. Set GOBIN variable

    go env -w GOBIN=$(pwd)/bin

  2. Lint and format the code

    task lint
    task fmt
    
  3. Build the application

    go build

  4. Set variables specific to connector that you are running based on config.yaml file (see redpanda secrets):

    export JIRA_API_TOKEN=yourToken

  5. Run the connector

    ./connect --config config.yaml

Config & usage examples

  • Example processor configuration:
pipeline:
  threads: 1
  processors:
    - jira:
        base_url: "https://{your_org}.atlassian.net"
        username: "jiraUsername"
        api_token: "${JIRA_API_TOKEN}"
        max_results_per_page: 200
        max_retries: 50
  • Example input configuration that produces one message into the pipeline:
input:
  generate:
    interval: "1m"
    count: 1 # 0 for infinite @ specified interval
    mapping: |
      root = {
        "project": "RPC",
        "fields": ["summary", "Story Points", "assignee.displayName", "Sprint.name"],
        "updated": ">= -4w",
        "created": ">= -4w"
      }
  • Example message using JQL query - this retrieves the summary, name of sprint and story points for an issue from an user that is having status In progress
{
    "fields": ["summary", "Sprint.name", "Story Points"],
    "jql": "assignee=\"Andrei Arcan\" and status=\"In Progress\" order by created DESC"
}
  • Example message using Project - this gets the summary and status for all issues that are created in the last 4 weeks and updated in the last 2 days for RPC project
{
    "project": "RPC",
    "fields": ["summary", "status.name"],
    "created": ">= -4w",
    "updated": ">= -2d"
}
  • Example message using Project - this gets all fields for all issues
{
    "project": "RPC"
}

Copy link
Collaborator

@Jeffail Jeffail left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @zoltancsontosness! Just a couple of comments, code looks really neat on a first pass through.

args: optional arguments for format string interpolation
*/
func (j *jiraProc) debug(format string, args ...interface{}) {
if j != nil && j.log != nil {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's actually safe to call methods on a nil *service.Logger as it already guards against nil references: https://github.com/redpanda-data/benthos/blob/main/public/service/logger.go#L49, so we don't really need this wrapper and are probably better off without it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done ✔️

package jira

// JiraAPIBasePath is the base path for Jira Rest API
const JiraAPIBasePath = "/rest/api/3"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This probably doesn't need to be its own file, lets move this into client.go.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done ✔️

@mmatczuk
Copy link
Collaborator

Would you mind converting block comments to line comments especially for docstrings?

@@ -0,0 +1 @@
package jira
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is probably not needed.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done ✔️

@mmatczuk
Copy link
Collaborator

Also, would be good to add file headers.

"github.com/redpanda-data/benthos/v4/public/service"
)

type jiraProc struct {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the split to different resources files, I'm confused why this one is called config?

Probably jiraProc belongs to resources, the rest to processor.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done ✔️

@mmatczuk
Copy link
Collaborator

I’d like to suggest we reduce the number of exported types in the package to keep the API simple and easy to use. It would also be clearer if all Jira-specific code lived in a single package, for example jirahttp. In that case, I suggest we migrate all of the helpers content (and its subpackages) into jirahttp, so everything Jira-related is grouped in one place. Our internal Jira library can then live under the jira package.

@mmatczuk
Copy link
Collaborator

Please add enterprise license check

Look for

	if err := license.CheckRunningEnterprise(mgr); err != nil {
		return nil, err
	}

If customFields are present in the data, they will also be replaced with their real name;
example: custom_field_10100 will be replaced with "Story Points"
*/
func (j *jiraProc) filter(data interface{}, selectors selectorTree, custom map[string]string) (interface{}, error) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use any instead of interface{}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this be a function not method? Or method of selectorTree?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done ✔️

"summary": {}
}
*/
func (j *jiraProc) buildSelectorTree(fields []string, custom map[string]string) (selectorTree, error) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about selectorTreeFrom() - a constructor not a method of jiraProc

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done ✔️

Package http_helper usage

Overview
- Provides a thin, robust wrapper around net/http to:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Improve rendering in go doc.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done ✔️


5. Example helper wrapping a Jira call

func callJira(ctx context.Context, client *http.Client, req *http.Request) ([]byte, *jira_helper.JiraError) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Improve code formatting - maybe use examples https://go.dev/blog/examples

@CLAassistant
Copy link

CLAassistant commented Sep 22, 2025

CLA assistant check
All committers have signed the CLA.

reduce number of exported types; moved most of the code inside the jirahttp package;
Comment on lines 137 to 149
return &jirahttp.JiraProc{
BaseURL: baseURL,
Username: username,
ApiToken: apiToken,
MaxResults: maxResults,
RetryOpts: http_helper.RetryOptions{
MaxRetries: maxRetries,
},
HttpClient: http_metrics.NewInstrumentedClient(
mgr.Metrics(), "jira_http",
httpClient),
Log: mgr.Logger(),
}, nil
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'd like to have all the connect integration code here - in the main jira package, and all the rest, http related, in jirahttp. Also, the helpers belong to jirahttp, as they contain logic specific to JIRA http needs and semantics.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done ✔️

)

// Configuration specification for the Jira processor
var jiraProcessorConfigSpec = service.NewConfigSpec().
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use function instead of a global variable.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done ✔️

@@ -0,0 +1,161 @@
// Copyright 2024 Redpanda Data, Inc.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rename this file to processor_jira.go

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done ✔️

Description("Maximum number of retries in case of 429 HTTP Status Code").
Default(10))

func newJiraProcessor(conf *service.ParsedConfig, mgr *service.Resources) (*jirahttp.JiraProc, error) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm confused, we have a constructor for a type defined in a different package.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done ✔️

Comment on lines 138 to 148
BaseURL: baseURL,
Username: username,
ApiToken: apiToken,
MaxResults: maxResults,
RetryOpts: http_helper.RetryOptions{
MaxRetries: maxRetries,
},
HttpClient: http_metrics.NewInstrumentedClient(
mgr.Metrics(), "jira_http",
httpClient),
Log: mgr.Logger(),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The type should have a real constructor and those fields should not be exported.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done ✔️


tests := []struct {
name string
yaml string
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's safer to avoid names that conflict with common package names

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done ✔️

@mmatczuk
Copy link
Collaborator

mmatczuk commented Oct 3, 2025

The only type exported from jirahttp is the JiraProc which should be split and possibly renamed. The Process and searchResource belong to the main package. We need jirahttp to be more flexible, it shall export all the functionalities separately for ease of extension and maintenance.

@mmatczuk
Copy link
Collaborator

mmatczuk commented Oct 9, 2025

I wonder why we have decided to implement custom retries for http instead of using something established like https://github.com/hashicorp/go-retryablehttp. It would be faster, easier and more secure. To make this retry scheme work in general case you need a more graceful handling of request body, for example note that (from Do)

// The request Body, if non-nil, will be closed by the underlying
// Transport, even on errors. The Body may be closed asynchronously after
// Do returns.

Given that at this point I suggest to move http_helper.go to jirahttp.

@mmatczuk
Copy link
Collaborator

mmatczuk commented Oct 9, 2025

Also, the jira_helper hosting only

// CheckJiraAuth inspects an HTTP response from Jira and
// returns a *JiraError if an authentication or rate-limiting
// problem is detected.
//
// The following cases are handled:
//   - 401 Unauthorized: Invalid credentials (e.g., bad API token).
//   - 403 Forbidden: Valid credentials but insufficient permissions.
//   - 429 Too Many Requests: Jira API throttling; caller should retry
//     after the delay indicated in the Retry-After header.
//   - 200 OK with X-Seraph-LoginReason header: Jira sometimes returns
//     200 but indicates authentication issues in this header.
//
// For all other 4xx/5xx statuses, a JiraError is returned with the status text as the reason.
// On success (no detected problem), CheckJiraAuth returns nil and the caller should proceed to read from the response body.
func CheckJiraAuth(resp *http.Response) ([]byte, error)

seems to have a lot in common with the retries and most likely be integrated there - maybe we can reuse the error now each package has it's own.

@mmatczuk
Copy link
Collaborator

mmatczuk commented Oct 9, 2025

I'd like to suggest we add internal/httpclient package for all the general http concerns and configurations. We already have one in benthos. We could either move it from internal to public there or use it as inspiration and copy relevant parts to connect. I think the latter makes more sense given that we'd avoid work coordinate across repos.

Given that we'd like to add more HTTP based processors it makes sense to put pure HTTP concerns is a dedicated package. Then http_metrics should go there, as well as logging, PROXY setting and autodetection that I think will be important.

cc @Jeffail

@mmatczuk
Copy link
Collaborator

mmatczuk commented Oct 9, 2025

nit: could we rename jirahttp.JiraHttp to jirahttp.Client, and merge client.go and jira_http.go?

@mmatczuk
Copy link
Collaborator

mmatczuk commented Oct 9, 2025

We'd take care of internal/httpclient later.

// It holds the client state and orchestrates calls into the jirahttp package.
type jiraProcessor struct {
log *service.Logger
jiraHttp *jirahttp.Client
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For clarity let's rename that to client.

@mmatczuk
Copy link
Collaborator

@zoltancsontosness I think that in general we are good to go.

Last items:

  • please move jira_helper contents to jirahttp directly as it belongs there
  • please move http_metrics package under jirahttp package i.e. jirahttp/http_metrics, and remove the helpers now empty dir
  • please add row in info.csv with enterprise license
  • please run task docs to generate the docs

@mmatczuk
Copy link
Collaborator

Please make sure lining and testing works.

@atudose-ness
Copy link
Contributor

@mmatczuk can you please run the pipeline again to see if the linting passes? locally it runs successfully

@atudose-ness
Copy link
Contributor

tried generating the docs with task docs, but nothing is generated. this is the output of the task:

task docs
task: [target-dir] mkdir -p 'target'
task: [build:redpanda-connect] go build  -tags "" -ldflags "-w -s -X main.Version=v -X main.DateBuilt=2025-10-15T17:09:16Z" -o target/redpanda-connect ./cmd/redpanda-connect
task: [docs] go run -tags "" ./cmd/tools/docs_gen
task: [docs] go run -tags "" ./cmd/tools/plugins_csv_fmt
task: [docs] target/redpanda-connect lint --deprecated "./config/examples/*.yaml" "./docs/modules/**/*.md"
task: [docs] target/redpanda-connect template lint "./config/template_examples/*.yaml"

@mmatczuk
Copy link
Collaborator

Work moved to #3699 closing this.

@mmatczuk mmatczuk closed this Oct 16, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants