Skip to content
This repository was archived by the owner on May 16, 2024. It is now read-only.

Commit cecdf6c

Browse files
initial
0 parents  commit cecdf6c

11 files changed

+452
-0
lines changed

.gitignore

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# .gitignore
2+
3+
/.idea/
4+
/.env
5+
target/
6+

README.md

+113
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
Play2向け Sentryログ出力サポート
2+
=====
3+
4+
Play2においてSentryへのログ出力にHTTPリクエストの内容を記録させるためのモジュールです。
5+
Play2では、Java Servletとは違う独自の構造でHTTPリクエスト情報を保持しているため、それをSentry向けに変換する必要があります、
6+
7+
また、1リクエスト内でもマルチスレッドで処理することが多いため、スレッド内でのログにリクエスト情報を付与するためにはスレッド間でリクエスト情報を共有する必要があります。
8+
9+
これらに対処するため、本モジュールでは主に以下のことを行っています。
10+
11+
* PlayのHTTPリクエスト情報をSentryに送信するためのフォーマット変換処理を実装
12+
* 本モジュールを有効化したSentryClientを生成するための独自SentryClientFactoryを定義
13+
* Playのリクエスト情報をスレッドローカル変数に保存するためのフィルタを定義
14+
* Akkaでのスレッド起動時に、HTTP情報を持つスレッドローカル変数とlogbackのMDCを引き継ぐようにする処理を追加
15+
16+
使い方
17+
----
18+
19+
本モジュールの導入方法は以下のとおりです。
20+
21+
1. Playアプリの依存に追加する
22+
23+
```
24+
libraryDependencies ++= Seq(
25+
"com.m3.play2" % "play2-sentry" % "X.Y.Z"
26+
)
27+
```
28+
29+
2. sentry設定でfactoryを指定する
30+
31+
**sentry.properties**
32+
```
33+
factory=com.m3.play2.sentry.PlaySentryFactory
34+
```
35+
36+
3. Akkaのデフォルトdispatcherを本モジュールのクラスに指定する
37+
38+
**application.conf**
39+
40+
```
41+
play {
42+
akka {
43+
actor {
44+
default-dispatcher = {
45+
// Dispatcherを指定する
46+
type = "com.m3.play2.sentry.SentryDispatcherConfigurator"
47+
}
48+
}
49+
}
50+
}
51+
```
52+
53+
4. HTTPフィルタにSentryLoggingFilterを追加
54+
55+
```
56+
import com.m3.play2.sentry.SentryLoggingFilter
57+
58+
class ApplicationComponents(context: Context) extends BuiltInComponentsFromContext(context) {
59+
// filter を追加
60+
override lazy val httpFilters = Seq(
61+
new SentryLoggingFilter()
62+
)
63+
}
64+
```
65+
66+
5. Sentryの標準的な導入設定を行う
67+
68+
参考: https://docs.sentry.io/clients/java/
69+
70+
* ログ出力設定でSentryAppenderを指定する
71+
* アプリケーション固有のパッケージ名を明記
72+
* etc...
73+
74+
75+
本モジュールの開発方法
76+
----
77+
78+
### ビルド
79+
80+
```
81+
$ sbt package
82+
```
83+
84+
### ローカル環境での動作確認
85+
86+
1. モジュールをivyのローカルキャッシュにインストールする
87+
88+
```
89+
$ sbt publishLocal
90+
```
91+
92+
2. 動作確認用Playアプリで、resolvers にローカルキャッシュディレクトリを追加する
93+
94+
**build.sbt**
95+
96+
```
97+
resolvers += "MyLocalIvy" at "file://"+Path.userHome.absolutePath+"/.ivy2/local"
98+
```
99+
100+
3. アプリに組み込む
101+
102+
4. アプリを実行する
103+
104+
105+
### artifactory にデプロイ
106+
107+
以下のコマンドでartifactoryにjarを登録します
108+
109+
```
110+
$ export REPOSITORY_URL={{デプロイ先のartifactoryのURL}}
111+
$ sbt clean package publish
112+
```
113+

build.sbt

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
2+
scalaVersion := "2.11.8"
3+
4+
javacOptions ++= Seq("-source", "1.7", "-target", "1.7")
5+
6+
crossPaths := false
7+
8+
organization := "com.m3.play2"
9+
10+
name := "play2-sentry"
11+
12+
version := "2.0.1-SNAPSHOT"
13+
14+
libraryDependencies ++= Seq(
15+
"io.sentry" % "sentry-logback" % "1.7.5",
16+
"com.typesafe.play" % "play_2.11" % "2.4.11" % "provided"
17+
)
18+
19+
publishTo := version { v: String =>
20+
sys.env.get("REPOSITORY_URL").map { base =>
21+
if (v.trim.endsWith("SNAPSHOT"))
22+
"snapshots" at base + "libs-snapshots"
23+
else
24+
"releases" at base + "libs-releases"
25+
}
26+
}.value
27+
28+
resolvers += Resolver.typesafeRepo("releases")
29+

project/build.properties

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
sbt.version=1.2.1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package com.m3.play2.sentry
2+
3+
import io.sentry.event.EventBuilder
4+
import io.sentry.event.helper.EventBuilderHelper
5+
6+
class PlayHttpEventBuilderHelper() extends EventBuilderHelper {
7+
8+
override def helpBuildingEvent(eventBuilder: EventBuilder): Unit = {
9+
val hiOpt = SentryDispatcher.httpInterfaceValue.value
10+
hiOpt.foreach( eventBuilder.withSentryInterface(_, false) )
11+
}
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package com.m3.play2.sentry
2+
3+
import java.net.InetAddress
4+
5+
import io.sentry.event.interfaces.SentryInterface
6+
import play.api.mvc.RequestHeader
7+
8+
import scala.collection.Seq
9+
import scala.util.Try
10+
11+
/**
12+
* The HTTP interface of an Play2 HTTP request
13+
*/
14+
object PlayHttpInterface {
15+
private val HTTP_INTERFACE = "sentry.interfaces.Http"
16+
17+
def apply(request: RequestHeader): PlayHttpInterface = {
18+
val localHostAddress: Try[InetAddress] = Try(InetAddress.getLocalHost)
19+
20+
new PlayHttpInterface(
21+
requestUrl = getRequestFullURL(request),
22+
method = request.method,
23+
parameters = request.queryString,
24+
queryString = request.rawQueryString,
25+
cookies = request.cookies.map { cookie => (cookie.name, cookie.value) }.toSeq,
26+
headers = request.headers.toMap,
27+
remoteAddr = request.remoteAddress,
28+
serverName = request.host,
29+
localAddr = localHostAddress.map(_.getHostAddress).getOrElse(""),
30+
localName = localHostAddress.map(_.getHostName).getOrElse(""),
31+
secure = request.secure
32+
)
33+
}
34+
35+
private def getRequestFullURL(request: RequestHeader) =
36+
(if (request.secure) "https" else "http") + "://" + request.host + request.uri
37+
}
38+
39+
/**
40+
* Creates a an HTTP interface
41+
*/
42+
case class PlayHttpInterface(
43+
// arguments of HTTP interface
44+
requestUrl: String,
45+
method: String,
46+
parameters: Map[String, Seq[String]],
47+
queryString: String,
48+
cookies: Seq[(String, String)],
49+
headers: Map[String, Seq[String]],
50+
51+
// environment data
52+
remoteAddr: String,
53+
serverName: String,
54+
localAddr: String,
55+
localName: String,
56+
secure: Boolean) extends SentryInterface {
57+
58+
override def getInterfaceName: String = PlayHttpInterface.HTTP_INTERFACE
59+
60+
override def toString: String =
61+
"PlayHttpInterface{" +
62+
"requestUrl='" + requestUrl + "', " +
63+
"method='" + method + "', " +
64+
"parameters=" + parameters + '}'
65+
66+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package com.m3.play2.sentry
2+
3+
import java.io.IOException
4+
5+
import com.fasterxml.jackson.core.JsonGenerator
6+
import io.sentry.marshaller.json.InterfaceBinding
7+
8+
object PlayHttpInterfaceBinding {
9+
private val URL = "url"
10+
private val METHOD = "method"
11+
private val DATA = "data"
12+
private val QUERY_STRING = "query_string"
13+
private val COOKIES = "cookies"
14+
private val HEADERS = "headers"
15+
private val ENVIRONMENT = "env"
16+
private val ENV_REMOTE_ADDR = "REMOTE_ADDR"
17+
private val ENV_SERVER_NAME = "SERVER_NAME"
18+
private val ENV_LOCAL_ADDR = "LOCAL_ADDR"
19+
private val ENV_LOCAL_NAME = "LOCAL_NAME"
20+
private val ENV_REQUEST_SECURE = "REQUEST_SECURE"
21+
}
22+
23+
class PlayHttpInterfaceBinding extends InterfaceBinding[PlayHttpInterface] {
24+
import PlayHttpInterfaceBinding._
25+
26+
@throws[IOException]
27+
override def writeInterface(generator: JsonGenerator, playHttpInterface: PlayHttpInterface): Unit = {
28+
generator.writeStartObject()
29+
generator.writeStringField(URL, playHttpInterface.requestUrl)
30+
generator.writeStringField(METHOD, playHttpInterface.method)
31+
32+
generator.writeFieldName(DATA)
33+
writeParameters(generator, playHttpInterface.parameters)
34+
35+
generator.writeStringField(QUERY_STRING, playHttpInterface.queryString)
36+
37+
generator.writeFieldName(COOKIES)
38+
writeCookies(generator, playHttpInterface.cookies)
39+
40+
generator.writeFieldName(HEADERS)
41+
writeHeaders(generator, playHttpInterface.headers)
42+
43+
generator.writeFieldName(ENVIRONMENT)
44+
writeEnvironment(generator, playHttpInterface)
45+
46+
generator.writeEndObject()
47+
}
48+
49+
@throws[IOException]
50+
private def writeEnvironment(generator: JsonGenerator, playHttpInterface: PlayHttpInterface): Unit = {
51+
generator.writeStartObject()
52+
generator.writeStringField(ENV_REMOTE_ADDR, playHttpInterface.remoteAddr)
53+
generator.writeStringField(ENV_SERVER_NAME, playHttpInterface.serverName)
54+
generator.writeStringField(ENV_LOCAL_ADDR, playHttpInterface.localAddr)
55+
generator.writeStringField(ENV_LOCAL_NAME, playHttpInterface.localName)
56+
generator.writeBooleanField(ENV_REQUEST_SECURE, playHttpInterface.secure)
57+
generator.writeEndObject()
58+
}
59+
60+
@throws[IOException]
61+
private def writeHeaders(generator: JsonGenerator, headers: Map[String, Seq[String]]): Unit = {
62+
generator.writeStartObject()
63+
headers.foreach {case (key, values) =>
64+
generator.writeStringField(key, values.mkString(","))
65+
}
66+
generator.writeEndObject()
67+
}
68+
69+
@throws[IOException]
70+
private def writeCookies(generator: JsonGenerator, cookies: Seq[(String, String)]): Unit = {
71+
if (cookies.isEmpty) {
72+
generator.writeNull()
73+
74+
} else {
75+
generator.writeStartObject()
76+
cookies.foreach { case (key, value) =>
77+
generator.writeStringField(key, value)
78+
}
79+
generator.writeEndObject()
80+
}
81+
}
82+
83+
@throws[IOException]
84+
private def writeParameters(generator: JsonGenerator, parameterMap: Map[String, Seq[String]]): Unit = {
85+
if (parameterMap.isEmpty) {
86+
generator.writeNull()
87+
88+
} else {
89+
generator.writeStartObject()
90+
91+
parameterMap.foreach { case (key, values) =>
92+
generator.writeArrayFieldStart(key)
93+
values.foreach { value => generator.writeString(value) }
94+
generator.writeEndArray()
95+
}
96+
97+
generator.writeEndObject()
98+
}
99+
}
100+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package com.m3.play2.sentry
2+
3+
import io.sentry._
4+
import io.sentry.dsn.Dsn
5+
import io.sentry.marshaller.Marshaller
6+
import io.sentry.marshaller.json.JsonMarshaller
7+
8+
/**
9+
* Custom sentry client factory for Play
10+
*
11+
* You need specify this class in the sentry settings (default sentry.properties file on classpath)
12+
*/
13+
class PlaySentryFactory extends DefaultSentryClientFactory {
14+
15+
override def createSentryClient(dsn: Dsn): SentryClient = {
16+
val sentry = super.createSentryClient(dsn)
17+
sentry.addBuilderHelper(new PlayHttpEventBuilderHelper)
18+
sentry
19+
}
20+
21+
override protected def createMarshaller(dsn: Dsn): Marshaller = {
22+
val marshaller = super.createMarshaller(dsn).asInstanceOf[JsonMarshaller]
23+
marshaller.addInterfaceBinding(classOf[PlayHttpInterface], new PlayHttpInterfaceBinding)
24+
marshaller
25+
}
26+
}

0 commit comments

Comments
 (0)