├── .gitignore ├── LICENSE ├── README.md ├── docs ├── cn │ ├── advance.md │ ├── arch.md │ ├── consumer.md │ ├── design.md │ ├── install.md │ ├── message.md │ ├── producer.md │ ├── query.md │ ├── quickstart.md │ └── table.md └── images │ ├── arch.png │ ├── design.png │ ├── param1.png │ ├── param2.png │ ├── param3.png │ └── qq.png ├── pom.xml └── src └── main ├── java └── com │ └── qunar │ └── cm │ └── ic │ ├── Application.java │ ├── common │ └── exception │ │ ├── ExceptionEnum.java │ │ └── ICException.java │ ├── controller │ ├── AbstractController.java │ ├── EventController.java │ ├── ListenerController.java │ └── TypeController.java │ ├── dao │ ├── EventRepository.java │ ├── ListenerRepository.java │ ├── ProducerRepository.java │ ├── PropertyRepository.java │ ├── TypeRepository.java │ ├── converter │ │ ├── EventReadConverter.java │ │ ├── EventWriteConverter.java │ │ └── package-info.java │ └── page │ │ ├── FirstEventPage.java │ │ └── package-info.java │ ├── dto │ ├── EventResult.java │ ├── EventSaveResult.java │ ├── IpMatcher.java │ ├── ListenerFetchResult.java │ └── MessageResponse.java │ ├── model │ ├── Event.java │ ├── IdentityCounter.java │ ├── Listener.java │ ├── Producer.java │ ├── Property.java │ ├── Type.java │ └── jackson │ │ ├── EventDeserializer.java │ │ └── EventSerializer.java │ ├── package-info.java │ └── service │ ├── EventConsumerService.java │ ├── EventService.java │ ├── ListenerService.java │ ├── ProducerService.java │ ├── PropertyService.java │ ├── TypeService.java │ └── impl │ ├── EventConsumerServiceImpl.java │ ├── EventServiceImpl.java │ ├── ListenerServiceImpl.java │ ├── ProducerServiceImpl.java │ ├── PropertyServiceImpl.java │ └── TypeServiceImpl.java ├── resources ├── application.properties ├── base-type.json ├── logback.xml ├── mongodb.properties └── spring-mongodb.xml └── webapp ├── WEB-INF └── web.xml └── index.html /.gitignore: -------------------------------------------------------------------------------- 1 | ### Java template 2 | *.class 3 | 4 | # Mobile Tools for Java (J2ME) 5 | .mtj.tmp/ 6 | 7 | # Package Files # 8 | *.jar 9 | *.war 10 | *.ear 11 | 12 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 13 | hs_err_pid* 14 | ### Maven template 15 | /*.iml 16 | /.idea 17 | target/ 18 | pom.xml.tag 19 | pom.xml.releaseBackup 20 | pom.xml.versionsBackup 21 | pom.xml.next 22 | release.properties 23 | dependency-reduced-pom.xml 24 | buildNumber.properties 25 | .mvn/timing.properties 26 | /.sonar 27 | 28 | src/main/webapp/components 29 | src/test -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Qunar, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # IC 2 | 3 | IC是去哪儿公司内部CI、CD以及devops体系建设过程中使用的消息系统和数据中心。由于其基于HTTP协议的特性,具有跨平台、跨语言的优点。而devops体系搭建中,会引入各种开源工具,这些工具的语言差异也很大。基于IC,我们不仅快速实现了流程自动化,而且系统解耦,自动化进程大大提高。 4 | 5 | ## IC主要提供以下功能: 6 | * 消息接收 7 | * 消息监听(轮询方式实时返回,长轮询方式可能延时2s,可自行修改) 8 | * 历史消息按时间段查询 9 | * 历史消息按id查询 10 | 11 | ## 依赖 12 | * MongoDB 3.2.8 13 | * JDK 8 14 | * Tomcat 8 15 | 16 | 17 | 18 | 19 | 20 | ## 文档 21 | * [快速开始](docs/cn/quickstart.md) 22 | * [安装](docs/cn/install.md) 23 | * [设计背景](docs/cn/design.md) 24 | * [架构概览](docs/cn/arch.md) 25 | * [表结构介绍](docs/cn/table.md) 26 | * [配置消息](docs/cn/message.md) 27 | * [发送消息](docs/cn/producer.md) 28 | * [消费消息](docs/cn/consumer.md) 29 | * [查询消息](docs/cn/query.md) 30 | * [进阶说明](docs/cn/advance.md) 31 | 32 | 33 | ## 技术支持 34 | 35 | ### QQ群 36 |  37 | 38 | -------------------------------------------------------------------------------- /docs/cn/advance.md: -------------------------------------------------------------------------------- 1 | [上一页](query.md) 2 | [回目录](../../README.md) 3 | 4 | 5 | # 进阶说明 6 | 7 | ### consumer使用长轮询方式监听消息的实时性改进 8 | 在实际实际使用时,我们在去哪儿内部是集成了QMQ的,每个消息在可以被消费时会发出一个QMQ消息,所以在consumer发起长轮询时,IC随时监听QMQ消息,当有新消息时会立马返回给consumer,提高实时性。但在开源版本中为了提高IC服务部署的简洁性,去掉了对QMQ的依赖,当consumer发起长轮询时返回新消息可能会有两秒的延时。 9 | 10 | ### 新增消息类型和新增生产者的说明 11 | 在开源版本中,我们没有给管理员提供配置界面,都是直接操作的数据库,在使用上便捷性可能不是很友好,因为在去哪儿内部,我们开发了另一个系统最为管理员操作页面,这个系统直接操作IC的mongodb数据库,没有和IC集成到一起。 12 | 下图是消息从产生到消费的时序图: 13 | 14 | 15 | 16 | [上一页](query.md) 17 | [回目录](../../README.md) -------------------------------------------------------------------------------- /docs/cn/arch.md: -------------------------------------------------------------------------------- 1 | [上一页](design.md) 2 | [回目录](../../README.md) 3 | [下一页](table.md) 4 | 5 | 6 | # 架构概览 7 | 下图是消息从产生到消费的时序图: 8 | 9 | * IC 本服务 10 | * MongoDB 存储消息的数据库 11 | * producer 消息生产者 12 | * consumer 消息消费者 13 | 14 |  15 | 16 | 根据图中的编号描述一下其交互过程 17 | 1. producer向IC推送消息 18 | 2. IC将消息内容进行处理,并添加一些固定字段,其中将_hidden设置为true,即此时该消息不会被消费者消费 19 | 3. 另一个线程将连续的待被消费的消息的_hidden字段更新成false,从而保证消息会按顺序被消费,且不会丢失消息 20 | 4. 更新数据库中last_event_id,即最后一个可被消费的消息id 21 | 5. consumer轮询/长轮询监听消息 22 | 6. IC查询数据库获取新消息 23 | 7. IC返回新消息给consumer 24 | 25 | 26 | [上一页](design.md) 27 | [回目录](../../README.md) 28 | [下一页](table.md) 29 | -------------------------------------------------------------------------------- /docs/cn/consumer.md: -------------------------------------------------------------------------------- 1 | [上一页](producer.md) 2 | [回目录](../../README.md) 3 | [下一页](query.md) 4 | 5 | 6 | # 消费消息(consumer) 7 | 8 | ### 发送GET请求监听消息 9 | 10 | GET /api/v2/event/listener/{token}?type=student-chooseClasses&longPoll=true 11 | 12 | 请求参数说明 13 | 14 |  15 | 16 | ps:新的消费者token可以自定义传入,只要和其他消费者的token不重复就可以 17 | 18 | 返回值 19 |  20 | 21 | ```json 22 | { 23 | "code": "cd007e2e-fda5-4508-b4ca-588d326385ab", 24 | "list": [ 25 | { 26 | "id": 7683203, 27 | "type": "student-chooseClasses", 28 | "event": "student-chooseClasses", 29 | "operator": "san.zhang", 30 | "source": "courseSelectionSystem", 31 | "time": "2017-12-06T18:15:33+08:00", 32 | "timestamp": 1512555333139, 33 | "updated": "2018-12-17T19:59:27+08:00", 34 | "updatedTimestamp": 1545047967933, 35 | "userName": "san.zhang", 36 | "classes": [ 37 | "math", 38 | "english", 39 | "chinese" 40 | ], 41 | "age": 14, 42 | "sex": "male", 43 | "hobby": "basketball" 44 | } 45 | ], 46 | "maxResults": 100, 47 | "hasMore": false 48 | } 49 | ``` 50 | 51 | ## 消费成功发ACK确认 52 | POST /api/v2/event/listener/{token}/{code} 53 | 54 | * token是监听时传入的token 55 | * code是监听时返回的code 56 | 57 | 返回值 58 | 59 | ```json 60 | { 61 | "message": "success" 62 | } 63 | ``` 64 | 65 | [上一页](producer.md) 66 | [回目录](../../README.md) 67 | [下一页](query.md) 68 | -------------------------------------------------------------------------------- /docs/cn/design.md: -------------------------------------------------------------------------------- 1 | [上一页](install.md) 2 | [回目录](../../README.md) 3 | [下一页](arch.md) 4 | 5 | # 设计背景 6 | 7 | ### 真实使用场景 8 | 在我们工程效率团队,有大量的CI、CD、DevOps工具,如何能够实现工具解耦,且在短期实现自动化?其实只需要一个消息总线。 9 | 10 | ### 消息总线 11 | 消息总线,简单理解就是一个消息中心,众多微服务实例可以连接到总线上,实例可以往消息中心发送或接收消息(通过监听)。比如:实例1发送一条消息到总线上,总线上的实例2可以接收到消息(实例2监听了实例1发送的消息类型),这样的话,消息总线就充当一个中间者的角色,使得实例1和实例2解偶了,很方便。 12 | 13 |  14 | 15 | 16 | ### 不直接使用现有的消息中间件原因 17 | 去哪儿内部已经存在了工程师广泛使用的消息中间件QMQ,java的工程能够很方便地通过引入jar包和注解的方式发送和接收QMQ消息,但是对于公司内部相对小众语言,比如:python、nodejs等开发人员来说,享受不到QMQ带来的便利,需要一个跨语言跨平台的消息系统。IC由此诞生,它不仅仅解决了跨语言的消息传递问题,还是一个大数据中心。而且在去哪儿内部和QMQ相结合,使python系统和java系统都能够便利地发送和接收消息(主要是python系统发送消息,java系统接收消息)。 18 | 19 | 20 | [上一页](install.md) 21 | [回目录](../../README.md) 22 | [下一页](arch.md) 23 | -------------------------------------------------------------------------------- /docs/cn/install.md: -------------------------------------------------------------------------------- 1 | [上一页](quickstart.md) 2 | [回目录](../../README.md) 3 | [下一页](design.md) 4 | 5 | # 安装 6 | 7 | ### 初始化数据库 8 | 创建mongo数据库 9 | ``` 10 | > use dc 11 | switched to db dc 12 | ``` 13 | 14 | 创建mongo数据库用户 15 | ``` 16 | > db.createUser({user:'username',pwd:'pwd',roles:['readWrite']}) 17 | Successfully added user: { "user" : "username", "roles" : [ "readWrite" ] } 18 | ``` 19 | 创建collection 20 | ``` 21 | > db.createCollection("eventinfos") 22 | { "ok" : 1 } 23 | > db.createCollection("identitycounters") 24 | { "ok" : 1 } 25 | > db.createCollection("listenerinfos") 26 | { "ok" : 1 } 27 | > db.createCollection("producerinfos") 28 | { "ok" : 1 } 29 | > db.createCollection("typeinfos") 30 | { "ok" : 1 } 31 | ``` 32 | ### 初始化数据 33 | ``` 34 | db.identitycounters.insert({ 35 | "model" : "EventInfo", 36 | "field" : "id", 37 | "count" : 0.0, 38 | "__v" : 0 39 | }) 40 | 41 | db.propertyinfos.insert({ 42 | "key" : "cache.schema", 43 | "version" : 0 44 | }) 45 | 46 | db.propertyinfos.insert({ 47 | "key" : "cache.producer", 48 | "version" : 0 49 | }) 50 | 51 | ``` 52 | 53 | ### 配置文件 54 | *mongodb.properties* 55 | ``` 56 | # 必填,mongo数据库连接地址 57 | mongo.hosts=
:
10 | * 这个类的作用主要是为了排序和限制返回的数量,而不是分页
11 | */
12 | public final class FirstEventPage extends AbstractPageRequest {
13 |
14 | public static FirstEventPage of(int size) {
15 | //只返回第一页
16 | return new FirstEventPage(0, size);
17 | }
18 |
19 | private FirstEventPage(int page, int size) {
20 | super(page, size);
21 | }
22 |
23 |
24 | @Override
25 | public Sort getSort() {
26 | return Sort.by(Sort.Order.asc("id"));
27 | }
28 |
29 | @Override
30 | public Pageable next() {
31 | throw new UnsupportedOperationException();
32 | }
33 |
34 | @Override
35 | public Pageable previous() {
36 | throw new UnsupportedOperationException();
37 | }
38 |
39 | @Override
40 | public Pageable first() {
41 | throw new UnsupportedOperationException();
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/main/java/com/qunar/cm/ic/dao/page/package-info.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by yu.qi on 2018/08/30.
3 | */
4 | @NonNullApi
5 | package com.qunar.cm.ic.dao.page;
6 |
7 | import org.springframework.lang.NonNullApi;
--------------------------------------------------------------------------------
/src/main/java/com/qunar/cm/ic/dto/EventResult.java:
--------------------------------------------------------------------------------
1 | package com.qunar.cm.ic.dto;
2 |
3 | /**
4 | * Created by dandan.sha on 2018/09/13.
5 | */
6 | public class EventResult {
7 | private Long id;
8 |
9 | public EventResult(Long id) {
10 | this.id = id;
11 | }
12 |
13 | public Long getId() {
14 | return id;
15 | }
16 |
17 | public void setId(Long id) {
18 | this.id = id;
19 | }
20 |
21 | }
22 |
--------------------------------------------------------------------------------
/src/main/java/com/qunar/cm/ic/dto/EventSaveResult.java:
--------------------------------------------------------------------------------
1 | package com.qunar.cm.ic.dto;
2 |
3 | /**
4 | * Created by dandan.sha on 2018/09/13.
5 | */
6 | public class EventSaveResult extends MessageResponse {
7 | private EventResult event;
8 |
9 | public EventSaveResult(EventResult event) {
10 | super("success");
11 | this.event = event;
12 | }
13 |
14 | public EventResult getEvent() {
15 | return event;
16 | }
17 |
18 | public void setEvent(EventResult event) {
19 | this.event = event;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/main/java/com/qunar/cm/ic/dto/IpMatcher.java:
--------------------------------------------------------------------------------
1 | package com.qunar.cm.ic.dto;
2 |
3 | import com.google.common.collect.Lists;
4 |
5 | import java.util.List;
6 | import java.util.regex.Pattern;
7 | import java.util.stream.Collectors;
8 |
9 | /**
10 | * Created by yu.qi on 2018/09/11.
11 | */
12 |
13 | public class IpMatcher {
14 | private List
16 | * 事件有3个字段在数据库中的名称和类的字段名不同,分别是:
17 | * event->type
18 | * _ip->ip
19 | * _hidden->hidden
20 | * 使用Field注解表明字段在数据库中的类型,但实际上并没有作用,因为在读写数据库时使用的是Converter完成的数据转化
21 | */
22 |
23 | @Document(collection = "eventinfos")
24 | @JsonSerialize(using = EventSerializer.class)
25 | @JsonDeserialize(using = EventDeserializer.class)
26 | public class Event {
27 | @Field("id")
28 | private Long id;
29 |
30 | @Field("event")
31 | private String type;
32 | private String operator;
33 | //事件来源
34 | private String source;
35 | //事件发生的时间
36 | private Date time;
37 | //事件被添加到IC的时间
38 | private Date updated;
39 | //用户定义的其它的字段,也可以包含上面定义的字段,但如果二者的值不同,则以上面定义的字段值为准
40 | private Map