StringBoot整合ELK日志收集和搜索自动补全功能(详细图文教程)

@TOC

StringBootELK实现日志收集和搜索自动补全功能(详细图文教程)

<font color=#999AAA >最近在给小组搭建ELK环境,并集成到项目中使用,这里记录下(jdk等基础环境配置请自行百度)</font>

<hr style=" border:solid; width:100px; height:1px;" color=#000000 size=1">

在这里插入图片描述

一、下载ELK的安装包上传并解压

<font color=#999AAA >我这边用到的版本是7.6.1版本,ELK最好版本统一,避免版本兼容导致失败,可以在官网下载,下载可能会慢,我这里下载好了,可以直接用

1.Elasticsearch下载

elasticsearch-7.6.1-linux-x86_64.tar.gz
链接:https://pan.baidu.com/s/1cAswtaYx8_2Q56XPZN_ZJA
提取码:idn3

2.Logstash下载

logstash-7.6.1.tar.gz
链接:https://pan.baidu.com/s/1bXAMXSVEe1zpOx3xIbF8LA
提取码:475x

3.Kibana下载

kibana-7.6.1-linux-x86_64.tar.gz
链接:https://pan.baidu.com/s/1xTxTLEooqCHypMIFB_tPng
提取码:w6ob

上传到自定义目录:

/usr/local/src/software
我的目录结构

4.上传并解压

接下来使用 XShell 工具连接到服务器或者虚拟机(我使用的是阿里云服务器)

ES压缩包解压:进入 /usr/local/src/software 目录
elasticsearch解压命令:tar -zxvf elasticsearch-7.6.1-linux-x86_64.tar.gz
logstash解压命令:tar -zxvf logstash-7.6.1.tar.gz
kibana解压命令:tar -zxvf kibana-7.6.1-linux-x86_64.tar.gz

解压后的文件夹修改下名字,方便输入:mv elasticsearch-7.6.1 elasticsearch7
解压后的文件夹修改下名字,方便输入:mv kibana-7.6.1 kibana7

ES目录介绍

bin:下面存放着Es启动文件         elasticsearch.bat/elasticsearch
config:配置目录
data:数据目录
jdk、lib:Java运行环境以及依赖包
logs:日志目录
modules、plugins:模块及插件目录,head插件可以存放在plugins目录下

二、Elasticsearch配置

1.修改配置文件elasticsearch.yml

<font color=#999AAA >基础配置
ES 本身其实也相当于是一个数据库,为此,我们在 elasticsearch7 文件夹下自己建一个 data 文件夹,用于存放数据:

mdkir data
在这里插入图片描述

进入 data 文件夹下,执行 pwd 命令拷贝下该文件夹的路径,下面配置要用到!

我的 data 路径:

/usr/local/src/software/elasticsearch7/data

修改配置文件elasticsearch.yml,我们进入elasticsearch7 这个文件夹下的config 文件夹,编辑 elasticsearch.yml 文件:


在这里插入图片描述
vim elasticsearch.yml
在这里插入图片描述

跨域问题

http.cors.enabled: true
http.cors.allow-origin: "*"
http.cors.allow-headers: Authorization,X-Requested-With,Content-Length,Content-Type

保存退出esc+(:wq!)

2.新建es用户

<font color=#999AAA >
因为Elasticsearch为了安全起见,不支持root用户直接启动

新建一个用户elasticsearch,命令:

adduser es
//添加一个名为tommy的用户
#passwd tommy   //修改密码
Changing password for user tommy.
New UNIX password:     //在这里输入新密码
Retype new UNIX password:  //再次输入新密码
passwd: all authentication tokens updated successfully.

在software 目录下 赋予 elasticsearch7 这个文件夹的权限给 es用户,命令:

chown -R es:es /usr/local/src/software/elasticsearch7

命令,切换到 es 用户,并重新到 bin 目录下执行 ES服务

su es

3.启动Elasticsearch

在es安装目录bin下执行

./elasticsearch  -d

浏览器访问:http://服务器ip:9200/
显示如下内容表示成功


在这里插入图片描述

4.启动问题:

<font color=#999AAA >
由于某种原因可能导致启动失败,我这边也遇到过一些情况,可以参考解决方案:

4.1:JDK版本问题:

future versions of Elasticsearch will require Java 11; your Java version from [/usr/local/jdk8/jdk1.8.0_291/jre] does not meet this requirement

原因:说明这个版本对应的jdk应该是java11。我们环境中的jdk是java8。其实新版的es是自带了jdk的。但是和我们环境变量的冲突了。

解决方案:修改启动文件,添加如下修改

wget https://download.java.net/java/GA/jdk11/13/GPL/openjdk-11.0.1_linux-x64_bin.tar.gz
tar -xzvf openjdk-11.0.1_linux-x64_bin.tar.gz /opt/
vi bin/elasticsearch

配置自己的jdk11

export JAVA_HOME=/opt/jdk-11.0.1
export PATH=$JAVA_HOME/bin:$PATH

添加jdk判断

if [ -x "$JAVA_HOME/bin/java" ]; then
        JAVA="/opt/jdk-11.0.1/bin/java"
else
        JAVA=`which java`
fi

4.2:内存不足问题处理:

可以修改 config 下的 jvm.options 配置文件,将运行大小 2g 修改为 1g(还是不行的话,再小设置到256m):


在这里插入图片描述

三、Logstash配置

1.新建持久化目录:

mkdir -p /usr/local/src/software/logstash-7.6.1/plugin-data

2.修改配置文件logstash.yml

<font color=#999AAA >编辑 /config/logstash.yml 配置文件

node.name: cpy04.dev.xjh.com    #设置节点名称,一般写主机名
path.data: /usr/local/src/software/logstash-7.6.1/plugin-data    #上一步创建logstash 和插件使用的持久化目录
config.reload.automatic: true    #开启配置文件自动加载
config.reload.interval: 10    #定义配置文件重载时间周期
http.host: "172.18.10.172"    #(阿里云服务器私网IP)定义访问主机名,一般为域名或IP

3.安装logstash所需插件

从Filebeat 输入、过滤、输出至elasticsearch(logstash 有非常多插件,详见官网,此处不列举)
安装logstash-input-jdbc 和logstash-input-beats-master 插件
如果你的Logstash没有安装logstash-codec-json_lines插件,通过以下命令安装

/usr/local/src/software/logstash-7.6.1/bin/logstash-plugin install logstash-integration-jdbc
wget https://github.com/logstash-plugins/logstash-input-beats/archive/master.zip -O /opt/master.zip
unzip -d /usr/local/src/software/logstash-7.6.1 /opt/master.zip
root@iZwz91w9jegcgf28ttbe2yZ:/usr/local/src/software/logstash-7.6.1/bin# ./logstash-plugin install logstash-codec-json_lines
Validating logstash-codec-json_lines
Installing logstash-codec-json_lines
Installation successful

4.新建配置文件 logstash.conf(/bin目录下)

vim logstash.conf

配置文档内容

input {
  tcp {
    #模式选择为server
    mode => "server"
    #阿里云服务器私网ip和端口根据自己情况填写,端口默认4560,我这边改成4567,对应后面整合spring boot的logback.xml里appender中的destination
    host => "172.18.10.172"
    port => 4567
    #格式json
    codec => json_lines
  }
}
filter {
  #过滤器,根据需要填写
}
output {
  elasticsearch {
    action => "index"
    #这里是es的地址(es服务在阿里云服务器私网ip),多个es要写成数组的形式
    hosts  => "172.18.10.172:9200"
    #用于kibana过滤,可以填项目名称
    index  => "springboot-logstash-%{+YYYY.MM.dd}"
  }
}

5.启动logstash

切换到/usr/local/src/software/logstash-7.6.1/bin

nohup ./logstash -f logstash.conf >/dev/null 2>&1 &

验证
访问http://服务器ip:9600/,成功的话会显示一个JSON串


在这里插入图片描述

6.启动问题:

<font color=#999AAA >
由于某种原因可能导致启动失败,我这边也遇到过一些情况,可以参考解决方案:

6.1:内存问题:

:Java HotSpot(TM) 64-Bit Server VM warning: INFO: os::commit_memory(0x00000000c5330000

解决方案;
.修改logstash核心的执行文件 在bin目录下,有一个可执行的文件logstash,需要添加参数:(可以先
不用设置,报错的话再设置)

LS_JAVA_OPTS="-server -Xms256m -Xmx512m -XX:PermSize=128m -XX:MaxPermSize=256m"

2./usr/local/src/software/logstash-7.6.1/config修改jvm.options

-Xms256m
-Xmx256m

四、Kibana配置

1.修改配置文件kibana.yml

<font color=#999AAA >修改内容:

在这里插入图片描述

2.启动kibana

进入bin目录启动:  ./bin/kibana
或者守护进程启动: nohup ./bin/kibana &
或者 nohup ./bin/kibana >/dev/null 2>&1 &  
 
关键在于最后的 >/dev/null 2>&1 部分,/dev/null是一个虚拟的空设备(类似物理中的黑洞),任何输出信息被重定向到该设备后,将会石沉大海
>/dev/null 表示将标准输出信息重定向到"黑洞"
2>&1 表示将标准错误重定向到标准输出(由于标准输出已经定向到“黑洞”了,即:标准输出此时也是"黑洞",再将标准错误输出定向到标准输出,相当于错误输出也被定向至“黑洞”)

进行访问本机访问 http://服务器ip:5601

在这里插入图片描述

五、Spring boot整合ELK实现日志采集

<font color=#999AAA >日志采集新增Logback直接发送日志到Logstash的形式。如果采用此方式,web服务可减少部分生成log文件配置,提高实时性和日志推送效率

在这里插入图片描述

1. 修改 pom.xml

logback 相关

<!-- logback -->
<dependency>
   <groupId>ch.qos.logback</groupId>
   <artifactId>logback-classic</artifactId>
</dependency>

<!-- logback -->

<dependency>
   <groupId>ch.qos.logback</groupId>
   <artifactId>logback-core</artifactId>
</dependency>
<dependency>
   <groupId>ch.qos.logback</groupId>
   <artifactId>logback-classic</artifactId>
</dependency>
<dependency>
   <groupId>ch.qos.logback</groupId>
   <artifactId>logback-access</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/net.logstash.logback/logstash-logback-encoder -->
<dependency>
   <groupId>net.logstash.logback</groupId>
   <artifactId>logstash-logback-encoder</artifactId>
   <version>5.3</version>
</dependency>

2. logback.xml

<?xml version="1.0" encoding="UTF-8"?>


<configuration>
    <include resource="org/springframework/boot/logging/logback/defaults.xml"/>
    <include resource="org/springframework/boot/logging/logback/console-appender.xml"/>
    <!--应用名称-->
    <property name="APP_NAME" value="VBlog"/>
    <!--日志文件保存路径-->
    <property name="LOG_FILE_PATH" value="${LOG_FILE:-${LOG_PATH:-${LOG_TEMP:-${java.io.tmpdir:-/tmp}}}/logs}"/>
    <contextName>${APP_NAME}</contextName>
    <!--每天记录日志到文件appender-->
    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${LOG_FILE_PATH}/${APP_NAME}-%d{yyyy-MM-dd}.log</fileNamePattern>
            <maxHistory>30</maxHistory>
        </rollingPolicy>
        <encoder>
            <pattern>${FILE_LOG_PATTERN}</pattern>
        </encoder>
    </appender>
    <!--输出到logstash的appender-->
    <appender name="LOGSTASH" class="net.logstash.logback.appender.LogstashTcpSocketAppender">
        <!--可以访问的logstash日志收集端口-->
        <destination>172.18.10.172:4567</destination>
        <encoder charset="UTF-8" class="net.logstash.logback.encoder.LogstashEncoder"/>
    </appender>
    <root level="INFO">
        <appender-ref ref="CONSOLE"/>
        <appender-ref ref="FILE"/>
        <appender-ref ref="LOGSTASH"/>
    </root>

</configuration>

3. Test.java

在接口中使用log进行日志生成,启动项目


在这里插入图片描述

4. 进入kibana -> 管理 -> 索引模式 -> 创建索引模式,输入 * ,下一步创建

在这里插入图片描述

5. 调用接口触发日志记录,打开Kibana-->Discover菜单-->筛选" * ",显示请求日志

在这里插入图片描述

六、使用logstash-input-jdbc进行Mysql与ES数据同步

1,下载 mysql-connector-java-8.0.20.jar 包放到安装目录,复制绝对路径下面会用到

2,新建配置jdbc.conf和jdbc.sql文件

在bin目录下创建,
jdbc.sql文件

select
  *
from user

jdbc.conf 配置文件内容如下

input {
    stdin {
    }
    jdbc {
      # mysql相关jdbc配置
      jdbc_connection_string => "jdbc:mysql://mysqlip:3306/vueblog2"
      jdbc_user => "root"
      jdbc_password => "***"
 
      # jdbc连接mysql驱动的文件目录,可去官网下载:https:#dev.mysql.com/downloads/connector/j/
      jdbc_driver_library => "/usr/local/src/software/logstash-7.6.1/mysql-connector-java-8.0.20.jar"
      # the name of the driver class for mysql
      jdbc_driver_class => "com.mysql.jdbc.Driver"
      jdbc_paging_enabled => "true"
      jdbc_page_size => "50000"

      # mysql文件, 也可以直接写SQL语句在此处,如下:
      # statement => "SELECT * from user;"
      statement_filepath => "/usr/local/src/software/logstash-7.6.1/bin/jdbc.sql"

      # 这里类似crontab,定时字段 各字段含义(由左至右)分、时、天、月、年,全部为*默认含义为每分钟都更新,比如每10分钟("*/10 * * * *")执行一次同步
      schedule => "* * * * *"
      #设定ES索引类型
      type => "jdbc"

      # 是否记录上次执行结果, 如果为真,将会把上次执行到的 tracking_column 字段的值记录下来,保存到 last_run_metadata_path 指定的文件中
      #record_last_run => "true"

      # 是否需要记录某个column 的值,如果record_last_run为真,可以自定义我们需要 track 的 column 名称,此时该参数就要为 true. 否则默认 track 的是 timestamp 的值.
      #use_column_value => "true"

      # 如果 use_column_value 为真,需配置此参数. track 的数据库 column 名,该 column 必须是递增的. 一般是mysql主键
      #tracking_column => "id"

      #last_run_metadata_path => "/opt/logstash/conf/last_id"

      # 是否清除 last_run_metadata_path 的记录,如果为真那么每次都相当于从头开始查询所有的数据库记录
      #clean_run => "false"

      #是否将 字段(column) 名称转小写
      #lowercase_column_names => "false"
    }
}

过滤处理,如果需要,也可参考elk安装那篇

filter {

  json {
  source => "message"
  remove_field => ["message"]
  }

}

output {
    # 输出到elasticsearch的配置
    elasticsearch {
        hosts => ["http://ESIP:9200/"]
        index => "jdbc"
        document_type => "user"
        #自增ID编号
        document_id => "%{id}"
        template_overwrite => true
    }

    # 这里输出调试,正式运行时可以注释掉
    stdout {
        codec => json_lines
    }
}

在logstash安装目录bin下面创建文件夹 logstash_jdbc,将之前的 logstash.conf和刚才创建的jdbc.conf 移动到 logstash_jdbc文件夹下。后面启动logstash会用到

3,然后到bin目录下启动 Logstash

nohup ./logstash -f logstash_jdbc/ >/dev/null 2>&1 &

七、使用ES实现简单的搜索自动补全功能

1,安装es客户端pom.xml依赖包

<!--es客户端-->
<dependency>
   <groupId>org.elasticsearch.client</groupId>
   <artifactId>elasticsearch-rest-high-level-client</artifactId>
   <version>7.6.1</version>
   <exclusions>
      <exclusion>
         <groupId>org.elasticsearch</groupId>
         <artifactId>elasticsearch</artifactId>
      </exclusion>
      <exclusion>
         <groupId>org.elasticsearch.client</groupId>
         <artifactId>elasticsearch-rest-client</artifactId>
      </exclusion>
   </exclusions>
</dependency>
<dependency>
   <groupId>org.elasticsearch.client</groupId>
   <artifactId>elasticsearch-rest-client</artifactId>
   <version>7.6.1</version>
</dependency>
<dependency>
   <groupId>org.elasticsearch</groupId>
   <artifactId>elasticsearch</artifactId>
   <version>7.6.1</version>
</dependency>

2,编写ES-Java客户端调用代码

/**
 * Created by fansongsong
 */
@Component
@Service
public class Util {

    public static User getCurrentUser() {
        User user = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        return user;
    }

    private static RestHighLevelClient client;
    public Util() {
        RestHighLevelClient client = new RestHighLevelClient(
                RestClient.builder(
                        new HttpHost("ESIP", 9200, "http")));
        this.client = client;
    }
    public void shutdown(){
        if(client!=null){
            try {
                client.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }


    /**
     * 根据指定的内容,查询所有Doc。
     * @return
     */
    public static List<Map<String,Object>> searchArticle(String index,String key, String value){
        SearchHits searchHits = search(index,key,value);
        List<Map<String,Object>> list = new ArrayList<>();
        for(SearchHit hit:searchHits.getHits()){
            System.out.println( hit.getSourceAsString());
            Map<String,Object> stringObjectMap = hit.getSourceAsMap();
            stringObjectMap.put("name", stringObjectMap.get("nickname"));
            list.add(stringObjectMap);
        }
        return list;
    }

    public static SearchHits search(String index, String key, String value){
        QueryBuilder matchQueryBuilder = QueryBuilders.matchPhraseQuery(key, value);
//        matchQueryBuilder.
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
//        sourceBuilder.query(QueryBuilders.termQuery("content", content));
        sourceBuilder.query(matchQueryBuilder);
        sourceBuilder.from(0);
        sourceBuilder.size(100);
        sourceBuilder.timeout(new TimeValue(60, TimeUnit.SECONDS));
        SearchRequest searchRequest = new SearchRequest();
        searchRequest.indices(index);
        searchRequest.source(sourceBuilder);
        SearchResponse searchResponse;
        List<Map<String,Object>> list = new ArrayList<>();
        SearchHits searchHits = null;
        try {
            searchResponse = client.search(searchRequest,RequestOptions.DEFAULT);
            searchHits =  searchResponse.getHits();
            for(SearchHit hit:searchHits.getHits()){
                System.out.println( hit.getSourceAsString());
                list.add(hit.getSourceAsMap());
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return searchHits;
    }

}

3,Service

public List<String> getUserByNicknameKeyword(String nickname) {
    List<Map<String, Object>> mapList = Util.searchArticle("jdbc", "nickname", nickname);
    List<String> goodsNameList = mapList.stream().map(m->m.get("nickname").toString()).collect(Collectors.toList());
    log.info(nickname+"==>搜索自动补全:"+JSONArray.toJSONString(mapList));
    return goodsNameList;
}

4,Controller

@RestController
@RequestMapping("/admin")
public class UserManaController {
    @Autowired
    UserService userService;
        
    @RequestMapping(value = "/user/keyword", method = RequestMethod.GET)
    @ResponseBody
    public List<?> getUserByNicknameKeyword(String nickname) {
        return userService.getUserByNicknameKeyword(nickname);
    }
}

前端代码vue

<div style="margin-top: 10px;display: flex;justify-content: center">
  <input
    placeholder="默认展示部分用户,可以通过用户名搜索用户..."
    prefix-icon="el-icon-search"
    v-model="keywords" style="width: 400px" size="small"
    @keyup="get($event)" @keydown.down.prevent="selectDown"
    @keydown.up.prevent="selectUp"
  >

  <el-button type="primary" icon="el-icon-search" size="small" style="margin-left: 3px" @click="searchClick">搜索
  </el-button>
</div>
<div style="margin-top: 10px;display: flex;justify-content: center">
  <ul>
    <li class="text-center" v-for="(value,index) in myData"><span class="text-success textprimary" :class="{gray:index==now}">{{value}}</span></li>
  </ul>
</div>
methods: {  
    get:function (event) {
        if(event.keyCode==38||event.keyCode==40)return;
        var _this = this;
        getRequest("/admin/user/keyword?nickname="+this.keywords).then(resp=> {
          _this.loading = false;
          if (resp.status == 200) {
            _this.myData = resp.data;
          } else {
            _this.$message({type: 'error', message: '数据加载失败!'});
          }
        }, resp=> {
          _this.loading = false;
          if (resp.response.status == 403) {
            var data = resp.response.data;
            _this.$message({type: 'error', message: data});
          }
        });
    },
    selectDown:function () {
        this.now++;
        if(this.now==this.myData.length)this.now=-1;
        this.keywords=this.myData[this.now];
    },
    selectUp:function () {
        this.now--;
        if(this.now==-2)this.now=this.myData.length-1;
        this.keywords=this.myData[this.now];
      }
    },
    data(){
      return {
        loading: false,     
        keywords: '',     
        myData:[],
        now:-1
      }
    }
      

显示效果


在这里插入图片描述
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,530评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,403评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,120评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,770评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,758评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,649评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,021评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,675评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,931评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,751评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,410评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,004评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,969评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,042评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,493评论 2 343

推荐阅读更多精彩内容