From e42e373de4c90ebd10a69af4bb0a76f063a46dcc Mon Sep 17 00:00:00 2001 From: Martin Edenhofer Date: Mon, 18 Apr 2016 02:02:31 +0200 Subject: [PATCH] Added http log support. --- .../app/controllers/_integration/_base.coffee | 21 ++--- .../controllers/_integration/icinga.coffee | 5 +- .../controllers/_integration/nagios.coffee | 5 +- .../app/controllers/_integration/slack.coffee | 16 +++- .../app/controllers/widget/http_log.coffee | 55 +++++++++++ .../app/views/integration/base.jst.eco | 1 + .../app/views/widget/http_log.jst.eco | 21 +++++ .../app/views/widget/http_log_show.jst.eco | 27 ++++++ app/assets/stylesheets/zammad.scss | 5 +- app/controllers/http_logs_controller.rb | 23 +++++ app/models/http_log.rb | 24 +++++ app/models/transaction/slack.rb | 68 ++++++++++---- app/views/slack/ticket_escalation/en.md.erb | 2 +- .../slack/ticket_escalation_warning/en.md.erb | 2 +- config/routes/http_log.rb | 8 ++ db/migrate/20160417000002_add_http_log.rb | 32 +++++++ lib/user_agent.rb | 71 +++++++++++++- test/integration/slack_test.rb | 93 +++++++++++++++++++ 18 files changed, 432 insertions(+), 47 deletions(-) create mode 100644 app/assets/javascripts/app/controllers/widget/http_log.coffee create mode 100644 app/assets/javascripts/app/views/widget/http_log.jst.eco create mode 100644 app/assets/javascripts/app/views/widget/http_log_show.jst.eco create mode 100644 app/controllers/http_logs_controller.rb create mode 100644 app/models/http_log.rb create mode 100644 config/routes/http_log.rb create mode 100644 db/migrate/20160417000002_add_http_log.rb diff --git a/app/assets/javascripts/app/controllers/_integration/_base.coffee b/app/assets/javascripts/app/controllers/_integration/_base.coffee index 76fa0d629..dfe224a8d 100644 --- a/app/assets/javascripts/app/controllers/_integration/_base.coffee +++ b/app/assets/javascripts/app/controllers/_integration/_base.coffee @@ -16,6 +16,8 @@ class App.ControllerIntegrationBase extends App.Controller return if !@authenticate(false, 'Admin') @title @featureName, true + @initalRender = true + @subscribeId = App.Setting.subscribe(@render, initFetch: true, clear: false) switch: => @@ -23,17 +25,14 @@ class App.ControllerIntegrationBase extends App.Controller App.Setting.set(@featureIntegration, value) render: => - localEl = $( App.view('integration/base')( - header: @featureName - description: @description - feature: @featureIntegration - featureEnabled: App.Setting.get(@featureIntegration) - )) - @form localEl - @html localEl - - form: (localEl) -> - console.log('implement own form method') + if @initalRender + @html App.view('integration/base')( + header: @featureName + description: @description + feature: @featureIntegration + featureEnabled: App.Setting.get(@featureIntegration) + ) + @initalRender = false submit: (e) => e.preventDefault() diff --git a/app/assets/javascripts/app/controllers/_integration/icinga.coffee b/app/assets/javascripts/app/controllers/_integration/icinga.coffee index 3c486a06b..9784a5698 100644 --- a/app/assets/javascripts/app/controllers/_integration/icinga.coffee +++ b/app/assets/javascripts/app/controllers/_integration/icinga.coffee @@ -7,10 +7,11 @@ class Index extends App.ControllerIntegrationBase ['If the host and service is recovered again, the ticket will be closed automatically.'] ] - form: (localeEl) -> + render: => + super new App.SettingsForm( area: 'Integration::Icinga' - el: localeEl.find('.js-form') + el: @$('.js-form') ) class State diff --git a/app/assets/javascripts/app/controllers/_integration/nagios.coffee b/app/assets/javascripts/app/controllers/_integration/nagios.coffee index ab8bf6394..429c3913e 100644 --- a/app/assets/javascripts/app/controllers/_integration/nagios.coffee +++ b/app/assets/javascripts/app/controllers/_integration/nagios.coffee @@ -7,10 +7,11 @@ class Index extends App.ControllerIntegrationBase ['If the host and service is recovered again, the ticket will be closed automatically.'] ] - form: (localeEl) -> + render: => + super new App.SettingsForm( area: 'Integration::Nagios' - el: localeEl.find('.js-form') + el: @$('.js-form') ) class State diff --git a/app/assets/javascripts/app/controllers/_integration/slack.coffee b/app/assets/javascripts/app/controllers/_integration/slack.coffee index de3169477..906c92d69 100644 --- a/app/assets/javascripts/app/controllers/_integration/slack.coffee +++ b/app/assets/javascripts/app/controllers/_integration/slack.coffee @@ -7,7 +7,8 @@ class Index extends App.ControllerIntegrationBase ['To setup this Service you need to create a new |"Incoming webhook"| in your %s integration panel, and enter the Webhook URL below.', 'Slack'] ] - form: (localEl) => + render: => + super params = App.Setting.get(@featureConfig) if params && params.items @@ -24,10 +25,10 @@ class Index extends App.ControllerIntegrationBase { name: 'types', display: 'Trigger', tag: 'checkbox', options: options, 'null': false, class: 'vertical', note: 'Where notification is sent.' }, { name: 'group_id', display: 'Group', tag: 'select', relation: 'Group', multiple: true, 'null': false, note: 'Only for this groups.' }, { name: 'webhook', display: 'Webhook', tag: 'input', type: 'text', limit: 200, 'null': false, placeholder: 'https://hooks.slack.com/services/...' }, - { name: 'username', display: 'username', tag: 'input', type: 'text', limit: 100, 'null': false, placeholder: 'username' }, - { name: 'channel', display: 'channel', tag: 'input', type: 'text', limit: 100, 'null': true, placeholder: '#channel' }, + { name: 'username', display: 'Username', tag: 'input', type: 'text', limit: 100, 'null': false, placeholder: 'username' }, + { name: 'channel', display: 'Channel', tag: 'input', type: 'text', limit: 100, 'null': true, placeholder: '#channel' }, ] - console.log('p', params) + settings = [] for item in configureAttributes setting = @@ -54,7 +55,12 @@ class Index extends App.ControllerIntegrationBase params: localParams ) - localEl.find('.js-form').html(formEl) + @$('.js-form').html(formEl) + + new App.HttpLog( + el: @$('.js-log') + facility: 'slack_webhook' + ) class State @current: -> diff --git a/app/assets/javascripts/app/controllers/widget/http_log.coffee b/app/assets/javascripts/app/controllers/widget/http_log.coffee new file mode 100644 index 000000000..01951edad --- /dev/null +++ b/app/assets/javascripts/app/controllers/widget/http_log.coffee @@ -0,0 +1,55 @@ +class App.HttpLog extends App.Controller + events: + 'click .js-record': 'show' + + constructor: -> + super + @fetch() + @records = [] + + fetch: => + @ajax( + id: 'http_logs' + type: 'GET' + url: "#{@apiPath}/http_logs/#{@facility}" + data: + limit: @limit || 50 + processData: true + success: (data) => + @records = data + @render() + ) + + render: => + @html App.view('widget/http_log')( + records: @records + ) + #@delay(message, 2000) + + show: (e) => + e.preventDefault() + record_id = $(e.currentTarget).data('id') + for record in @records + if record_id.toString() is record.id.toString() + new Show( + record: record + container: @el.closest('.content') + ) + return + +class Show extends App.ControllerModal + authenticateRequired: true + large: true + head: 'HTTP Log' + buttonClose: true + buttonCancel: false + buttonSubmit: false + + constructor: -> + super + + content: -> + console.log('cont') + App.view('widget/http_log_show')( + record: @record + ) diff --git a/app/assets/javascripts/app/views/integration/base.jst.eco b/app/assets/javascripts/app/views/integration/base.jst.eco index d941bd965..272620104 100644 --- a/app/assets/javascripts/app/views/integration/base.jst.eco +++ b/app/assets/javascripts/app/views/integration/base.jst.eco @@ -14,4 +14,5 @@ <% end %> <% end %>
+
\ No newline at end of file diff --git a/app/assets/javascripts/app/views/widget/http_log.jst.eco b/app/assets/javascripts/app/views/widget/http_log.jst.eco new file mode 100644 index 000000000..4f3790b12 --- /dev/null +++ b/app/assets/javascripts/app/views/widget/http_log.jst.eco @@ -0,0 +1,21 @@ +
+ +<%- @T('Recent logs') %> +
+ + + + + +<% for record in @records: %> + + +
<%- @T('Direction') %> + <%- @T('Request') %> + <%- @T('Created at') %> +
<%- @T(record.direction) %> + <%= record.status %> <%= record.method %> <%= record.url %> + <%- @datetime(record.created_at) %> +<% end %> +
+
diff --git a/app/assets/javascripts/app/views/widget/http_log_show.jst.eco b/app/assets/javascripts/app/views/widget/http_log_show.jst.eco new file mode 100644 index 000000000..d09625666 --- /dev/null +++ b/app/assets/javascripts/app/views/widget/http_log_show.jst.eco @@ -0,0 +1,27 @@ +
+ + + + + + + + + + +
<%- @T('Direction') %> + <%- @T(@record.direction) %> +
<%- @T('URL') %> + <%= @record.url %> +
<%- @T('Method') %> + <%= @record.method %> +
<%- @T('Status') %> + <%= @record.status %> +
<%- @T('Request') %> + <%- App.Utils.text2html(@record.request.content) %> +
<%- @T('Response') %> + <%- App.Utils.text2html(@record.response.content) %> +
<%- @T('Created at') %> + <%- @datetime(@record.created_at) %> +
+
diff --git a/app/assets/stylesheets/zammad.scss b/app/assets/stylesheets/zammad.scss index 4c7c0eb01..ac4904416 100644 --- a/app/assets/stylesheets/zammad.scss +++ b/app/assets/stylesheets/zammad.scss @@ -6929,7 +6929,9 @@ output { background: white; table-layout: auto; margin-bottom: 20px; - + word-break: break-all; + word-wrap: break-word; + &.is-invalid { border-radius: 3px; box-shadow: @@ -6975,6 +6977,7 @@ output { letter-spacing: 0.05em; background: hsl(197,20%,93%); border-bottom: none; + word-break: normal; } td.empty-cell { diff --git a/app/controllers/http_logs_controller.rb b/app/controllers/http_logs_controller.rb new file mode 100644 index 000000000..87ce0aece --- /dev/null +++ b/app/controllers/http_logs_controller.rb @@ -0,0 +1,23 @@ +# Copyright (C) 2012-2014 Zammad Foundation, http://zammad-foundation.org/ + +class HttpLogsController < ApplicationController + before_action :authentication_check + + # GET /http_logs/:facility + def index + return if deny_if_not_role(Z_ROLENAME_ADMIN) + list = if params[:facility] + HttpLog.where(facility: params[:facility]).order('created_at DESC').limit(params[:limit] || 50) + else + HttpLog.order('created_at DESC').limit(params[:limit] || 50) + end + model_index_render_result(list) + end + + # POST /http_logs + def create + return if deny_if_not_role(Z_ROLENAME_ADMIN) + model_create_render(HttpLog, params) + end + +end diff --git a/app/models/http_log.rb b/app/models/http_log.rb new file mode 100644 index 000000000..364b9f9fa --- /dev/null +++ b/app/models/http_log.rb @@ -0,0 +1,24 @@ +# Copyright (C) 2012-2014 Zammad Foundation, http://zammad-foundation.org/ + +class HttpLog < ApplicationModel + store :request + store :response + +=begin + +cleanup old http logs + + HttpLog.cleanup + +optional you can parse the max oldest chat entries + + HttpLog.cleanup(1.month) + +=end + + def self.cleanup(diff = 1.month) + HttpLog.where('created_at < ?', Time.zone.now - diff).delete_all + true + end + +end diff --git a/app/models/transaction/slack.rb b/app/models/transaction/slack.rb index 2499f7a18..e5ae6e880 100644 --- a/app/models/transaction/slack.rb +++ b/app/models/transaction/slack.rb @@ -2,6 +2,14 @@ class Transaction::Slack =begin + +backend = Transaction::Slack.new( + object: 'Ticket', + type: 'create', + ticket_id: 1, +) +backend.perform + { object: 'Ticket', type: 'update', @@ -83,24 +91,30 @@ class Transaction::Slack # check action if item['types'] - hit = false - item['types'].each {|type| - next if type.to_s != @item[:type].to_s - hit = true - break - } - next if !hit + if item['types'].class == Array + hit = false + item['types'].each {|type| + next if type.to_s != @item[:type].to_s + hit = true + break + } + next if !hit + end + next if item['types'].to_s != @item[:type].to_s end # check group if item['group_ids'] - hit = false - item['group_ids'].each {|group_id| - next if group_id.to_s != ticket.group_id.to_s - hit = true - break - } - next if !hit + if item['group_ids'].class == Array + hit = false + item['group_ids'].each {|group_id| + next if group_id.to_s != ticket.group_id.to_s + hit = true + break + } + next if !hit + end + next if item['group_ids'].to_s != ticket.group_id.to_s end Rails.logger.debug "sent webhook (#{@item[:type]}/#{ticket.id}/#{item['webhook']})" @@ -108,8 +122,9 @@ class Transaction::Slack item['webhook'], channel: item['channel'], username: item['username'], - icon_url: logo_url, + #icon_url: logo_url, mrkdwn: true, + http_client: Transaction::Slack::Client, ) if item['expand'] body = "#{result[:subject]}\n#{result[:body]}" @@ -123,11 +138,9 @@ class Transaction::Slack result = notifier.ping result[:subject], attachments: [attachment] end - if !result - Rails.logger.error "Unable to post webhook: #{item['webhook']}" - end - if result.code.to_s != '200' && result.code.to_s != '201' + if !result.success? Rails.logger.error "Unable to post webhook: #{item['webhook']}: #{result.inspect}" + next end Rails.logger.debug "sent webhook (#{@item[:type]}/#{ticket.id}/#{item['webhook']})" } @@ -224,4 +237,21 @@ class Transaction::Slack changes end + class Transaction::Slack::Client + def self.post(uri, params = {}) + UserAgent.post( + uri.to_s, + params, + { + open_timeout: 4, + read_timeout: 10, + total_timeout: 20, + log: { + facility: 'slack_webhook', + } + }, + ) + end + end + end diff --git a/app/views/slack/ticket_escalation/en.md.erb b/app/views/slack/ticket_escalation/en.md.erb index 330998489..e4853da66 100644 --- a/app/views/slack/ticket_escalation/en.md.erb +++ b/app/views/slack/ticket_escalation/en.md.erb @@ -1,6 +1,6 @@ # <%= d 'ticket.title' %> _<<%= c 'http_type' %>://<%= c 'fqdn' %>/#ticket/zoom/<%= d 'ticket.id' %>|Ticket#<%= d 'ticket.number' %>>: Escalated at <%= d 'ticket.escalation_time' %>_ -A ticket (<%= d 'ticket.title' %>) from "<%= d 'ticket.customer.longname' %>" is escalated since "<%= d 'ticket.escalation_time' %>"! +A ticket (<%= d 'ticket.title' %>) from "<%= d 'ticket.customer.longname' %>" is escalated since "<%= d 'ticket.escalation_time' %>"! <% if @objects[:article] %> <%= a_text 'article' %> diff --git a/app/views/slack/ticket_escalation_warning/en.md.erb b/app/views/slack/ticket_escalation_warning/en.md.erb index 15f48c0a3..806c49fba 100644 --- a/app/views/slack/ticket_escalation_warning/en.md.erb +++ b/app/views/slack/ticket_escalation_warning/en.md.erb @@ -1,6 +1,6 @@ # <%= d 'ticket.title' %> _<<%= c 'http_type' %>://<%= c 'fqdn' %>/#ticket/zoom/<%= d 'ticket.id' %>|Ticket#<%= d 'ticket.number' %>>: Will escalate at <%= d 'ticket.escalation_time' %>_ -A ticket (<%= d 'ticket.title' %>) from "<%= d 'ticket.customer.longname' %>" will escalate at "<%= d 'ticket.escalation_time' %>"! +A ticket (<%= d 'ticket.title' %>) from "<%= d 'ticket.customer.longname' %>" will escalate at "<%= d 'ticket.escalation_time' %>"! <% if @objects[:article] %> <%= a_text 'article' %> diff --git a/config/routes/http_log.rb b/config/routes/http_log.rb new file mode 100644 index 000000000..2dad26384 --- /dev/null +++ b/config/routes/http_log.rb @@ -0,0 +1,8 @@ +Zammad::Application.routes.draw do + api_path = Rails.configuration.api_path + + match api_path + '/http_logs', to: 'http_logs#index', via: :get + match api_path + '/http_logs/:facility', to: 'http_logs#index', via: :get + match api_path + '/http_logs', to: 'http_logs#create', via: :post + +end diff --git a/db/migrate/20160417000002_add_http_log.rb b/db/migrate/20160417000002_add_http_log.rb new file mode 100644 index 000000000..8480981ef --- /dev/null +++ b/db/migrate/20160417000002_add_http_log.rb @@ -0,0 +1,32 @@ +class AddHttpLog < ActiveRecord::Migration + def up + + create_table :http_logs do |t| + t.column :direction, :string, limit: 20, null: false + t.column :facility, :string, limit: 100, null: false + t.column :method, :string, limit: 100, null: false + t.column :url, :string, limit: 255, null: false + t.column :status, :string, limit: 20, null: true + t.column :ip, :string, limit: 50, null: true + t.column :request, :string, limit: 10_000, null: false + t.column :response, :string, limit: 10_000, null: false + t.column :updated_by_id, :integer, null: true + t.column :created_by_id, :integer, null: true + t.timestamps null: false + end + add_index :http_logs, [:facility] + add_index :http_logs, [:created_by_id] + add_index :http_logs, [:created_at] + + Scheduler.create_if_not_exists( + name: 'Cleanup HttpLog', + method: 'HttpLog.cleanup', + period: 24 * 60 * 60, + prio: 2, + active: true, + updated_by_id: 1, + created_by_id: 1, + ) + + end +end diff --git a/lib/user_agent.rb b/lib/user_agent.rb index edf7ba483..8fb48b3bd 100644 --- a/lib/user_agent.rb +++ b/lib/user_agent.rb @@ -62,9 +62,10 @@ returns total_timeout = options[:total_timeout] || 60 Timeout.timeout(total_timeout) do response = http.request(request) - return process(response, uri, count, params, options) + return process(request, response, uri, count, params, options) end rescue => e + log(url, request, nil, options) return Result.new( error: e.inspect, success: false, @@ -114,9 +115,10 @@ returns total_timeout = options[:total_timeout] || 60 Timeout.timeout(total_timeout) do response = http.request(request) - return process(response, uri, count, params, options) + return process(request, response, uri, count, params, options) end rescue => e + log(url, request, nil, options) return Result.new( error: e.inspect, success: false, @@ -165,9 +167,10 @@ returns total_timeout = options[:total_timeout] || 60 Timeout.timeout(total_timeout) do response = http.request(request) - return process(response, uri, count, params, options) + return process(request, response, uri, count, params, options) end rescue => e + log(url, request, nil, options) return Result.new( error: e.inspect, success: false, @@ -209,9 +212,10 @@ returns total_timeout = options[:total_timeout] || 60 Timeout.timeout(total_timeout) do response = http.request(request) - return process(response, uri, count, {}, options) + return process(request, response, uri, count, {}, options) end rescue => e + log(url, request, nil, options) return Result.new( error: e.inspect, success: false, @@ -293,7 +297,64 @@ returns request end - def self.process(response, uri, count, params, options) + def self.log(url, request, response, options) + return if !options[:log] + + # request + request_data = { + content: '', + content_type: request['Content-Type'], + content_encoding: request['Content-Encoding'], + source: request['User-Agent'] || request['Server'], + } + request.each_header {|key, value| + request_data[:content] += "#{key}: #{value}\n" + } + body = request.body + if body + request_data[:content] += "\n" + body + end + request_data[:content] = request_data[:content].slice(0, 8000) + + # response + response_data = { + code: 0, + content: '', + content_type: nil, + content_encoding: nil, + source: nil, + } + if response + response_data[:code] = response.code + response_data[:content_type] = response['Content-Type'] + response_data[:content_encoding] = response['Content-Encoding'] + response_data[:source] = response['User-Agent'] || response['Server'] + response.each_header {|key, value| + response_data[:content] += "#{key}: #{value}\n" + } + body = response.body + if body + response_data[:content] += "\n" + body + end + response_data[:content] = response_data[:content].slice(0, 8000) + end + + record = { + direction: 'out', + facility: options[:log][:facility], + url: url, + status: response_data[:code], + ip: nil, + request: request_data, + response: response_data, + method: request.method, + } + HttpLog.create(record) + end + + def self.process(request, response, uri, count, params, options) # rubocop:disable Metrics/ParameterLists + log(uri.to_s, request, response, options) + if !response return Result.new( error: "Can't connect to #{uri}, got no response!", diff --git a/test/integration/slack_test.rb b/test/integration/slack_test.rb index 33320dfdb..ac000bd96 100644 --- a/test/integration/slack_test.rb +++ b/test/integration/slack_test.rb @@ -124,6 +124,99 @@ class SlackTest < ActiveSupport::TestCase # check if message exists assert(slack_check(channel, hash)) + items = [ + { + group_ids: slack_group.id.to_s, + types: 'create', + webhook: webhook, + channel: channel, + username: 'zammad bot', + expand: false, + } + ] + Setting.set('slack_config', { items: items }) + + # case 3 + customer = User.find(2) + hash = hash_gen + text = "#{rand_word}... #{hash}" + + default_group = Group.first + ticket3 = Ticket.create( + title: text, + customer_id: customer.id, + group_id: default_group.id, + state: Ticket::State.find_by(name: 'new'), + priority: Ticket::Priority.find_by(name: '2 normal'), + updated_by_id: 1, + created_by_id: 1, + ) + article3 = Ticket::Article.create( + ticket_id: ticket3.id, + body: text, + type: Ticket::Article::Type.find_by(name: 'note'), + sender: Ticket::Article::Sender.find_by(name: 'Customer'), + internal: false, + updated_by_id: 1, + created_by_id: 1, + ) + + Observer::Transaction.commit + Delayed::Worker.new.work_off + + # check if message exists + assert_not(slack_check(channel, hash)) + + ticket3.state = Ticket::State.find_by(name: 'open') + ticket3.save + + Observer::Transaction.commit + Delayed::Worker.new.work_off + + # check if message exists + assert_not(slack_check(channel, hash)) + + # case 4 + hash = hash_gen + text = "#{rand_word}... #{hash}" + + ticket4 = Ticket.create( + title: text, + customer_id: customer.id, + group_id: slack_group.id, + state: Ticket::State.find_by(name: 'new'), + priority: Ticket::Priority.find_by(name: '2 normal'), + updated_by_id: 1, + created_by_id: 1, + ) + article4 = Ticket::Article.create( + ticket_id: ticket4.id, + body: text, + type: Ticket::Article::Type.find_by(name: 'note'), + sender: Ticket::Article::Sender.find_by(name: 'Customer'), + internal: false, + updated_by_id: 1, + created_by_id: 1, + ) + + Observer::Transaction.commit + Delayed::Worker.new.work_off + + # check if message exists + assert(slack_check(channel, hash)) + + hash = hash_gen + text = "#{rand_word}... #{hash}" + + ticket4.title = text + ticket4.save + + Observer::Transaction.commit + Delayed::Worker.new.work_off + + # check if message exists + assert_not(slack_check(channel, hash)) + end def hash_gen