Added http log support.

This commit is contained in:
Martin Edenhofer 2016-04-18 02:02:31 +02:00
parent 5b8be81ef5
commit e42e373de4
18 changed files with 432 additions and 47 deletions

View file

@ -16,6 +16,8 @@ class App.ControllerIntegrationBase extends App.Controller
return if !@authenticate(false, 'Admin') return if !@authenticate(false, 'Admin')
@title @featureName, true @title @featureName, true
@initalRender = true
@subscribeId = App.Setting.subscribe(@render, initFetch: true, clear: false) @subscribeId = App.Setting.subscribe(@render, initFetch: true, clear: false)
switch: => switch: =>
@ -23,17 +25,14 @@ class App.ControllerIntegrationBase extends App.Controller
App.Setting.set(@featureIntegration, value) App.Setting.set(@featureIntegration, value)
render: => render: =>
localEl = $( App.view('integration/base')( if @initalRender
header: @featureName @html App.view('integration/base')(
description: @description header: @featureName
feature: @featureIntegration description: @description
featureEnabled: App.Setting.get(@featureIntegration) feature: @featureIntegration
)) featureEnabled: App.Setting.get(@featureIntegration)
@form localEl )
@html localEl @initalRender = false
form: (localEl) ->
console.log('implement own form method')
submit: (e) => submit: (e) =>
e.preventDefault() e.preventDefault()

View file

@ -7,10 +7,11 @@ class Index extends App.ControllerIntegrationBase
['If the host and service is recovered again, the ticket will be closed automatically.'] ['If the host and service is recovered again, the ticket will be closed automatically.']
] ]
form: (localeEl) -> render: =>
super
new App.SettingsForm( new App.SettingsForm(
area: 'Integration::Icinga' area: 'Integration::Icinga'
el: localeEl.find('.js-form') el: @$('.js-form')
) )
class State class State

View file

@ -7,10 +7,11 @@ class Index extends App.ControllerIntegrationBase
['If the host and service is recovered again, the ticket will be closed automatically.'] ['If the host and service is recovered again, the ticket will be closed automatically.']
] ]
form: (localeEl) -> render: =>
super
new App.SettingsForm( new App.SettingsForm(
area: 'Integration::Nagios' area: 'Integration::Nagios'
el: localeEl.find('.js-form') el: @$('.js-form')
) )
class State class State

View file

@ -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'] ['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) params = App.Setting.get(@featureConfig)
if params && params.items 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: '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: '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: '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: '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: 'channel', display: 'Channel', tag: 'input', type: 'text', limit: 100, 'null': true, placeholder: '#channel' },
] ]
console.log('p', params)
settings = [] settings = []
for item in configureAttributes for item in configureAttributes
setting = setting =
@ -54,7 +55,12 @@ class Index extends App.ControllerIntegrationBase
params: localParams params: localParams
) )
localEl.find('.js-form').html(formEl) @$('.js-form').html(formEl)
new App.HttpLog(
el: @$('.js-log')
facility: 'slack_webhook'
)
class State class State
@current: -> @current: ->

View file

@ -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
)

View file

@ -14,4 +14,5 @@
<% end %> <% end %>
<% end %> <% end %>
<div class="js-form"></div> <div class="js-form"></div>
<div class="js-log"></div>
</div> </div>

View file

@ -0,0 +1,21 @@
<hr>
<%- @T('Recent logs') %>
<div class="settings-entry">
<table class="settings-list" style="width: 100%;">
<thead>
<tr>
<th width="10%"><%- @T('Direction') %>
<th><%- @T('Request') %>
<th width="15%"><%- @T('Created at') %>
</thead>
<tbody>
<% for record in @records: %>
<tr data-id="<%= record.id %>" class="js-record">
<td><%- @T(record.direction) %>
<td><a href="#"><%= record.status %> <%= record.method %> <%= record.url %></a>
<td><%- @datetime(record.created_at) %>
<% end %>
</tbody>
</table>
</div>

View file

@ -0,0 +1,27 @@
<div class="settings-entry">
<table class="settings-list" style="width: 100%;">
<tbody>
<tr>
<td width="20%"><%- @T('Direction') %>
<td><%- @T(@record.direction) %>
<tr>
<td><%- @T('URL') %>
<td><%= @record.url %>
<tr>
<td><%- @T('Method') %>
<td><%= @record.method %>
<tr>
<td><%- @T('Status') %>
<td><%= @record.status %>
<tr>
<td><%- @T('Request') %>
<td><%- App.Utils.text2html(@record.request.content) %>
<tr>
<td><%- @T('Response') %>
<td><%- App.Utils.text2html(@record.response.content) %>
<tr>
<td><%- @T('Created at') %>
<td><%- @datetime(@record.created_at) %>
</tbody>
</table>
</div>

View file

@ -6929,6 +6929,8 @@ output {
background: white; background: white;
table-layout: auto; table-layout: auto;
margin-bottom: 20px; margin-bottom: 20px;
word-break: break-all;
word-wrap: break-word;
&.is-invalid { &.is-invalid {
border-radius: 3px; border-radius: 3px;
@ -6975,6 +6977,7 @@ output {
letter-spacing: 0.05em; letter-spacing: 0.05em;
background: hsl(197,20%,93%); background: hsl(197,20%,93%);
border-bottom: none; border-bottom: none;
word-break: normal;
} }
td.empty-cell { td.empty-cell {

View file

@ -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

24
app/models/http_log.rb Normal file
View file

@ -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

View file

@ -2,6 +2,14 @@
class Transaction::Slack class Transaction::Slack
=begin =begin
backend = Transaction::Slack.new(
object: 'Ticket',
type: 'create',
ticket_id: 1,
)
backend.perform
{ {
object: 'Ticket', object: 'Ticket',
type: 'update', type: 'update',
@ -83,24 +91,30 @@ class Transaction::Slack
# check action # check action
if item['types'] if item['types']
hit = false if item['types'].class == Array
item['types'].each {|type| hit = false
next if type.to_s != @item[:type].to_s item['types'].each {|type|
hit = true next if type.to_s != @item[:type].to_s
break hit = true
} break
next if !hit }
next if !hit
end
next if item['types'].to_s != @item[:type].to_s
end end
# check group # check group
if item['group_ids'] if item['group_ids']
hit = false if item['group_ids'].class == Array
item['group_ids'].each {|group_id| hit = false
next if group_id.to_s != ticket.group_id.to_s item['group_ids'].each {|group_id|
hit = true next if group_id.to_s != ticket.group_id.to_s
break hit = true
} break
next if !hit }
next if !hit
end
next if item['group_ids'].to_s != ticket.group_id.to_s
end end
Rails.logger.debug "sent webhook (#{@item[:type]}/#{ticket.id}/#{item['webhook']})" Rails.logger.debug "sent webhook (#{@item[:type]}/#{ticket.id}/#{item['webhook']})"
@ -108,8 +122,9 @@ class Transaction::Slack
item['webhook'], item['webhook'],
channel: item['channel'], channel: item['channel'],
username: item['username'], username: item['username'],
icon_url: logo_url, #icon_url: logo_url,
mrkdwn: true, mrkdwn: true,
http_client: Transaction::Slack::Client,
) )
if item['expand'] if item['expand']
body = "#{result[:subject]}\n#{result[:body]}" body = "#{result[:subject]}\n#{result[:body]}"
@ -123,11 +138,9 @@ class Transaction::Slack
result = notifier.ping result[:subject], result = notifier.ping result[:subject],
attachments: [attachment] attachments: [attachment]
end end
if !result if !result.success?
Rails.logger.error "Unable to post webhook: #{item['webhook']}"
end
if result.code.to_s != '200' && result.code.to_s != '201'
Rails.logger.error "Unable to post webhook: #{item['webhook']}: #{result.inspect}" Rails.logger.error "Unable to post webhook: #{item['webhook']}: #{result.inspect}"
next
end end
Rails.logger.debug "sent webhook (#{@item[:type]}/#{ticket.id}/#{item['webhook']})" Rails.logger.debug "sent webhook (#{@item[:type]}/#{ticket.id}/#{item['webhook']})"
} }
@ -224,4 +237,21 @@ class Transaction::Slack
changes changes
end 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 end

View file

@ -1,6 +1,6 @@
# <%= d 'ticket.title' %> # <%= d 'ticket.title' %>
_<<%= c 'http_type' %>://<%= c 'fqdn' %>/#ticket/zoom/<%= d 'ticket.id' %>|Ticket#<%= d 'ticket.number' %>>: Escalated at <%= d 'ticket.escalation_time' %>_ _<<%= 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 "<b><%= d 'ticket.customer.longname' %></b>" 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] %> <% if @objects[:article] %>
<%= a_text 'article' %> <%= a_text 'article' %>

View file

@ -1,6 +1,6 @@
# <%= d 'ticket.title' %> # <%= d 'ticket.title' %>
_<<%= c 'http_type' %>://<%= c 'fqdn' %>/#ticket/zoom/<%= d 'ticket.id' %>|Ticket#<%= d 'ticket.number' %>>: Will escalate at <%= d 'ticket.escalation_time' %>_ _<<%= 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 "<b><%= d 'ticket.customer.longname' %></b>" 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] %> <% if @objects[:article] %>
<%= a_text 'article' %> <%= a_text 'article' %>

View file

@ -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

View file

@ -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

View file

@ -62,9 +62,10 @@ returns
total_timeout = options[:total_timeout] || 60 total_timeout = options[:total_timeout] || 60
Timeout.timeout(total_timeout) do Timeout.timeout(total_timeout) do
response = http.request(request) response = http.request(request)
return process(response, uri, count, params, options) return process(request, response, uri, count, params, options)
end end
rescue => e rescue => e
log(url, request, nil, options)
return Result.new( return Result.new(
error: e.inspect, error: e.inspect,
success: false, success: false,
@ -114,9 +115,10 @@ returns
total_timeout = options[:total_timeout] || 60 total_timeout = options[:total_timeout] || 60
Timeout.timeout(total_timeout) do Timeout.timeout(total_timeout) do
response = http.request(request) response = http.request(request)
return process(response, uri, count, params, options) return process(request, response, uri, count, params, options)
end end
rescue => e rescue => e
log(url, request, nil, options)
return Result.new( return Result.new(
error: e.inspect, error: e.inspect,
success: false, success: false,
@ -165,9 +167,10 @@ returns
total_timeout = options[:total_timeout] || 60 total_timeout = options[:total_timeout] || 60
Timeout.timeout(total_timeout) do Timeout.timeout(total_timeout) do
response = http.request(request) response = http.request(request)
return process(response, uri, count, params, options) return process(request, response, uri, count, params, options)
end end
rescue => e rescue => e
log(url, request, nil, options)
return Result.new( return Result.new(
error: e.inspect, error: e.inspect,
success: false, success: false,
@ -209,9 +212,10 @@ returns
total_timeout = options[:total_timeout] || 60 total_timeout = options[:total_timeout] || 60
Timeout.timeout(total_timeout) do Timeout.timeout(total_timeout) do
response = http.request(request) response = http.request(request)
return process(response, uri, count, {}, options) return process(request, response, uri, count, {}, options)
end end
rescue => e rescue => e
log(url, request, nil, options)
return Result.new( return Result.new(
error: e.inspect, error: e.inspect,
success: false, success: false,
@ -293,7 +297,64 @@ returns
request request
end 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 if !response
return Result.new( return Result.new(
error: "Can't connect to #{uri}, got no response!", error: "Can't connect to #{uri}, got no response!",

View file

@ -124,6 +124,99 @@ class SlackTest < ActiveSupport::TestCase
# check if message exists # check if message exists
assert(slack_check(channel, hash)) 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 end
def hash_gen def hash_gen