05 de junho, 2024
Atualmente muitas equipes de TI trabalham com metodologias ágeis como o Scrum e utilizam ferramentas como o Jira Software ou Azure Boards para realizar o controle das atividades durante a Sprint. Essas ferramentas por padrão oferecem alternativas de gráficos para acompanhamento das métricas da Sprint, entretanto caso seja desejado realizar análises mais complexas, envolvendo as regras de negócio de cada empresa, é necessário customizar uma solução.
Como uma alternativa para essa customização, este projeto visa criar uma base de dados para construção de um gráfico de Burndown, junto com o tempo gasto pelo time, proporcionando uma análise adicional a essa métrica já tão amplamente utilizada para acompanhamento da Sprint.
Para desenvolvimento do projeto foi criado um quadro de amostra no Jira, com dados fictícios simulando uma equipe de TI que utiliza a metodologia Scrum.
Então, a partir dos dados no Jira, será realizado um processo ELT para disponibilizar os dados de maneira a serem utilizados para construção do gráfico final no Power BI, conforme imagem a seguir:
Essas ferramentas foram escolhidas por possibilitar a realização do projeto com custo zero, e assim representam uma solução que também pode ser aplicada a empresas de pequeno porte, sem representar nenhum custo de manutenção. Além disso, caso alguma empresa já utilize essas ou ferramentas similares, o projeto pode ser adaptado as especificações e necessidades de cada empresa.
Detalhamento de custo por ferramenta:
Para extrair os dados do Jira é necessário configurá-lo como fonte no Airbyte. Para isso, dentro do Airbyte, em fontes, procurar e selecionar a fonte Jira. Primeiramente é necessário ter o API Token para se conectar ao Jira. Caso a conta do Jira ainda não possua um Token, basta seguir a própria documentação do Jira, contida no Airbyte para adquirir o Token. Assim a configuração poderá ser realizada no Airbyte, com a opção de filtrar projeto e data de início para esta fonte:
Os dados serão carregados em um banco de dados Postgresql para serem armazenados e transformados. A fim de ter um ambiente próprio e isolado para a solução, o Postgresql será utilizado a partir de um container Docker. O Docker Desktop é um software de conteinerização seguro e inovador que oferece aos desenvolvedores e equipes um kit de ferramentas híbrido e robusto para criar, compartilhar e executar aplicativos em qualquer lugar.
Para instalar o Docker localmente basta seguir as instruções conforme o sistema operacional em https://www.docker.com/products/docker-desktop/.
O Docker nos permite criar um container totalmente personalizado com os softwares, arquivos e configurações necessárias a partir de um Dockerfile, que irá criar uma imagem no Docker que servirá como template para a criação de containers com as configurações contidas nesse Dockerfile.
Devido ao processo de atualização do Airbyte, que será esclarecido na seção 9, serão necessárias algumas customizações no container, assim para criar o container Docker será utilizada uma imagem personalizada, criada através do seguinte Dockerfile (sem extensão):
# Pegar imagem postgres na versão 16.1 como base
FROM postgres:16.1
# Atualizar e instalar cron e vim
RUN apt-get update && apt-get install -y cron vim
# Definir fuso horário do cron para São Paulo
RUN echo "America/Sao_Paulo" > /etc/timezone
# Definir fuso horário do container para São Paulo
RUN ln -sf /usr/share/zoneinfo/America/Sao_Paulo /etc/localtime
# Copiar o arquivo crontab com as configurações de agendamento
COPY crontab /var/spool/cron/crontabs/root
# Copiar os scripts shell a serem executados pelo crontab
COPY drop_view.sh /tmp
COPY create_view.sh /tmp
# Dar permissão de execução aos arquivos
RUN chmod +x /tmp/drop_view.sh
RUN chmod +x /tmp/create_view.sh
Com o arquivo Dockerfile criado, abrir o terminal na pasta em que está o arquivo e executar o comando (obs.: para executar o comando é necessário ter os arquivos drop_view.sh, create_view.sh e crontab criados, sendo seus códigos apresnetados na seção 9):
docker build -t postgres-cron-vim:16.1 .
Em seguida executar o comando abaixo para criação do container, passando as variáveis de ambiente.
docker run -d --name dashboardscrum -p 5579:5432 -e POSTGRES_DB=DashboardScrum -e POSTGRES_USER=user -e POSTGRES_PASSWORD=XXXX postgres-cron-vim:16.1
Obs.: a porta padrão para rodar o Postgresql é a 5432, mas caso na máquina local já esteja rodando o Postgresql ou a porta 5432 já esteja em uso por algum motivo, poderá ser realizado o mapeamento de portas (-p 5579:5432), assim o banco de dados será acessado pela porta 5579 da máquina local e será redirecionado para a porta 5432 dentro do container, onde o Postgresql estará rodando em sua porta padrão.
Para acessar o banco de dados pode ser utilizado o pgAdmin, disponível para download em https://www.pgadmin.org/download/. Neste projeto o pgAdmin foi instalado localmente, mas também é possível que sua instalação e acesso sejam realizados em um container Docker.
Após acessar a tela do pgAdmin, clicar com o botão direito em Servers > Register > Server...
E posteriormente realizar a configuração conforme a porta e as variáveis de ambiente definidas no comando de criação do container.
Para carregar os dados no Postgresql é necessário configurá-lo como fonte no Airbyte. Para isso, dentro do Airbyte, em destinos, procurar e selecionar o destino Postgres e realizar a configuração conforme as variáveis de ambiente definidas no comando de criação do container:
Para configurar a conexão no Airbyte, em conexões, selecionar a fonte (Jira) e o destino (Postgres). Na seleção de streams selecionar as tabelas, com os respectivos Sync mode (por padrão já vêm selecionadas algumas outras tabelas que deverão ser desmarcadas):
Obs.: ainda dentro da seleção de streams é possível selecionar as colunas que serão extraídas de cada tabela, sendo assim, as colunas a serem trazidas podem ser adaptadas conforme a necessidade da empresa.
Posteriormente será configurado o nome, e o agendamento manual para a conexão, para que possa ser realizada a primeira carga no banco de dados.
Após configurada a conexão, acessá-la em conexões e clicar em Sync now para realizar a primeira carga no banco de dados.
Para construir o gráfico Burndown serão necessárias as seguintes informações:
A fim de disponibilizar esses dados para serem utilizados no Power BI, será construída uma view contendo essas informações.
Após uma análise exploratória e manipulação dos dados no pgAdmin, ficou determinado a construção de 4 funções para serem utilizadas na formação da view. Os próximos tópicos trazem os respectivos códigos.
CREATE FUNCTION cont_dias_sprint(sprint_name VARCHAR)
RETURNS SETOF NUMERIC AS $$
-- Selecionado o número de dias da Sprint menos 1 para divisão na coluna de planejado
SELECT COUNT(intervalo) - 1
FROM generate_series(
-- Gerando série de datas entre o dia de início e dia fim da sprint
(SELECT "startDate" FROM public.sprints WHERE name = sprint_name),
(SELECT "endDate" FROM public.sprints WHERE name = sprint_name),
INTERVAL '1 day'
) AS intervalo
-- Extraindo os dias da semana e filtrando para retirar sábado e domingo
WHERE EXTRACT(dow FROM intervalo) NOT IN (6, 0);
$$ LANGUAGE sql;
CREATE FUNCTION estimado_total(sprint_name VARCHAR)
RETURNS SETOF NUMERIC AS $$
-- Somando a estimativa para cada card e trazendo relação com a sprint
SELECT SUM((CAST((i.fields -> 'timeoriginalestimate') AS INTEGER))/3600)
FROM public.issues AS i
JOIN public.sprint_issues AS si ON i.key = si.key
JOIN public.sprints AS s ON si."sprintId" = s.Id
WHERE s.name = sprint_name
GROUP BY s.name;
$$ LANGUAGE sql;
CREATE OR REPLACE FUNCTION planejado_ideal(sprint_name VARCHAR)
RETURNS TABLE (dia DATE, planejado NUMERIC) AS $$
DECLARE
-- Declarando variável a ser utilizada no loop começando com o valor estimado total para a sprint
current_value NUMERIC(8,6) := estimado_total(sprint_name);
-- Declarando variável de incremento
increment_value NUMERIC(8,6) := estimado_total(sprint_name) / cont_dias_sprint(sprint_name);
BEGIN
FOR dia IN
-- Loop para percorrer os dias da sprint
SELECT intervalo
FROM generate_series(
(SELECT "startDate" FROM public.sprints WHERE name = sprint_name),
(SELECT "endDate" FROM public.sprints WHERE name = sprint_name),
INTERVAL '1 day') AS intervalo
WHERE EXTRACT(dow FROM intervalo) NOT IN (6, 0)
LOOP
-- Começando pelo primeiro valor como o estimado total da sprint
planejado := current_value;
RETURN NEXT;
-- Atualizando próximo valor da coluna sendo o anterior menos o incremento
current_value := current_value - increment_value;
END LOOP;
RETURN;
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION horas_restantes(sprint_name VARCHAR, prev_horas_entregues NUMERIC, datas DATE)
RETURNS TABLE(dia DATE, horas_restantes NUMERIC) AS $$
DECLARE
-- Definindo cursor para percorrer todos os dias de cada sprint
tbl_cursor CURSOR FOR (
SELECT s.name AS sprint, data.data :: DATE
FROM public.sprints AS s
CROSS JOIN generate_series(s."startDate", s."endDate", interval '1 day') AS data
WHERE EXTRACT(dow FROM data.data) NOT IN (6, 0) AND s.name = sprint_name
ORDER BY data
);
tbl_row RECORD;
-- Definindo valor inicial como estimado total para a sprint
horas_restantes NUMERIC := estimado_total(sprint_name);
-- Definindo variáveis de controle para que a cada iteração possam ser selecionadas as horas entregues do dia anterior
horas_entregues NUMERIC := 0;
current_value NUMERIC := 0;
BEGIN
OPEN tbl_cursor;
LOOP
-- Definindo critério de parada para o cursor
FETCH FROM tbl_cursor INTO tbl_row;
EXIT WHEN NOT FOUND;
-- Selecionando as horas entregues
SELECT COALESCE(SUM((issues.fields ->> 'timeoriginalestimate')::INTEGER / 3600), 0)
INTO horas_entregues
FROM issues
WHERE to_date(issues.fields ->> 'resolutiondate', 'YYYY-MM-DD') = tbl_row.data;
-- Subtraindo o valor da coluna horas restantes pelo valor de horas entregues
horas_restantes := horas_restantes - current_value;
-- Armazenando o valor de horas entregues na data x, para que na próxima interação seja feita a
-- subtração por esse valor, que representará o valor de horas entregues na data anterior (x-1)
current_value := horas_entregues;
-- Retornando a data e o valor de horas_restante a cada iteração
RETURN QUERY SELECT tbl_row.data, horas_restantes;
END LOOP;
CLOSE tbl_cursor;
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE VIEW public.vw_dadosburndown AS
WITH dados_sprint AS (
SELECT
s.name AS sprint,
data.data :: DATE
FROM public.sprints AS s
CROSS JOIN generate_series(s."startDate", s."endDate", INTERVAL '1 day') AS data
WHERE EXTRACT(dow FROM data.data) NOT IN (6, 0)
ORDER BY data
),
dados_horas_entregues AS(
SELECT
SUM((CAST((fields -> 'timeoriginalestimate') AS INTEGER))/3600) AS horas_entregues,
to_date(fields ->> 'resolutiondate', 'YYYY-MM-DD') AS data_encerramento
FROM public.issues
GROUP BY data_encerramento
),
prev_dados_tempo_gasto AS(
SELECT
CAST((jsonb_array_elements(fields -> 'worklog' -> 'worklogs') -> 'timeSpentSeconds') AS INTEGER)/3600 AS tempo_gasto,
to_date(jsonb_array_elements(fields -> 'worklog' -> 'worklogs') ->> 'updated', 'YYYY-MM-DD') AS dia_gasto
FROM public.issues
),
dados_tempo_gasto AS(
SELECT
dia_gasto,
SUM(tempo_gasto) AS tempo_gasto
FROM prev_dados_tempo_gasto
GROUP BY dia_gasto
)
SELECT
ds.sprint,
ds.data,
dhe.horas_entregues,
dtg.tempo_gasto,
plan.planejado,
he.horas_restantes
FROM dados_sprint AS ds
LEFT JOIN LATERAL planejado_ideal(ds.sprint) AS plan ON ds.data = plan.dia
LEFT JOIN dados_horas_entregues AS dhe ON ds.data = dhe.data_encerramento
LEFT JOIN dados_tempo_gasto AS dtg ON ds.data = dtg.dia_gasto
JOIN horas_restantes(ds.sprint, dhe.horas_entregues, ds.data) AS he ON ds.data = he.dia
ORDER BY ds.sprint,ds.data;
No pgAdmin a view retorna a seguinte tabela com os dados. A seguir será realizada a conexão do Postgresql com o Power Bi para construção do gráfico Burndown com tempo gasto.
No Power Bi deverá ser acessada a fonte "Banco de dados PostgreSQL". Para o projeto em questão o Power BI foi acessado por outra máquina, assim no servidor é preciso configurar o IP, e a porta (caso tenha sido mapeada uma porta diferente na criação do container):
Posteriormente colocar usuário e senha conforme as variáveis de ambiente definidas no comando de criação do container:
E por fim selecionar a view criada entre as tabelas disponíveis:
Como pode se observar no gráfico, para a sprint em questão, as entregas estavam atrasadas (horas restantes acima da linha de planejado) até o dia 10/06. Então verifica-se um registro de tempo gasto maior nesse dia e no seguinte, o que pode representar a melhora no desempenho nos últimos dias da sprint. Além disso, outra vantagem de acompanhar o registro de tempo gasto por dia pode ser o auxilio na maturidade do time, quando ainda não se tem um bom controle de horas.
Para realizar a atualização dos dados é possível agendá-la no Airbyte. Entretanto, para este projeto será necessária uma pequena adaptação. A tabela "sprints" não possui uma coluna que possibilitaria realizar o apend incremental dos dados, portanto o seu modo de sincronização é o "Full refresh | Overwrite". Neste modo o Airbyte dropa (deleta) a tabela para então construí-la novamente com os dados atualizados. Porém, como a tabela "sprints" é utilizada na view no bando de dados, ela não pode ser deletada. Para contornar essa questão, a solução proposta foi agendar via CRON no container o drop e criação da view e a atualização do Airbyte ser realizada entre esses dois comandos.
Por isso na criação do container foram copiados os arquivos drop_view.sh, create_view.sh e crontab que representam, respectivamente, os arquivos com os comando para deletar a view, criar a view e o agendamento no CRON. Os próximos tópicos trazem esses códigos.
#!/bin/bash
# Definindo as variáveis de ambiente para o PostgreSQL
export PGHOST=localhost
export PGUSER=user
export PGPASSWORD=XXXXX
export PGDATABASE=DashboardScrum
# Executando o comando para deletar a view
psql -c "DROP VIEW public.vw_dadosburndown;"
#!/bin/bash
# Definindo as variáveis de ambiente para o PostgreSQL
export PGHOST=localhost
export PGUSER=gpb
export PGPASSWORD=gpb5579
export PGDATABASE=DashboardScrum
# Váriavél com a criação da view
VIEW_DEFINITION="
CREATE OR REPLACE VIEW public.vw_dadosburndown AS
WITH dados_sprint AS (
SELECT
s.name AS sprint,
data.data :: DATE
FROM public.sprints AS s
CROSS JOIN generate_series(s.\"startDate\", s.\"endDate\", INTERVAL '1 day') AS data
WHERE EXTRACT(dow FROM data.data) NOT IN (6, 0)
ORDER BY data
),
dados_horas_entregues AS(
SELECT
SUM((CAST((fields -> 'timeoriginalestimate') AS INTEGER))/3600) AS horas_entregues,
to_date(fields ->> 'resolutiondate', 'YYYY-MM-DD') AS data_encerramento
FROM public.issues
GROUP BY data_encerramento
),
prev_dados_tempo_gasto AS(
SELECT
CAST((jsonb_array_elements(fields -> 'worklog' -> 'worklogs') -> 'timeSpentSeconds') AS INTEGER)/3600 AS tempo_gasto,
to_date(jsonb_array_elements(fields -> 'worklog' -> 'worklogs') ->> 'updated', 'YYYY-MM-DD') AS dia_gasto
FROM public.issues
),
dados_tempo_gasto AS(
SELECT
dia_gasto,
SUM(tempo_gasto) AS tempo_gasto
FROM prev_dados_tempo_gasto
GROUP BY dia_gasto
)
SELECT
ds.sprint,
ds.data,
dhe.horas_entregues,
dtg.tempo_gasto,
plan.planejado,
he.horas_restantes
FROM dados_sprint AS ds
LEFT JOIN LATERAL planejado_ideal(ds.sprint) AS plan ON ds.data = plan.dia
LEFT JOIN dados_horas_entregues AS dhe ON ds.data = dhe.data_encerramento
LEFT JOIN dados_tempo_gasto AS dtg ON ds.data = dtg.dia_gasto
JOIN horas_restantes(ds.sprint, dhe.horas_entregues, ds.data) AS he ON ds.data = he.dia
ORDER BY ds.sprint,ds.data;"
# Executando o comando de criação da view
psql -c "$VIEW_DEFINITION"
0 3 * * 1-5 /tmp/drop_view.sh >> /tmp/cron_drop.log 2>&1
0 5 * * 1-5 /tmp/create_view.sh >> /tmp/cron_create.log 2>&1
Dessa forma os scripts serão executados às 3:00h e 5:00h da manhã de segunda a sexta-feira, e sua saídas/erros serão gravadas nos arquivos cron_drop.log e cron_create.log dentro da pasta tmp do container.
Obs.: utilizando >> serão gravados todos os logs. Utilizando > será gravado apenas o log da útlima execução.
Para ativar os comandos do CRON é necessário entrar no terminal do container e, estando na linguagem bash, executar o seguinte comando:
service cron start
Para verificar se o CRON está de fato rodando pode ser utilizado o comando:
service cron status
Por fim é necessário agendar a atualização no Airbyte, que pode ser feita em Settings, na conexão, conforme a imagem a seguir:
Dessa forma, às 3:00h a view será deletada do banco de dados, às 04:00h o Airbyte irá realizar a atualização do bando de dados e às 5:00h a view será recriada, deixando os dados disponíveis para serem atualizados no Power BI normalmente. Esta é apenas uma sugestão de configuração, os horários podem ser alterados a depender das necessidades da empresa.
O gráfico de Burndown com tempo gasto é apenas um exemplo do que pode ser feito obtendo os dados brutos do Jira e transformando-os, sendo possível adaptar essa transformação conforme as necessidade e regras de negócio de cada empresa. Vale lembrar que existem plug-ins e aplicativos que podem ser adicionados ao Jira para gráficos personalizados, entretanto nem sempre são gratuitos e trazendo a solução dessa forma, a flexibilidade tanto para construir os gráficos, quanto para implementar as regras de negócio da empresa é muito maior.
Caso tenha alguma dúvida, sugestão ou apenas queira trocar uma ideia sobre este projeto, fico à disposição.
E-mail: gabrielapbarros15@gmail.com
LinkedIn: https://www.linkedin.com/in/gabriela-pereira-barros/