Sonarqube 이슈 엑셀 레포트 생성

Sonarqube는 널리 알려진 오픈소스 코드 품질 점검 도구이다.

공공SI 프로젝트에서도 Sonarqube는 거의 필수로 사용되고 있다. 내부적인 소스코드 품질 향상을 위한 것도 있지만, 요샌 거의 감리 받을때도 필수로 지켜야 되고, 발주 주관기관에서도 아예 PMD나 Checkstyle, FindBug 자체 룰을 관리 하는 곳도 있다.

소스코드 분석은 다양한 툴들을 통해 할 수 있다. Maven, Gradle, Sonar-scanner 등을 통해 CI서버와의 연결을 통해 많이들 자동 빌드 및 코드 점검 환경을 구축하고 있다.

관련 튜토리얼은 지천에 널리 알려 있으니 마음만 먹으면 쉽게 점검 환경을 구축할 수 있어 많은 사람들의 사랑을 받는 점검 도구이다.

포스팅을 하는 이 시점엔 5.3 버전까지 나와 있는 상태이고 추가되는 언어나 신규 룰들도 재빠르게 업데이트 되는 편이다.

근데 한가지 좀 불편한게 Excel로 직접 Export하는 기능은 없다. 보통 한개 프로젝트 하면 어떤 곳에선 많으면 20~30 개 시스템에 대해서 이슈 레포팅을 뽑아서 감리 수감을 해야 하는 경우나 고객 보고 용도로 레포팅을 뽑아야 하는 경우가 가끔 발생하는데 레포트 뽑기가 참 수월치가 않았다

예전에 3.x대 버전이었을 경우 CSV Export Plugin 쓰다가 Deprecate 된 후로 PDF Report Plugin도 사용은 했으나 원하는 레포트를 뽑아내기가 영 수월치 않았다.

그래서 Sonarqube에서도 권고 하고 있는 Web Service API를 사용해서 Excel로 레포팅을 뽑고 있었는데 이번에 5.3버전으로 업그레이드 하면서 이슈 검색하는 API의 파라미터들이 일부 변경이 되서 약간 수정하는 겸 Excel로 레포팅 하는 방법을 정리해 본다.

사실 스크립트를 만드는 시점에 돌아가는 것에 초점을 맞춘 스크립트이기 때문에 참고용도로 활용하기 바란다.

SonarQube Web Service API

사용할 Web Service APIGET api/issues/search를 사용해서 조건에 맞게 레포트를 생성하게 된다. 기존에 4.x 버전 때에는 componentRoots 파라미터를 사용해서 검색했었는데 5.1부터 deprecate되서 componentKeysprojectKeys 파라미터를 사용해서 프로젝트 별로 검색이 가능하다. 설치 되어 있는 Sonarqube에서 https://nemo.sonarqube.org/api_documentation/api로 들어가면 전체 제공되는 API들을 확인해 볼 수 있다.

GET api/issues/search

이슈 검색을 위한 API로 검색 권한을 가지고 있어야 한다.

주요 Parameters

상세한 검색을 위해서는 더 많은 파라미터를 사용할 수 있으나, 에제에는 아래의 파라미터만 사용해서 검색조건에 활용했다.

componentKeys optional
To retrieve issues associated to a specific list of components sub-components (comma-separated list of component keys). A component can be a view, developer, project, module, directory or file. If this parameter is set, componentUuids must not be set.
Example value: my_project

projectKeys optional
To retrieve issues associated to a specific list of projects (comma-separated list of project keys). This parameter is mostly used by the Issues page, please prefer usage of the componentKeys parameter. If this parameter is set, projectUuids must not be set.
Example value: my_project

ps optional
Page size. Must be greater than 0 and less than 500
Default value: 100
Example value: 20

severities optional
Comma-separated list of severities
INFO, MINOR, MAJOR, CRITICAL, BLOCKER
Example value: BLOCKER,CRITICAL

예를 들면 GET 방식으로 /api/issues/search?componentKeys=my-project-id&severities=BLOCKER&ps=100 를 호출해보면 아래아 같은 json 결과값을 리턴해준다.

{
  "paging": {
    "pageIndex": 1,
    "pageSize": 100,
    "total": 1
  },
  "issues": [
    {
      "key": "01fc972e-2a3c-433e-bcae-0bd7f88f5123",
      "component": "com.github.kevinsawicki:http-request:com.github.kevinsawicki.http.HttpRequest",
      "project": "com.github.kevinsawicki:http-request",
      "rule": "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.MagicNumberCheck",
      "status": "RESOLVED",
      "resolution": "FALSE-POSITIVE",
      "severity": "MINOR",
      "message": "'3' is a magic number.",
      "line": 530,
      "textRange": {
        "startLine": 81,
        "endLine": 81,
        "startOffset": 0,
        "endOffset": 134
      },
      "author": "Developer 1",
      "debt": "2h1min",
      "creationDate": "2013-05-13T17:55:39+0200",
      "updateDate": "2013-05-13T17:55:39+0200",
      "tags": ["bug"],
      "comments": [
        {
          "key": "7d7c56f5-7b5a-41b9-87f8-36fa70caa5ba",
          "login": "john.smith",
          "htmlText": "Must be "final"!",
          "markdown": "Must be \"final\"!",
          "updatable": false,
          "createdAt": "2013-05-13T18:08:34+0200"
        }
      ],
      "attr": {
        "jira-issue-key": "SONAR-1234"
      },
      "transitions": [
        "unconfirm",
        "resolve",
        "falsepositive"
      ],
      "actions": [
        "comment"
      ]
    }
  ],
  "components": [
    {
      "key": "com.github.kevinsawicki:http-request:src/main/java/com/github/kevinsawicki/http/HttpRequest.java",
      "enabled": true,
      "qualifier": "FIL",
      "name": "HttpRequest.java",
      "longName": "src/main/java/com/github/kevinsawicki/http/HttpRequest.java",
      "path": "src/main/java/com/github/kevinsawicki/http/HttpRequest.java"
    },
    {
      "key": "com.github.kevinsawicki:http-request",
      "enabled": true,
      "qualifier": "TRK",
      "name": "http-request",
      "longName": "http-request"
    }
  ],
  "rules": [
    {
      "key": "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.MagicNumberCheck",
      "name": "Magic Number",
      "status": "READY",
      "lang": "java",
      "langName": "Java"
    }
  ],
  "users": [
    {
      "login": "admin",
      "name": "Administrator",
      "active": true,
      "email": "admin@sonarqube.org"
    }
  ]

}

서비스 호출 실패 시에는 아래와 같이 메세지가 전달된다.

{
  "errors": [
  {
    "msg": "Value of parameter 'statuses' (REOPEN) must be one of: [OPEN, CONFIRMED, REOPENED, RESOLVED, CLOSED"
  }
  ]
}

Excel 매크로 작성

ExcelExport하는 매크로 작성 순서는 아래와 같다.

  1. Visual Basic Editor를 사용하여 매크로 모듈 생성
  2. 조회 파라미터 조건 설정
  3. MSXMLl2.ServerXMLHTTP.6.0 라이브러리를 통해 Web Service API를 호출
  4. 결과 json 오브젝트를 vbscript 오브젝트들로 변환
    • VB-JSON 오픈소스 vb6 클래스 라이브러리 사용
  5. 워크시트에 조회해 온 내용 삽입

윈도우면 개발자 도구 - Visual Basic Editor를 실행해서 메뉴 - 삽입 - 모듈 을 통해 모듈 하나를 생성하고 아래와 같이 코드를 추가한다.

Private Const REQUEST_COMPLETE = 4
Private Const PAGE_SIZE = 100
Private Const DATA_FIRST_ROW = 12

Private m_xmlhttp as Object
Private m_respone as String
Pirvate m_currentRow as Long
Private m_currentPageIndex as Long

' 쉬트 내용 초기화
Sub initSheet()
    Dim sht as Worksheet
    Dim lastRow as Long
    
    Set sht = ThisWorkbook.Worksheets("Sheet1")
    
    lastRow = sht.Range("A11").CurrentRegion.rows.Count
    
    If lastRow > 1 Then
        rows(DATA_FIRST_ROW & ":" & lastRow + DATA_FIRST_ROW).EntireRow.Delete
    End If
End Sub

' Sonar Web Service 호출
Sub getSonarData_Click()
    Dim reportQueryParamaters as String
    Dim reportQuery As String
    
    If Not IsEmpty(Range("componentKeysParameter")) Then
        reportQueryParameters = reportQueryParameters & "&componentKeys=" & Range("componentKeysParameter").Value     
    End If
    
    If Not IsEmpty(Range("statusesParameter")) Then
        reportQueryParameters = reportQueryParameters & "&statuses=" & Range("statusesParameter").Value     
    End If
    
    If Not IsEmpty(Range("severitiesParameter")) Then
        reportQueryParameters = reportQueryParameters & "&severities=" & Range("severitiesParameter").Value     
    End If
    
    reportQuery = Range("sonarServerURL").Value & "/api/issues/search" & "?" & reportQueryParameters + "&ps=" + PAGE_SIZE
    
    Call getReportData(reportQuery, 1)
End Sub

' HTTP 연결을 통해 Sonar Web Service 호출
Sub getReportData(reportQuery As String, pageIndex as Long)
On Error GoTo handleError
    Dim jsonObj As Dictionary
    Dim jsonRows as Collection
    Dim pagingInfo As Dictionary
    Dim jsonRow as Dictionary
    Dim jsonVal as String
    Dim ws As Worksheet
    Dim currentRow as Long
    Dim startColumn As Long
    Dim element
    Dim totalCnt As Long
    
    Set m_xmlhttp = CreateObject("MSXML2.ServerXMLHTTP.6.0")
    m_xmlhttp.Open "GET", reportQuery & "&p=" & pageIndex, False
    m_xmlhttp.setRequestHeader "Content-Type", "application/text"
    m_xmlhttp.setRequestHeader "Accept", "application/text"
    m_xmlhttp.send
    
    If m_xmlhttp.readyState = REQUEST_COMPLETE Then
        m_response = m_xmlhttp.responseText
    Else
        MsgBox "xmlhttp send error"
        Exit Sub
    end If
    
    ' VBA.json을 사용하여 json 데이터 변환
    Set jsonObj = JSON.parse(m_response)
    Set jsonRows = jsonObj("issues")
    
    ' 작업할 워크시트 지정
    Set ws = Worksheets("Shee1")
    
    For Each jsonRow In jsonRows
        ' 시작 컬럼 위치
        startColumn = 1
        
        For Each element in jsonRow.Items
            jsonVal = ""
            
            Select Case TypeName(element)
                ' textRange 속성은 {} 배열 Dictionary Type으로 변환되어 처리
                Case "Dictionary"
                    For Each key in element.keys
                        jsonVal = jsonVal + keys + ":" + CSTR(element.Item(key)) + ","
                    Next
                
                ' flowm tags 속성은 [] 배열 Collection Type으로 변환되어 처리
                Case "Collection"
                    For Each colVal in element
                        jsonVal = jsonVal + colVal + ","
                    Next
                    
                Case Else
                    jsonVal = element
            End Select
            
            If Right(jsonVal, 1) = "," Then
                jsonVal = Mid(jsonVal, 1, Len(jsonVal) -1)
            End If
            
            ws.Cells(m_currentRow, startColumn) = jsonVal
            startColumn = startColumn + 1
        Next element
        
        m_currentRow = m_currentRow + 1
    Next jsonRow
    
    ' 전체 페이지 수 가져오기
    Set pagingInfo = jsonObj("paging")
    
    totalCnt = pagingInfo.Item("total")
    
    ' 전체 결과를 얻오 올때까지 재귀 호출
    If totalCnt > PAGE_SIZE * pageIndex Then
        Call getReportData(reportQuery, pageIndex + 1)
    End If
    
    Exit Sub
    
handleError:
    error = MsgBox("The report cannot be generated." & chr(13) & "Please check your parameters.", vbCritical, "Error")
    ' error인 경우 결과 json 중에 error 배열에 담겨 있는 값을 적절히 프린트 해도 됨
End Sub

이름 정의 및 수식에 이름 사용을 사용하여 조건에 해당되는 셀을 설정하고(위의 코드에서는 componentKeysParameter, statusesParameter, severitiesParameter 등을 사용했다.) Web Service API를 호출하여 원하는 엑셀의 영역에 json 결과값을 적절하게 가공 후 표시하면 된다 .

몇 가지 Attribute 들의 배열 표현 형식에 따라 Dictionary나 Collection 오브젝트로 변환되는 경우가 있으니 주의하면 되겠다.

버튼 오브젝트 등에 생성한 매크로 모듈 Function을 호출하면 원하는 결과를 얻을 수 있다.

샘플에서 표현하진 않았지만 다양한 검색조건을 활용할 수도 있고 받아온 데이터는 피벗 테이블 등을 이용해 Back-data뿐만 아니라 통계자료로도 활용할 수 있다.

마치며

경함 상 엑셀로 매크로 작업은 정말 유용하게 쓰일 때가 많기 때문에 매크로 작업 때 쓰이는 몇가지 vbscript를 다룰줄 안다면 큰 도움이 된다.

그리고 더 좋은 방법이나 아이디어 가지고 계신 분은 꼭 좀 알려주세요~~ ^^

ps. 인터넷이 안되는 PC에서 작업한 결과물을 타이핑 하다보니 실제 코드가 동작 안할 수 있으니 참조만 하시면 됩니다.